diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..078077f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +vendor +example +coverage.txt +coverage +node_modules +*.log +graylog-mock-server +dist +docker-compose.yml +env.sh diff --git a/.gometalinter.json b/.gometalinter.json new file mode 100644 index 00000000..6a5077ca --- /dev/null +++ b/.gometalinter.json @@ -0,0 +1,21 @@ +{ + "DisableAll": true, + "Enable": [ + "vet", + "nakedret", + "misspell", + "gosimple", + "unused", + "maligned", + "interfacer", + "unconvert", + "goimports", + "gofmt", + + "varcheck", + "structcheck", + "goconst" + ], + "Vendor": true, + "Deadline": "300s" +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..b19277fd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +--- +language: go +go: + - 1.x +before_install: + - node -v + - npm -v +install: + - npm install + - go get -t ./... + - go get -u gopkg.in/alecthomas/gometalinter.v2 + - ln -s $GOPATH/bin/gometalinter.v2 $GOPATH/bin/gometalinter + - gometalinter --install +script: + - npm run commitlint-travis + - npm run lint + - bash codecov-test.sh +after_success: + - bash codecov.sh + - bash upload.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..2b6cf451 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contributing + +## Requirements + +* [npm](https://www.npmjs.com/): to validate a commit message and generate the Change Log +* [Golang](https://golang.org/) +* [dep](https://golang.github.io/dep/) +* [gometalinter](https://github.com/alecthomas/gometalinter) + +``` +$ npm i +``` + +## Test + +``` +$ npm t +``` + +## Commit Message Format + +The commit message format of this project conforms to the [AngularJS Commit Message Format](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format). +By conforming its format, we can generate the [Change Log](CHANGELOG.md) and conform the [semantic versioning](http://semver.org/) automatically by [standard-version](https://www.npmjs.com/package/standard-version). +We validate the commit message with git's `commit-msg` hook using [commitlint](http://marionebl.github.io/commitlint/#/) and [husky](https://www.npmjs.com/package/husky). + +## Coding Guide + +* https://github.com/golang/go/wiki/CodeReviewComments +* https://github.com/alecthomas/gometalinter + +``` +$ go get -u github.com/alecthomas/gometalinter +$ gometalinter --install +``` + +``` +$ npm run lint +``` + +## docker-compose.yml + +To run graylog using docker for development, we prepare the template of `docker-compose.yml`. + + +``` +$ cp docker-compose.yml.tmpl docker-compose.yml +$ docker-compose up -d +``` + +## env.sh + +To set environment variables for development, we prepare the template of `setenv.sh` . + +``` +$ cp env.sh.tmpl env.sh +``` + +## Develop terraform provider + +* https://www.terraform.io/docs/plugins/provider.html +* https://www.terraform.io/guides/writing-custom-terraform-providers.html +* https://godoc.org/github.com/hashicorp/terraform/helper/schema +* https://godoc.org/github.com/hashicorp/terraform/helper/resource#TestCase diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 00000000..a6999035 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,512 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/agext/levenshtein" + packages = ["."] + revision = "5f10fee965225ac1eecdc234c09daf5cd9e7f7b6" + version = "v1.2.1" + +[[projects]] + branch = "master" + name = "github.com/apparentlymart/go-cidr" + packages = ["cidr"] + revision = "2bd8b58cf4275aeb086ade613de226773e29e853" + +[[projects]] + branch = "master" + name = "github.com/apparentlymart/go-textseg" + packages = ["textseg"] + revision = "b836f5c4d331d1945a2fead7188db25432d73b69" + +[[projects]] + branch = "master" + name = "github.com/armon/go-radix" + packages = ["."] + revision = "1fca145dffbcaa8fe914309b1ec0cfc67500fe61" + +[[projects]] + name = "github.com/aws/aws-sdk-go" + packages = [ + "aws", + "aws/awserr", + "aws/awsutil", + "aws/client", + "aws/client/metadata", + "aws/corehandlers", + "aws/credentials", + "aws/credentials/ec2rolecreds", + "aws/credentials/endpointcreds", + "aws/credentials/stscreds", + "aws/defaults", + "aws/ec2metadata", + "aws/endpoints", + "aws/request", + "aws/session", + "aws/signer/v4", + "internal/sdkio", + "internal/sdkrand", + "internal/shareddefaults", + "private/protocol", + "private/protocol/query", + "private/protocol/query/queryutil", + "private/protocol/rest", + "private/protocol/restxml", + "private/protocol/xml/xmlutil", + "service/s3", + "service/sts" + ] + revision = "74ca63959be8b9a9a24cd86eaaea8bf99de8022d" + version = "v1.13.25" + +[[projects]] + branch = "master" + name = "github.com/bgentry/go-netrc" + packages = ["netrc"] + revision = "9fd32a8b3d3d3f9d43c341bfe098430e07609480" + +[[projects]] + name = "github.com/bgentry/speakeasy" + packages = ["."] + revision = "4aabc24848ce5fd31929f7d1e4ea74d3709c14cd" + version = "v0.1.0" + +[[projects]] + name = "github.com/blang/semver" + packages = ["."] + revision = "2ee87856327ba09384cabd113bc6b5d174e9ec0f" + version = "v3.5.1" + +[[projects]] + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" + +[[projects]] + name = "github.com/go-ini/ini" + packages = ["."] + revision = "6333e38ac20b8949a8dd68baa3650f4dee8f39f0" + version = "v1.33.0" + +[[projects]] + name = "github.com/go-playground/locales" + packages = [ + ".", + "currency" + ] + revision = "e4cbcb5d0652150d40ad0646651076b6bd2be4f6" + version = "v0.11.2" + +[[projects]] + name = "github.com/go-playground/universal-translator" + packages = ["."] + revision = "b32fa301c9fe55953584134cb6853a13c87ec0a1" + version = "v0.16.0" + +[[projects]] + name = "github.com/golang/protobuf" + packages = [ + "proto", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/timestamp" + ] + revision = "925541529c1fa6821df4e44ce2723319eb2be768" + version = "v1.0.0" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/errwrap" + packages = ["."] + revision = "7554cd9344cec97297fa6649b055a8c98c2a1e55" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/go-cleanhttp" + packages = ["."] + revision = "d5fe4b57a186c716b0e00b8c301cbd9b4182694d" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/go-getter" + packages = [ + ".", + "helper/url" + ] + revision = "64040d90d4ab861e7e833d689dc76a0f176d8dec" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/go-hclog" + packages = ["."] + revision = "5bcb0f17e36442247290887cc914a6e507afa5c4" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/go-multierror" + packages = ["."] + revision = "b7773ae218740a7be65057fc60b366a49b538a44" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/go-plugin" + packages = ["."] + revision = "e8d22c780116115ae5624720c9af0c97afe4f551" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/go-uuid" + packages = ["."] + revision = "27454136f0364f2d44b1276c552d69105cf8c498" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/go-version" + packages = ["."] + revision = "23480c0665776210b5fbbac6eaaee40e3e6a96b7" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/hcl" + packages = [ + ".", + "hcl/ast", + "hcl/parser", + "hcl/scanner", + "hcl/strconv", + "hcl/token", + "json/parser", + "json/scanner", + "json/token" + ] + revision = "f40e974e75af4e271d97ce0fc917af5898ae7bda" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/hcl2" + packages = [ + "gohcl", + "hcl", + "hcl/hclsyntax", + "hcl/json", + "hcldec", + "hclparse" + ] + revision = "5f8ed954abd873b2c09616ba0aa607892bbca7e9" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/hil" + packages = [ + ".", + "ast", + "parser", + "scanner" + ] + revision = "fa9f258a92500514cc8e9c67020487709df92432" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/logutils" + packages = ["."] + revision = "0dc08b1671f34c4250ce212759ebd880f743d883" + +[[projects]] + name = "github.com/hashicorp/terraform" + packages = [ + "config", + "config/configschema", + "config/hcl2shim", + "config/module", + "dag", + "flatmap", + "helper/config", + "helper/hashcode", + "helper/hilmapstructure", + "helper/logging", + "helper/resource", + "helper/schema", + "httpclient", + "moduledeps", + "plugin", + "plugin/discovery", + "registry", + "registry/regsrc", + "registry/response", + "svchost", + "svchost/auth", + "svchost/disco", + "terraform", + "tfdiags", + "version" + ] + revision = "342c529aa58d824f2f2ed1f6ec6118059876aad0" + version = "v0.11.5" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/yamux" + packages = ["."] + revision = "2658be15c5f05e76244154714161f17e3e77de2e" + +[[projects]] + name = "github.com/jmespath/go-jmespath" + packages = ["."] + revision = "0b12d6b5" + +[[projects]] + name = "github.com/julienschmidt/httprouter" + packages = ["."] + revision = "8c199fb6259ffc1af525cc3ad52ee60ba8359669" + version = "v1.1" + +[[projects]] + name = "github.com/mattn/go-isatty" + packages = ["."] + revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" + version = "v0.0.3" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/cli" + packages = ["."] + revision = "b068abc08c994321eafd9fd500d0704cb6defcb1" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/copystructure" + packages = ["."] + revision = "d23ffcb85de31694d6ccaa23ccb4a03e55c1303f" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/go-homedir" + packages = ["."] + revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/go-testing-interface" + packages = ["."] + revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/go-wordwrap" + packages = ["."] + revision = "ad45545899c7b13c020ea92b2072220eefad42b8" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/hashstructure" + packages = ["."] + revision = "2bca23e0e452137f789efbc8610126fd8b94f73b" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/mapstructure" + packages = ["."] + revision = "00c29f56e2386353d58c599509e8dc3801b0d716" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/reflectwalk" + packages = ["."] + revision = "63d60e9d0dbc60cf9164e6510889b0db6683d98c" + +[[projects]] + name = "github.com/oklog/run" + packages = ["."] + revision = "4dadeb3030eda0273a12382bb2348ffc7c9d1a39" + version = "v1.0.0" + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + name = "github.com/posener/complete" + packages = [ + ".", + "cmd", + "cmd/install", + "match" + ] + revision = "98eb9847f27ba2008d380a32c98be474dea55bdf" + version = "v1.1.1" + +[[projects]] + branch = "master" + name = "github.com/satori/go.uuid" + packages = ["."] + revision = "36e9d2ebbde5e3f13ab2e25625fd453271d6522e" + +[[projects]] + name = "github.com/sirupsen/logrus" + packages = ["."] + revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" + version = "v1.0.4" + +[[projects]] + name = "github.com/suzuki-shunsuke/go-ptr" + packages = ["."] + revision = "974e4afeaf714a288594b077987a10416b15a03c" + version = "v0.1.0" + +[[projects]] + name = "github.com/suzuki-shunsuke/go-set" + packages = ["."] + revision = "8193d543c32a1e7779167a533bb48c4bc82b42b9" + version = "v2.3.2" + +[[projects]] + name = "github.com/ulikunitz/xz" + packages = [ + ".", + "internal/hash", + "internal/xlog", + "lzma" + ] + revision = "0c6b41e72360850ca4f98dc341fd999726ea007f" + version = "v0.5.4" + +[[projects]] + branch = "master" + name = "github.com/zclconf/go-cty" + packages = [ + "cty", + "cty/convert", + "cty/function", + "cty/function/stdlib", + "cty/gocty", + "cty/json", + "cty/set" + ] + revision = "d006e4534bc4fbc512383aa98d04d641ea951ba5" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = [ + "bcrypt", + "blowfish", + "cast5", + "openpgp", + "openpgp/armor", + "openpgp/elgamal", + "openpgp/errors", + "openpgp/packet", + "openpgp/s2k", + "ssh/terminal" + ] + revision = "49796115aa4b964c318aad4f3084fdb41e9aa067" + +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = [ + "context", + "html", + "html/atom", + "http2", + "http2/hpack", + "idna", + "internal/timeseries", + "lex/httplex", + "trace" + ] + revision = "b68f30494add4df6bd8ef5e82803f308e7f7c59c" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = [ + "unix", + "windows" + ] + revision = "f6cff0780e542efa0c8e864dc8fa522808f6a598" + +[[projects]] + name = "golang.org/x/text" + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable" + ] + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" + +[[projects]] + branch = "master" + name = "google.golang.org/genproto" + packages = ["googleapis/rpc/status"] + revision = "ab0870e398d5dd054b868c0db1481ab029b9a9f2" + +[[projects]] + name = "google.golang.org/grpc" + packages = [ + ".", + "balancer", + "balancer/base", + "balancer/roundrobin", + "codes", + "connectivity", + "credentials", + "encoding", + "encoding/proto", + "grpclb/grpc_lb_v1/messages", + "grpclog", + "health", + "health/grpc_health_v1", + "internal", + "keepalive", + "metadata", + "naming", + "peer", + "resolver", + "resolver/dns", + "resolver/passthrough", + "stats", + "status", + "tap", + "transport" + ] + revision = "1e2570b1b19ade82d8dbb31bba4e65e9f9ef5b34" + version = "v1.11.1" + +[[projects]] + name = "gopkg.in/go-playground/validator.v9" + packages = ["."] + revision = "150fe5b6a4ccc9cd6ffdbf5d184b67fbea75efcc" + version = "v9.11.0" + +[[projects]] + branch = "v2" + name = "gopkg.in/mgo.v2" + packages = [ + "bson", + "internal/json" + ] + revision = "3f83fa5005286a7fe593b055f0d7771a7dce4655" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "c696b953d655211dce044b97c16561108aa2dc0d2b8e85e638b180a9ddfe69e5" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 00000000..92a4842e --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,38 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + name = "github.com/satori/go.uuid" + branch = "master" + +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" + +[prune] + go-tests = true + unused-packages = true diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..24242185 --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +TAG=edge + +coverage: + mkdir coverage +t: + go test ./... -covermode=atomic +cover: coverage + go test . -coverprofile=coverage/main.txt -covermode=atomic + go tool cover -html=coverage/main.txt +cover-client: coverage + go test ./client -coverprofile=coverage/client.txt -covermode=atomic + go tool cover -html=coverage/client.txt +cover-endpoint: coverage + go test ./client/endpoint -coverprofile=coverage/endpoint.txt -covermode=atomic + go tool cover -html=coverage/endpoint.txt +cover-mockserver: coverage *.go mockserver/*.go + go test ./mockserver -coverprofile=coverage/mockserver.txt -covermode=atomic + go tool cover -html=coverage/mockserver.txt +cover-logic: coverage + go test ./mockserver/logic -coverprofile=coverage/logic.txt -covermode=atomic + go tool cover -html=coverage/logic.txt +cover-store: coverage + go test ./mockserver/store -coverprofile=coverage/store.txt -covermode=atomic + go tool cover -html=coverage/store.txt +cover-plain: coverage + go test ./mockserver/store/plain -coverprofile=coverage/plain.txt -covermode=atomic + go tool cover -html=coverage/plain.txt +build: + gox -output="dist/$(TAG)/graylog-mock-server_$(TAG)_{{.OS}}_{{.Arch}}" -osarch="darwin/amd64 linux/amd64 windows/amd64" ./mockserver/exec + gox -output="dist/$(TAG)/terraform-provider-graylog_$(TAG)_{{.OS}}_{{.Arch}}" -osarch="darwin/amd64 linux/amd64 windows/amd64" ./terraform +upload: + # GITHUB_TOKEN envrionment variable + ghr -u suzuki-shunsuke $(TAG) dist/$(TAG) +upload-dep: + go get -u github.com/tcnksm/ghr + go get github.com/mitchellh/gox diff --git a/README.md b/README.md index 1bab6a99..389f8383 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,67 @@ # go-graylog -Graylog API client for golang + +[![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/suzuki-shunsuke/go-graylog) +[![Build Status](https://travis-ci.org/suzuki-shunsuke/go-graylog.svg?branch=master)](https://travis-ci.org/suzuki-shunsuke/go-graylog) +[![codecov](https://codecov.io/gh/suzuki-shunsuke/go-graylog/branch/master/graph/badge.svg)](https://codecov.io/gh/suzuki-shunsuke/go-graylog) +[![Go Report Card](https://goreportcard.com/badge/github.com/suzuki-shunsuke/go-graylog)](https://goreportcard.com/report/github.com/suzuki-shunsuke/go-graylog) +[![GitHub last commit](https://img.shields.io/github/last-commit/suzuki-shunsuke/go-graylog.svg)](https://github.com/suzuki-shunsuke/go-graylog) +[![GitHub tag](https://img.shields.io/github/tag/suzuki-shunsuke/go-graylog.svg)](https://github.com/suzuki-shunsuke/go-graylog/releases) +[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/suzuki-shunsuke/go-graylog/master/LICENSE) + +[Graylog](https://www.graylog.org/) API client and mock server for Golang and terraform provider for Graylog. + +## Supported APIs + +Graylog provides very various APIs so we can't support all of them yet. +Please check the following godoc's Client methods. + +https://godoc.org/github.com/suzuki-shunsuke/go-graylog/client + +## Example - client and mock server + +* https://godoc.org/github.com/suzuki-shunsuke/go-graylog/client/#example_Client + +## Mock Server CLI tool + +Download a binary from [the release page](https://github.com/suzuki-shunsuke/go-graylog/releases). + +``` +$ graylog-mock-server --help +graylog-mock-server - Run Graylog mock server. + +USAGE: + graylog-mock-server [options] + +VERSION: + 0.1.0 + +OPTIONS: + --port value port number. If you don't set this option, a free port is assigned and the assigned port number is outputed to the console when the mock server runs. + --log-level value the log level of logrus which the mock server uses internally. (default: "info") + --data value data file path. When the server runs data of the file is loaded and when data of the server is changed data is saved at the file. If this option is not set, no data is loaded and saved. + --help, -h show help + --version, -v print the version +``` + +## Terraform provider + +* [terraform-provider-graylog](https://github.com/suzuki-shunsuke/go-graylog/terraform) + +## Supported Graylog version + +We use [the graylog's official Docker Image](https://hub.docker.com/r/graylog/graylog/) for development. + +The version is `2.4.3-1` . + +## Contribution + +See [CONTRIBUTING.md](CONTRIBUTING.md) . + +## See also + +* http://docs.graylog.org/en/2.4/pages/configuration/rest_api.html +* http://docs.graylog.org/en/2.4/pages/users_and_roles/permission_system.html + +## License + +[MIT](LICENSE) diff --git a/client/README.md b/client/README.md new file mode 100644 index 00000000..c808d3eb --- /dev/null +++ b/client/README.md @@ -0,0 +1,13 @@ +# go-graylog client + +[![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/suzuki-shunsuke/go-graylog/client) +[![Build Status](https://travis-ci.org/suzuki-shunsuke/go-graylog.svg?branch=master)](https://travis-ci.org/suzuki-shunsuke/go-graylog) +[![codecov](https://codecov.io/gh/suzuki-shunsuke/go-graylog/branch/master/graph/badge.svg)](https://codecov.io/gh/suzuki-shunsuke/go-graylog) +[![Go Report Card](https://goreportcard.com/badge/github.com/suzuki-shunsuke/go-graylog)](https://goreportcard.com/report/github.com/suzuki-shunsuke/go-graylog) +[![GitHub last commit](https://img.shields.io/github/last-commit/suzuki-shunsuke/go-graylog.svg)](https://github.com/suzuki-shunsuke/go-graylog) +[![GitHub tag](https://img.shields.io/github/tag/suzuki-shunsuke/go-graylog.svg)](https://github.com/suzuki-shunsuke/go-graylog/releases) +[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/suzuki-shunsuke/go-graylog/master/LICENSE) + +[Graylog](https://www.graylog.org/) API client for Golang. + +See https://github.com/suzuki-shunsuke/go-graylog diff --git a/client/client.go b/client/client.go new file mode 100644 index 00000000..30ccfe83 --- /dev/null +++ b/client/client.go @@ -0,0 +1,40 @@ +package client + +import ( + "github.com/suzuki-shunsuke/go-graylog/client/endpoint" +) + +// Client represents a Graylog API client. +type Client struct { + name string + password string + endpoints *endpoint.Endpoints +} + +// NewClient returns a new Graylog API Client. +// ep is API endpoint url (ex. http://localhost:9000/api). +// name and password are authentication name and password. +// If you use an access token instead of password, name is access token and password is literal password "token". +// If you use a session token instead of password, name is session token and password is literal password "session". +func NewClient(ep string, name, password string) (*Client, error) { + endpoints, err := endpoint.NewEndpoints(ep) + if err != nil { + return nil, err + } + return &Client{name: name, password: password, endpoints: endpoints}, nil +} + +// Endpoints returns endpoints. +func (client *Client) Endpoints() *endpoint.Endpoints { + return client.endpoints +} + +// Name returns authentication name. +func (client *Client) Name() string { + return client.name +} + +// Password returns authentication password. +func (client *Client) Password() string { + return client.password +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 00000000..185874fa --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,57 @@ +package client_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/client" +) + +const ( + endpoint = "http://localhost:9000/api" +) + +func TestNewClient(t *testing.T) { + client, err := client.NewClient(endpoint, "admin", "password") + if err != nil { + t.Fatal("Failed to NewClient", err) + } + if client == nil { + t.Fatal("client == nil") + } +} + +func TestName(t *testing.T) { + name := "admin" + client, err := client.NewClient(endpoint, name, "password") + if err != nil { + t.Fatal("Failed to NewClient", err) + } + if client == nil { + t.Fatal("client == nil") + } + act := client.Name() + if act != name { + t.Fatalf("client.Name() == %s, wanted %s", act, name) + } + + exp := "http://localhost:9000/api/roles" + act = client.Endpoints().Roles() + if act != exp { + t.Fatalf("client.Endpoints().Roles == %s, wanted %s", act, exp) + } +} + +func TestPassword(t *testing.T) { + password := "password" + client, err := client.NewClient(endpoint, "admin", password) + if err != nil { + t.Fatal("Failed to NewClient", err) + } + if client == nil { + t.Fatal("client == nil") + } + real := client.Password() + if real != password { + t.Fatalf("client.Password() == %s, wanted %s", real, password) + } +} diff --git a/client/doc.go b/client/doc.go new file mode 100644 index 00000000..e559e0cc --- /dev/null +++ b/client/doc.go @@ -0,0 +1,4 @@ +/* +Package client provides Graylog API client. +*/ +package client diff --git a/client/endpoint/doc.go b/client/endpoint/doc.go new file mode 100644 index 00000000..232a5133 --- /dev/null +++ b/client/endpoint/doc.go @@ -0,0 +1,4 @@ +/* +Package endpoint provides Graylog API endpoints. +*/ +package endpoint diff --git a/client/endpoint/endpoint.go b/client/endpoint/endpoint.go new file mode 100644 index 00000000..90c1adbf --- /dev/null +++ b/client/endpoint/endpoint.go @@ -0,0 +1,70 @@ +package endpoint + +import ( + "fmt" + "net/url" + "path" +) + +func urlJoin(ep *url.URL, arg string) (*url.URL, error) { + return ep.Parse(path.Join(ep.Path, arg)) +} + +// Endpoints represents each API's endpoint URLs. +type Endpoints struct { + roles *url.URL + users *url.URL + inputs *url.URL + indexSets *url.URL + indexSetStats *url.URL + streams *url.URL + enabledStreams *url.URL +} + +// NewEndpoints returns a new Endpoints. +func NewEndpoints(endpoint string) (*Endpoints, error) { + if endpoint == "" { + return nil, fmt.Errorf("endpoint is required") + } + ep, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + roles, err := urlJoin(ep, "roles") + if err != nil { + return nil, err + } + users, err := urlJoin(ep, "users") + if err != nil { + return nil, err + } + inputs, err := urlJoin(ep, "system/inputs") + if err != nil { + return nil, err + } + indexSets, err := urlJoin(ep, "system/indices/index_sets") + if err != nil { + return nil, err + } + indexSetStats, err := urlJoin(indexSets, "stats") + if err != nil { + return nil, err + } + streams, err := urlJoin(ep, "streams") + if err != nil { + return nil, err + } + enabledStreams, err := urlJoin(streams, "enabled") + if err != nil { + return nil, err + } + return &Endpoints{ + roles: roles, + users: users, + inputs: inputs, + indexSets: indexSets, + indexSetStats: indexSetStats, + streams: streams, + enabledStreams: enabledStreams, + }, nil +} diff --git a/client/endpoint/endpoint_test.go b/client/endpoint/endpoint_test.go new file mode 100644 index 00000000..f7f6c483 --- /dev/null +++ b/client/endpoint/endpoint_test.go @@ -0,0 +1,24 @@ +package endpoint_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/client/endpoint" +) + +const ( + apiURL = "http://localhost:9000/api" + ID = "5a8c086fc006c600013ca6f5" +) + +func TestNewEndpoints(t *testing.T) { + if _, err := endpoint.NewEndpoints(""); err == nil { + t.Fatal("invalid argument") + } + if _, err := endpoint.NewEndpoints(":hoge"); err == nil { + t.Fatal("invalid argument") + } + if _, err := endpoint.NewEndpoints("http://localhost:9000/api"); err != nil { + t.Fatal(err) + } +} diff --git a/client/endpoint/index_set.go b/client/endpoint/index_set.go new file mode 100644 index 00000000..9408da9a --- /dev/null +++ b/client/endpoint/index_set.go @@ -0,0 +1,31 @@ +package endpoint + +import ( + "net/url" + "path" +) + +// IndexSet returns an IndexSet API's endpoint url. +func (ep *Endpoints) IndexSet(id string) (*url.URL, error) { + return urlJoin(ep.indexSets, id) +} + +// IndexSets returns an IndexSet API's endpoint url. +func (ep *Endpoints) IndexSets() string { + return ep.indexSets.String() +} + +// SetDefaultIndexSet returns SetDefaultIndexSet API's endpoint url. +func (ep *Endpoints) SetDefaultIndexSet(id string) (*url.URL, error) { + return urlJoin(ep.indexSets, path.Join(id, "default")) +} + +// IndexSetsStats returns all IndexSets stats API's endpoint url. +func (ep *Endpoints) IndexSetsStats() string { + return ep.indexSetStats.String() +} + +// IndexSetStats returns an IndexSet stats API's endpoint url. +func (ep *Endpoints) IndexSetStats(id string) (*url.URL, error) { + return urlJoin(ep.indexSets, path.Join(id, "stats")) +} diff --git a/client/endpoint/index_set_test.go b/client/endpoint/index_set_test.go new file mode 100644 index 00000000..363a839b --- /dev/null +++ b/client/endpoint/index_set_test.go @@ -0,0 +1,75 @@ +package endpoint_test + +import ( + "fmt" + "testing" + + "github.com/suzuki-shunsuke/go-graylog/client/endpoint" +) + +func TestIndexSets(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s", apiURL, "system/indices/index_sets") + if ep.IndexSets() != exp { + t.Fatalf(`ep.IndexSets() = "%s", wanted "%s"`, ep.IndexSets(), exp) + } +} + +func TestIndexSet(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s/%s", apiURL, "system/indices/index_sets", ID) + act, err := ep.IndexSet(ID) + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.IndexSet("%s") = "%s", wanted "%s"`, ID, act.String(), exp) + } +} + +func TestSetDefaultIndexSet(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s/%s/default", apiURL, "system/indices/index_sets", ID) + act, err := ep.SetDefaultIndexSet(ID) + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.SetDefaultIndexSet("%s") = "%s", wanted "%s"`, ID, act.String(), exp) + } +} + +func TestIndexSetsStats(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s", apiURL, "system/indices/index_sets/stats") + if ep.IndexSetsStats() != exp { + t.Fatalf(`ep.IndexSetsStats() = "%s", wanted "%s"`, ep.IndexSetsStats(), exp) + } +} + +func TestIndexSetStats(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s/%s/stats", apiURL, "system/indices/index_sets", ID) + act, err := ep.IndexSetStats(ID) + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.IndexSetStats("%s") = "%s", wanted "%s"`, ID, act.String(), exp) + } +} diff --git a/client/endpoint/input.go b/client/endpoint/input.go new file mode 100644 index 00000000..5de784ef --- /dev/null +++ b/client/endpoint/input.go @@ -0,0 +1,15 @@ +package endpoint + +import ( + "net/url" +) + +// Inputs returns an Input API's endpoint url. +func (ep *Endpoints) Inputs() string { + return ep.inputs.String() +} + +// Input returns an Input API's endpoint url. +func (ep *Endpoints) Input(id string) (*url.URL, error) { + return urlJoin(ep.inputs, id) +} diff --git a/client/endpoint/input_test.go b/client/endpoint/input_test.go new file mode 100644 index 00000000..b2cdd422 --- /dev/null +++ b/client/endpoint/input_test.go @@ -0,0 +1,35 @@ +package endpoint_test + +import ( + "fmt" + "testing" + + "github.com/suzuki-shunsuke/go-graylog/client/endpoint" +) + +func TestInputs(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s", apiURL, "system/inputs") + act := ep.Inputs() + if act != exp { + t.Fatalf(`ep.Inputs() = "%s", wanted "%s"`, act, exp) + } +} + +func TestInput(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s/%s", apiURL, "system/inputs", ID) + act, err := ep.Input(ID) + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.Input("%s") = "%s", wanted "%s"`, ID, act.String(), exp) + } +} diff --git a/client/endpoint/role.go b/client/endpoint/role.go new file mode 100644 index 00000000..cfbb900e --- /dev/null +++ b/client/endpoint/role.go @@ -0,0 +1,26 @@ +package endpoint + +import ( + "net/url" + "path" +) + +// Roles returns a Role API's endpoint url. +func (ep *Endpoints) Roles() string { + return ep.roles.String() +} + +// Role returns a Role API's endpoint url. +func (ep *Endpoints) Role(name string) (*url.URL, error) { + return urlJoin(ep.roles, name) +} + +// RoleMembers returns given role's member endpoint url. +func (ep *Endpoints) RoleMembers(name string) (*url.URL, error) { + return urlJoin(ep.roles, path.Join(name, "members")) +} + +// RoleMember returns given role member endpoint url. +func (ep *Endpoints) RoleMember(userName, roleName string) (*url.URL, error) { + return urlJoin(ep.roles, path.Join(roleName, "members", userName)) +} diff --git a/client/endpoint/role_test.go b/client/endpoint/role_test.go new file mode 100644 index 00000000..6544ea61 --- /dev/null +++ b/client/endpoint/role_test.go @@ -0,0 +1,64 @@ +package endpoint_test + +import ( + "fmt" + "testing" + + "github.com/suzuki-shunsuke/go-graylog/client/endpoint" +) + +func TestRoles(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s", apiURL, "roles") + if ep.Roles() != exp { + t.Fatalf(`ep.Roles() = "%s", wanted "%s"`, ep.Roles(), exp) + } +} + +func TestRole(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s", apiURL, "roles/foo") + act, err := ep.Role("foo") + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.Role("foo") = "%s", wanted "%s"`, act.String(), exp) + } +} + +func TestRoleMembers(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s", apiURL, "roles/foo/members") + act, err := ep.RoleMembers("foo") + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.RoleMembers("foo") = "%s", wanted "%s"`, act.String(), exp) + } +} + +func TestRoleMember(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s", apiURL, "roles/Admin/members/foo") + act, err := ep.RoleMember("foo", "Admin") + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.RoleMember("foo", "Admin") = "%s", wanted "%s"`, act.String(), exp) + } +} diff --git a/client/endpoint/stream.go b/client/endpoint/stream.go new file mode 100644 index 00000000..acece5e9 --- /dev/null +++ b/client/endpoint/stream.go @@ -0,0 +1,31 @@ +package endpoint + +import ( + "net/url" + "path" +) + +// Streams returns a Stream API's endpoint url. +func (ep *Endpoints) Streams() string { + return ep.streams.String() +} + +// Stream returns a Stream API's endpoint url. +func (ep *Endpoints) Stream(id string) (*url.URL, error) { + return urlJoin(ep.streams, id) +} + +// PauseStream returns PauseStream API's endpoint url. +func (ep *Endpoints) PauseStream(id string) (*url.URL, error) { + return urlJoin(ep.streams, path.Join(id, "pause")) +} + +// ResumeStream returns ResumeStream API's endpoint url. +func (ep *Endpoints) ResumeStream(id string) (*url.URL, error) { + return urlJoin(ep.streams, path.Join(id, "resume")) +} + +// EnabledStreams returns GetEnabledStreams API's endpoint url. +func (ep *Endpoints) EnabledStreams() string { + return ep.enabledStreams.String() +} diff --git a/client/endpoint/stream_rule.go b/client/endpoint/stream_rule.go new file mode 100644 index 00000000..f534bef1 --- /dev/null +++ b/client/endpoint/stream_rule.go @@ -0,0 +1,24 @@ +package endpoint + +import ( + "net/url" + "path" +) + +// StreamRules returns Stream Rules API's endpoint url. +func (ep *Endpoints) StreamRules(streamID string) (*url.URL, error) { + // /streams/{streamid}/rules + return urlJoin(ep.streams, path.Join(streamID, "rules")) +} + +// StreamRuleTypes returns Stream Rule Types API's endpoint url. +func (ep *Endpoints) StreamRuleTypes(streamID string) (*url.URL, error) { + // /streams/{streamid}/rules/types + return urlJoin(ep.streams, path.Join(streamID, "rules/types")) +} + +// StreamRule returns a Stream Rule API's endpoint url. +func (ep *Endpoints) StreamRule(streamID, streamRuleID string) (*url.URL, error) { + // /streams/{streamid}/rules/{streamRuleID} + return urlJoin(ep.streams, path.Join(streamID, "rules", streamRuleID)) +} diff --git a/client/endpoint/stream_rule_test.go b/client/endpoint/stream_rule_test.go new file mode 100644 index 00000000..b8a15171 --- /dev/null +++ b/client/endpoint/stream_rule_test.go @@ -0,0 +1,53 @@ +package endpoint_test + +import ( + "fmt" + "testing" + + "github.com/suzuki-shunsuke/go-graylog/client/endpoint" +) + +func TestStreamRules(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/streams/%s/rules", apiURL, ID) + act, err := ep.StreamRules(ID) + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.StreamRules("%s") = "%s", wanted "%s"`, ID, act.String(), exp) + } +} + +func TestStreamRuleTypes(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/streams/%s/rules/types", apiURL, ID) + act, err := ep.StreamRuleTypes(ID) + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.StreamRuleTypes("%s") = "%s", wanted "%s"`, ID, act.String(), exp) + } +} + +func TestStreamRule(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/streams/%s/rules/%s", apiURL, ID, ID) + act, err := ep.StreamRule(ID, ID) + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.StreamRule("%s", "%s") = "%s", wanted "%s"`, ID, ID, act.String(), exp) + } +} diff --git a/client/endpoint/stream_test.go b/client/endpoint/stream_test.go new file mode 100644 index 00000000..b7598b42 --- /dev/null +++ b/client/endpoint/stream_test.go @@ -0,0 +1,77 @@ +package endpoint_test + +import ( + "fmt" + "testing" + + "github.com/suzuki-shunsuke/go-graylog/client/endpoint" +) + +func TestStreams(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/streams", apiURL) + act := ep.Streams() + if act != exp { + t.Fatalf(`ep.Streams() = "%s", wanted "%s"`, act, exp) + } +} + +func TestStream(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/streams/%s", apiURL, ID) + act, err := ep.Stream(ID) + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.Stream("%s") = "%s", wanted "%s"`, ID, act.String(), exp) + } +} + +func TestPauseStream(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/streams/%s/pause", apiURL, ID) + act, err := ep.PauseStream(ID) + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.PauseStream("%s") = "%s", wanted "%s"`, ID, act.String(), exp) + } +} + +func TestResumeStream(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/streams/%s/resume", apiURL, ID) + act, err := ep.ResumeStream(ID) + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.ResumeStream("%s") = "%s", wanted "%s"`, ID, act.String(), exp) + } +} + +func TestEnabledStreams(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/streams/enabled", apiURL) + act := ep.EnabledStreams() + if act != exp { + t.Fatalf(`ep.EnabledStreams() = "%s", wanted "%s"`, act, exp) + } +} diff --git a/client/endpoint/user.go b/client/endpoint/user.go new file mode 100644 index 00000000..7b884423 --- /dev/null +++ b/client/endpoint/user.go @@ -0,0 +1,15 @@ +package endpoint + +import ( + "net/url" +) + +// User returns a User API's endpoint url. +func (ep *Endpoints) User(name string) (*url.URL, error) { + return urlJoin(ep.users, name) +} + +// Users returns a User API's endpoint url. +func (ep *Endpoints) Users() string { + return ep.users.String() +} diff --git a/client/endpoint/user_test.go b/client/endpoint/user_test.go new file mode 100644 index 00000000..8e3d96f4 --- /dev/null +++ b/client/endpoint/user_test.go @@ -0,0 +1,34 @@ +package endpoint_test + +import ( + "fmt" + "testing" + + "github.com/suzuki-shunsuke/go-graylog/client/endpoint" +) + +func TestUsers(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s", apiURL, "users") + if ep.Users() != exp { + t.Fatalf(`ep.Users() = "%s", wanted "%s"`, ep.Users(), exp) + } +} + +func TestUser(t *testing.T) { + ep, err := endpoint.NewEndpoints(apiURL) + if err != nil { + t.Fatal(err) + } + exp := fmt.Sprintf("%s/%s", apiURL, "users/foo") + act, err := ep.User("foo") + if err != nil { + t.Fatal(err) + } + if act.String() != exp { + t.Fatalf(`ep.User("foo") = "%s", wanted "%s"`, act.String(), exp) + } +} diff --git a/client/error.go b/client/error.go new file mode 100644 index 00000000..9ba7b2f2 --- /dev/null +++ b/client/error.go @@ -0,0 +1,14 @@ +package client + +import ( + "net/http" +) + +// ErrorInfo represents Graylog API's error information. +// Basically Client methods (ex. CreateRole) returns this, but note that Response is closed. +type ErrorInfo struct { + Type string `json:"type"` + Message string `json:"message"` + Request *http.Request `json:"request"` + Response *http.Response `json:"response"` +} diff --git a/client/example_test.go b/client/example_test.go new file mode 100644 index 00000000..fdd7c9f7 --- /dev/null +++ b/client/example_test.go @@ -0,0 +1,38 @@ +package client_test + +import ( + "fmt" + "log" + + "github.com/suzuki-shunsuke/go-graylog/client" + "github.com/suzuki-shunsuke/go-graylog/mockserver" +) + +func ExampleClient() { + // Create a mock server + server, err := mockserver.NewServer("", nil) + if err != nil { + log.Fatal(err) + } + // Start a server + server.Start() + defer server.Close() + + // Create a client + cl, err := client.NewClient(server.Endpoint(), "admin", "admin") + if err != nil { + log.Fatal(err) + } + + // get a role "Admin" + // ei.Response.Body is closed + role, ei, err := cl.GetRole("Admin") + if err != nil { + log.Fatal(err) + } + fmt.Println(ei.Response.StatusCode) + fmt.Println(role.Name) + // Output: + // 200 + // Admin +} diff --git a/client/index_set.go b/client/index_set.go new file mode 100644 index 00000000..c0d438fe --- /dev/null +++ b/client/index_set.go @@ -0,0 +1,141 @@ +package client + +import ( + "context" + "fmt" + "net/url" + "strconv" + + "github.com/pkg/errors" + "github.com/suzuki-shunsuke/go-graylog" +) + +// GetIndexSets returns a list of all index sets. +func (client *Client) GetIndexSets( + skip, limit int, stats bool, +) ([]graylog.IndexSet, map[string]graylog.IndexSetStats, int, *ErrorInfo, error) { + return client.GetIndexSetsContext(context.Background(), skip, limit, stats) +} + +// GetIndexSetsContext returns a list of all index sets with a context. +func (client *Client) GetIndexSetsContext( + ctx context.Context, skip, limit int, stats bool, +) ([]graylog.IndexSet, map[string]graylog.IndexSetStats, int, *ErrorInfo, error) { + indexSets := &graylog.IndexSetsBody{} + v := url.Values{ + "skip": []string{strconv.Itoa(skip)}, + "limit": []string{strconv.Itoa(limit)}, + "stats": []string{strconv.FormatBool(stats)}, + } + u := fmt.Sprintf("%s?%s", client.Endpoints().IndexSets(), v.Encode()) + ei, err := client.callGet( + ctx, u, nil, indexSets) + return indexSets.IndexSets, indexSets.Stats, indexSets.Total, ei, err +} + +// GetIndexSet returns a given index set. +func (client *Client) GetIndexSet(id string) (*graylog.IndexSet, *ErrorInfo, error) { + return client.GetIndexSetContext(context.Background(), id) +} + +// GetIndexSetContext returns a given index set with a context. +func (client *Client) GetIndexSetContext( + ctx context.Context, id string, +) (*graylog.IndexSet, *ErrorInfo, error) { + if id == "" { + return nil, nil, errors.New("id is empty") + } + is := &graylog.IndexSet{} + u, err := client.Endpoints().IndexSet(id) + if err != nil { + return nil, nil, err + } + ei, err := client.callGet( + ctx, u.String(), nil, is) + return is, ei, err +} + +// CreateIndexSet creates a Index Set. +func (client *Client) CreateIndexSet(indexSet *graylog.IndexSet) (*ErrorInfo, error) { + return client.CreateIndexSetContext(context.Background(), indexSet) +} + +// CreateIndexSetContext creates a Index Set with a context. +func (client *Client) CreateIndexSetContext( + ctx context.Context, is *graylog.IndexSet, +) (*ErrorInfo, error) { + if is == nil { + return nil, fmt.Errorf("index set is nil") + } + is.SetCreateDefaultValues() + + return client.callPost(ctx, client.Endpoints().IndexSets(), is, is) +} + +// UpdateIndexSet updates a given Index Set. +func (client *Client) UpdateIndexSet(is *graylog.IndexSetUpdateParams) (*graylog.IndexSet, *ErrorInfo, error) { + return client.UpdateIndexSetContext(context.Background(), is) +} + +// UpdateIndexSetContext updates a given Index Set with a context. +func (client *Client) UpdateIndexSetContext( + ctx context.Context, prms *graylog.IndexSetUpdateParams, +) (*graylog.IndexSet, *ErrorInfo, error) { + if prms == nil { + return nil, nil, fmt.Errorf("index set is nil") + } + if prms.ID == "" { + return nil, nil, errors.New("id is empty") + } + u, err := client.Endpoints().IndexSet(prms.ID) + if err != nil { + return nil, nil, err + } + a := *prms + a.ID = "" + is := &graylog.IndexSet{} + ei, err := client.callPut(ctx, u.String(), &a, is) + return is, ei, err +} + +// DeleteIndexSet deletes a given Index Set. +func (client *Client) DeleteIndexSet(id string) (*ErrorInfo, error) { + return client.DeleteIndexSetContext(context.Background(), id) +} + +// DeleteIndexSetContext deletes a given Index Set with a context. +func (client *Client) DeleteIndexSetContext( + ctx context.Context, id string, +) (*ErrorInfo, error) { + if id == "" { + return nil, errors.New("id is empty") + } + u, err := client.Endpoints().IndexSet(id) + if err != nil { + return nil, err + } + return client.callDelete(ctx, u.String(), nil, nil) +} + +// SetDefaultIndexSet sets default Index Set. +func (client *Client) SetDefaultIndexSet(id string) ( + *graylog.IndexSet, *ErrorInfo, error, +) { + return client.SetDefaultIndexSetContext(context.Background(), id) +} + +// SetDefaultIndexSetContext sets default Index Set with a context. +func (client *Client) SetDefaultIndexSetContext( + ctx context.Context, id string, +) (*graylog.IndexSet, *ErrorInfo, error) { + if id == "" { + return nil, nil, errors.New("id is empty") + } + u, err := client.Endpoints().SetDefaultIndexSet(id) + if err != nil { + return nil, nil, err + } + is := &graylog.IndexSet{} + ei, err := client.callPut(ctx, u.String(), nil, is) + return is, ei, err +} diff --git a/client/index_set_stats.go b/client/index_set_stats.go new file mode 100644 index 00000000..3b79a14f --- /dev/null +++ b/client/index_set_stats.go @@ -0,0 +1,49 @@ +package client + +import ( + "context" + + "github.com/pkg/errors" + "github.com/suzuki-shunsuke/go-graylog" +) + +// GetIndexSetStats returns a given Index Set statistics. +func (client *Client) GetIndexSetStats(id string) ( + *graylog.IndexSetStats, *ErrorInfo, error, +) { + return client.GetIndexSetStatsContext(context.Background(), id) +} + +// GetIndexSetStatsContext returns a given Index Set statistics with a context. +func (client *Client) GetIndexSetStatsContext( + ctx context.Context, id string, +) (*graylog.IndexSetStats, *ErrorInfo, error) { + if id == "" { + return nil, nil, errors.New("id is empty") + } + indexSetStats := &graylog.IndexSetStats{} + u, err := client.Endpoints().IndexSetStats(id) + if err != nil { + return nil, nil, err + } + ei, err := client.callGet( + ctx, u.String(), nil, indexSetStats) + return indexSetStats, ei, err +} + +// GetTotalIndexSetsStats returns stats of all Index Sets. +func (client *Client) GetTotalIndexSetsStats() ( + *graylog.IndexSetStats, *ErrorInfo, error, +) { + return client.GetTotalIndexSetsStatsContext(context.Background()) +} + +// GetTotalIndexSetsStatsContext returns stats of all Index Sets with a context. +func (client *Client) GetTotalIndexSetsStatsContext( + ctx context.Context, +) (*graylog.IndexSetStats, *ErrorInfo, error) { + indexSetStats := &graylog.IndexSetStats{} + ei, err := client.callGet( + ctx, client.Endpoints().IndexSetsStats(), nil, indexSetStats) + return indexSetStats, ei, err +} diff --git a/client/index_set_stats_test.go b/client/index_set_stats_test.go new file mode 100644 index 00000000..7727579b --- /dev/null +++ b/client/index_set_stats_test.go @@ -0,0 +1,78 @@ +package client_test + +import ( + "testing" + + "github.com/satori/go.uuid" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestGetIndexSetStats(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + iss, _, _, _, err := client.GetIndexSets(0, 0, false) + if err != nil { + t.Fatal(err) + } + u, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet(u.String()) + if len(iss) == 0 { + if _, err := client.CreateIndexSet(is); err != nil { + t.Fatal(err) + } + testutil.WaitAfterCreateIndexSet(server) + // clean + defer func(id string) { + if _, err := client.DeleteIndexSet(id); err != nil { + t.Fatal(err) + } + testutil.WaitAfterDeleteIndexSet(server) + }(is.ID) + } else { + is = &(iss[0]) + } + + if _, _, err := client.GetIndexSetStats(is.ID); err != nil { + t.Fatal(err) + } + if _, _, err := client.GetIndexSetStats(""); err == nil { + t.Fatal("index set id is required") + } + // if _, _, err := client.GetIndexSetStats("h"); err == nil { + // t.Fatal(`no index set whose id is "h"`) + // } +} + +func TestGetTotalIndexSetsStats(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + u, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + is, f, err := testutil.GetIndexSet(client, server, u.String()) + if err != nil { + t.Fatal(err) + } + if f != nil { + defer f(is.ID) + } + if _, _, err := client.GetTotalIndexSetsStats(); err != nil { + t.Fatal(err) + } +} diff --git a/client/index_set_test.go b/client/index_set_test.go new file mode 100644 index 00000000..043c60f5 --- /dev/null +++ b/client/index_set_test.go @@ -0,0 +1,224 @@ +package client_test + +import ( + "testing" + + "github.com/satori/go.uuid" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/testutil" + "github.com/suzuki-shunsuke/go-ptr" +) + +func TestGetIndexSets(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + if _, _, _, _, err := client.GetIndexSets(0, 0, false); err != nil { + t.Fatal(err) + } +} + +func TestGetIndexSet(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + u, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + is, f, err := testutil.GetIndexSet(client, server, u.String()) + if err != nil { + t.Fatal(err) + } + if f != nil { + defer f(is.ID) + } + + // success + r, _, err := client.GetIndexSet(is.ID) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("indexSet is nil") + } + if r.ID != is.ID { + t.Fatalf(`indexSet.ID = "%s", wanted "%s"`, r.ID, is.ID) + } + // id is required + if _, _, err := client.GetIndexSet(""); err == nil { + t.Fatal("id is required") + } + // invalid id + if _, _, err := client.GetIndexSet("h"); err == nil { + t.Fatal("index set should not be found") + } +} + +func TestCreateIndexSet(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + // nil check + if _, err := client.CreateIndexSet(nil); err == nil { + t.Fatal("index set is nil") + } + u, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet(u.String()) + // success + if _, err := client.CreateIndexSet(is); err != nil { + t.Fatal(err) + } + testutil.WaitAfterCreateIndexSet(server) + // clean + defer func() { + if _, err := client.DeleteIndexSet(is.ID); err != nil { + t.Fatal(err) + } + testutil.WaitAfterDeleteIndexSet(server) + }() +} + +func TestUpdateIndexSet(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + u, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + is, f, err := testutil.GetIndexSet(client, server, u.String()) + if err != nil { + t.Fatal(err) + } + if f != nil { + defer f(is.ID) + } + // success + if _, _, err := client.UpdateIndexSet(is.NewUpdateParams()); err != nil { + t.Fatal(err) + } + // id required + prms := is.NewUpdateParams() + prms.ID = "" + if _, _, err := client.UpdateIndexSet(prms); err == nil { + t.Fatal("index set id is required") + } + // nil check + if _, _, err := client.UpdateIndexSet(nil); err == nil { + t.Fatal("index set is required") + } + // invalid id + prms.ID = "h" + if _, _, err := client.UpdateIndexSet(prms); err == nil { + t.Fatal("index set should no be found") + } +} + +func TestDeleteIndexSet(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + // id required + if _, err := client.DeleteIndexSet(""); err == nil { + t.Fatal("id is required") + } + // invalid id + if _, err := client.DeleteIndexSet("h"); err == nil { + t.Fatal(`no index set with id "h" is found`) + } +} + +func TestSetDefaultIndexSet(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + iss, _, _, _, err := client.GetIndexSets(0, 0, false) + if err != nil { + t.Fatal(err) + } + var defIs, is *graylog.IndexSet + for _, i := range iss { + if i.Default { + defIs = &i + } else { + is = &i + } + } + if is == nil { + u, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + is = testutil.IndexSet(u.String()) + is.Default = false + is.Writable = true + if _, err := client.CreateIndexSet(is); err != nil { + t.Fatal(err) + } + testutil.WaitAfterCreateIndexSet(server) + defer func(id string) { + if _, err := client.DeleteIndexSet(id); err != nil { + t.Fatal(err) + } + testutil.WaitAfterDeleteIndexSet(server) + }(is.ID) + } + is, _, err = client.SetDefaultIndexSet(is.ID) + if err != nil { + t.Fatal("Failed to UpdateIndexSet", err) + } + defer func(id string) { + if _, _, err = client.SetDefaultIndexSet(id); err != nil { + t.Fatal(err) + } + }(defIs.ID) + if !is.Default { + t.Fatal("updatedIndexSet.Default == false") + } + if _, _, err := client.SetDefaultIndexSet(""); err == nil { + t.Fatal("index set id is required") + } + if _, _, err := client.SetDefaultIndexSet("h"); err == nil { + t.Fatal(`no index set whose id is "h"`) + } + + prms := is.NewUpdateParams() + prms.Writable = ptr.PBool(false) + + if _, _, err := client.UpdateIndexSet(prms); err == nil { + t.Fatal("Default index set must be writable.") + } +} diff --git a/client/input.go b/client/input.go new file mode 100644 index 00000000..300ca059 --- /dev/null +++ b/client/input.go @@ -0,0 +1,132 @@ +package client + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/suzuki-shunsuke/go-graylog" +) + +// GetInputs returns all inputs. +func (client *Client) GetInputs() ([]graylog.Input, int, *ErrorInfo, error) { + return client.GetInputsContext(context.Background()) +} + +// GetInputsContext returns all inputs with a context. +func (client *Client) GetInputsContext(ctx context.Context) ( + []graylog.Input, int, *ErrorInfo, error, +) { + inputs := &graylog.InputsBody{} + ei, err := client.callGet( + ctx, client.Endpoints().Inputs(), nil, inputs) + return inputs.Inputs, inputs.Total, ei, err +} + +// GetInput returns a given input. +func (client *Client) GetInput(id string) (*graylog.Input, *ErrorInfo, error) { + return client.GetInputContext(context.Background(), id) +} + +// GetInputContext returns a given input with a context. +func (client *Client) GetInputContext( + ctx context.Context, id string, +) (*graylog.Input, *ErrorInfo, error) { + if id == "" { + return nil, nil, errors.New("id is empty") + } + u, err := client.Endpoints().Input(id) + if err != nil { + return nil, nil, err + } + input := &graylog.Input{} + ei, err := client.callGet( + ctx, u.String(), nil, input) + return input, ei, err +} + +// CreateInput creates an input. +func (client *Client) CreateInput(input *graylog.Input) ( + ei *ErrorInfo, err error, +) { + return client.CreateInputContext(context.Background(), input) +} + +// CreateInputContext creates an input with a context. +func (client *Client) CreateInputContext( + ctx context.Context, input *graylog.Input, +) (ei *ErrorInfo, err error) { + if input == nil { + return nil, fmt.Errorf("input is nil") + } + if input.ID != "" { + return nil, fmt.Errorf("input id should be empty") + } + // change attributes to configuration + // https://github.com/Graylog2/graylog2-server/issues/3480 + d := map[string]interface{}{ + "title": input.Title, + "type": input.Type(), + "configuration": input.Attrs, + "global": input.Global, + } + if input.Node != "" { + d["node"] = input.Node + } + + return client.callPost(ctx, client.Endpoints().Inputs(), &d, input) +} + +// UpdateInput updates an given input. +func (client *Client) UpdateInput(input *graylog.InputUpdateParams) (*graylog.Input, *ErrorInfo, error) { + return client.UpdateInputContext(context.Background(), input) +} + +// UpdateInputContext updates an given input with a context. +func (client *Client) UpdateInputContext( + ctx context.Context, prms *graylog.InputUpdateParams, +) (*graylog.Input, *ErrorInfo, error) { + if prms == nil { + return nil, nil, fmt.Errorf("input is nil") + } + if prms.ID == "" { + return nil, nil, errors.New("id is empty") + } + u, err := client.Endpoints().Input(prms.ID) + if err != nil { + return nil, nil, err + } + // change attributes to configuration + // https://github.com/Graylog2/graylog2-server/issues/3480 + d := map[string]interface{}{ + "title": prms.Title, + "type": prms.Type, + "configuration": prms.Attrs, + "global": prms.Global, + } + if prms.Node != "" { + d["node"] = prms.Node + } + input := &graylog.Input{} + ei, err := client.callPut(ctx, u.String(), &d, input) + return input, ei, err +} + +// DeleteInput deletes an given input. +func (client *Client) DeleteInput(id string) (*ErrorInfo, error) { + return client.DeleteInputContext(context.Background(), id) +} + +// DeleteInputContext deletes an given input with a context. +func (client *Client) DeleteInputContext( + ctx context.Context, id string, +) (*ErrorInfo, error) { + if id == "" { + return nil, errors.New("id is empty") + } + u, err := client.Endpoints().Input(id) + if err != nil { + return nil, err + } + return client.callDelete(ctx, u.String(), nil, nil) +} diff --git a/client/input_test.go b/client/input_test.go new file mode 100644 index 00000000..a19ae9e9 --- /dev/null +++ b/client/input_test.go @@ -0,0 +1,140 @@ +package client_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestGetInputs(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + inputs, _, _, err := client.GetInputs() + if err != nil { + t.Fatal(err) + } + if inputs == nil { + t.Fatal("inputs is nil") + } + if len(inputs) == 0 { + t.Fatal("inputs is empty") + } +} + +func TestGetInput(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + input := testutil.Input() + if _, err := client.CreateInput(input); err != nil { + t.Fatal(err) + } + defer client.DeleteInput(input.ID) + r, _, err := client.GetInput(input.ID) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("input is nil") + } + if r.ID != input.ID { + t.Fatalf(`input.ID = "%s", wanted "%s"`, r.ID, input.ID) + } + if _, _, err := client.GetInput(""); err == nil { + t.Fatal("input id is required") + } + if _, _, err := client.GetInput("h"); err == nil { + t.Fatal("input should not be found") + } +} + +func TestCreateInput(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + // nil check + if _, err := client.CreateInput(nil); err == nil { + t.Fatal("input is nil") + } + input := testutil.Input() + if _, err := client.CreateInput(input); err != nil { + t.Fatal(err) + } + defer client.DeleteInput(input.ID) + attrs := input.Attrs.(*graylog.InputBeatsAttrs) + if attrs.BindAddress == "" { + t.Fatal(`attrs.BindAddress == ""`) + } + // error check + if _, err := client.CreateInput(input); err == nil { + t.Fatal("input id should be empty") + } +} + +func TestUpdateInput(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + input := testutil.Input() + if _, err := client.CreateInput(input); err != nil { + t.Fatal(err) + } + defer client.DeleteInput(input.ID) + attrs := input.Attrs.(*graylog.InputBeatsAttrs) + if attrs.BindAddress == "" { + t.Fatal(`attrs.BindAddress == ""`) + } + if _, _, err := client.UpdateInput(input.NewUpdateParams()); err != nil { + t.Fatal(err) + } + input.ID = "" + if _, _, err := client.UpdateInput(input.NewUpdateParams()); err == nil { + t.Fatal("input id is required") + } + if _, _, err := client.UpdateInput(nil); err == nil { + t.Fatal("input is required") + } + input.ID = "h" + if _, _, err := client.UpdateInput(input.NewUpdateParams()); err == nil { + t.Fatal("input should no be found") + } +} + +func TestDeleteInput(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + if _, err := client.DeleteInput(""); err == nil { + t.Fatal("input id is required") + } + if _, err := client.DeleteInput("h"); err == nil { + t.Fatal(`no input with id "h" is found`) + } +} diff --git a/client/role.go b/client/role.go new file mode 100644 index 00000000..45eb4e2a --- /dev/null +++ b/client/role.go @@ -0,0 +1,106 @@ +package client + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/suzuki-shunsuke/go-graylog" +) + +// CreateRole creates a new role. +func (client *Client) CreateRole(role *graylog.Role) (*ErrorInfo, error) { + return client.CreateRoleContext(context.Background(), role) +} + +// CreateRoleContext creates a new role with a context. +func (client *Client) CreateRoleContext( + ctx context.Context, role *graylog.Role, +) (*ErrorInfo, error) { + if role == nil { + return nil, fmt.Errorf("role is nil") + } + return client.callPost(ctx, client.Endpoints().Roles(), role, role) +} + +// GetRoles returns all roles. +func (client *Client) GetRoles() ([]graylog.Role, int, *ErrorInfo, error) { + return client.GetRolesContext(context.Background()) +} + +// GetRolesContext returns all roles with a context. +func (client *Client) GetRolesContext(ctx context.Context) ( + []graylog.Role, int, *ErrorInfo, error, +) { + roles := &graylog.RolesBody{} + ei, err := client.callGet( + ctx, client.Endpoints().Roles(), nil, roles) + return roles.Roles, roles.Total, ei, err +} + +// GetRole returns a given role. +func (client *Client) GetRole(name string) (*graylog.Role, *ErrorInfo, error) { + return client.GetRoleContext(context.Background(), name) +} + +// GetRoleContext returns a given role with a context. +func (client *Client) GetRoleContext( + ctx context.Context, name string, +) (*graylog.Role, *ErrorInfo, error) { + if name == "" { + return nil, nil, errors.New("name is empty") + } + u, err := client.Endpoints().Role(name) + if err != nil { + return nil, nil, err + } + role := &graylog.Role{} + ei, err := client.callGet( + ctx, u.String(), nil, role) + return role, ei, err +} + +// UpdateRole updates a given role. +func (client *Client) UpdateRole(name string, role *graylog.RoleUpdateParams) ( + *graylog.Role, *ErrorInfo, error, +) { + return client.UpdateRoleContext(context.Background(), name, role) +} + +// UpdateRoleContext updates a given role with a context. +func (client *Client) UpdateRoleContext( + ctx context.Context, name string, prms *graylog.RoleUpdateParams, +) (*graylog.Role, *ErrorInfo, error) { + if name == "" { + return nil, nil, errors.New("name is empty") + } + if prms == nil { + return nil, nil, fmt.Errorf("role is nil") + } + u, err := client.Endpoints().Role(name) + if err != nil { + return nil, nil, err + } + role := &graylog.Role{} + ei, err := client.callPut(ctx, u.String(), prms, role) + return role, ei, err +} + +// DeleteRole deletes a given role. +func (client *Client) DeleteRole(name string) (*ErrorInfo, error) { + return client.DeleteRoleContext(context.Background(), name) +} + +// DeleteRoleContext deletes a given role with a context. +func (client *Client) DeleteRoleContext( + ctx context.Context, name string, +) (*ErrorInfo, error) { + if name == "" { + return nil, errors.New("name is empty") + } + u, err := client.Endpoints().Role(name) + if err != nil { + return nil, err + } + return client.callDelete(ctx, u.String(), nil, nil) +} diff --git a/client/role_member.go b/client/role_member.go new file mode 100644 index 00000000..c14f8cd3 --- /dev/null +++ b/client/role_member.go @@ -0,0 +1,79 @@ +package client + +import ( + "context" + + "github.com/pkg/errors" + "github.com/suzuki-shunsuke/go-graylog" +) + +// GetRoleMembers returns a given role's members. +func (client *Client) GetRoleMembers(name string) ([]graylog.User, *ErrorInfo, error) { + return client.GetRoleMembersContext(context.Background(), name) +} + +// GetRoleMembersContext returns a given role's members with a context. +func (client *Client) GetRoleMembersContext( + ctx context.Context, name string, +) ([]graylog.User, *ErrorInfo, error) { + if name == "" { + return nil, nil, errors.New("name is empty") + } + u, err := client.Endpoints().RoleMembers(name) + if err != nil { + return nil, nil, err + } + users := &graylog.UsersBody{} + ei, err := client.callGet( + ctx, u.String(), nil, users) + return users.Users, ei, err +} + +// AddUserToRole adds a user to a role. +func (client *Client) AddUserToRole(userName, roleName string) ( + *ErrorInfo, error, +) { + return client.AddUserToRoleContext(context.Background(), userName, roleName) +} + +// AddUserToRoleContext adds a user to a role with a context. +func (client *Client) AddUserToRoleContext( + ctx context.Context, userName, roleName string, +) (*ErrorInfo, error) { + if userName == "" { + return nil, errors.New("userName is empty") + } + if roleName == "" { + return nil, errors.New("roleName is empty") + } + u, err := client.Endpoints().RoleMember(userName, roleName) + if err != nil { + return nil, err + } + return client.callPut(ctx, u.String(), nil, nil) +} + +// RemoveUserFromRole removes a user from a role. +func (client *Client) RemoveUserFromRole( + userName, roleName string, +) (*ErrorInfo, error) { + return client.RemoveUserFromRoleContext( + context.Background(), userName, roleName) +} + +// RemoveUserFromRoleContext removes a user from a role with a context. +func (client *Client) RemoveUserFromRoleContext( + ctx context.Context, userName, roleName string, +) (*ErrorInfo, error) { + if userName == "" { + return nil, errors.New("userName is empty") + } + if roleName == "" { + return nil, errors.New("roleName is empty") + } + u, err := client.Endpoints().RoleMember(userName, roleName) + if err != nil { + return nil, err + } + return client.callDelete(ctx, u.String(), nil, nil) +} diff --git a/client/role_member_test.go b/client/role_member_test.go new file mode 100644 index 00000000..85918618 --- /dev/null +++ b/client/role_member_test.go @@ -0,0 +1,36 @@ +package client_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/test" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestGetRoleMembers(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + if _, _, err := client.GetRoleMembers("Admin"); err != nil { + t.Fatal("Failed to GetRoleMembers", err) + } + if _, _, err := client.GetRoleMembers(""); err == nil { + t.Fatal("name is required") + } + if _, _, err := client.GetRoleMembers("h"); err == nil { + t.Fatal(`no role whose name is "h"`) + } +} + +func TestAddUserToRole(t *testing.T) { + test.TestAddUserToRole(t) +} + +func TestRemoveUserFromRole(t *testing.T) { + test.TestRemoveUserFromRole(t) +} diff --git a/client/role_test.go b/client/role_test.go new file mode 100644 index 00000000..aabd624f --- /dev/null +++ b/client/role_test.go @@ -0,0 +1,137 @@ +package client_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestCreateRole(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + role := testutil.Role() + client.DeleteRole(role.Name) + // nil check + if _, err := client.CreateRole(nil); err == nil { + t.Fatal("role is nil") + } + if _, err := client.CreateRole(role); err != nil { + t.Fatal(err) + } + if _, err := client.DeleteRole(role.Name); err != nil { + t.Fatal(err) + } + // error check + role.Name = "" + if _, err := client.CreateRole(role); err == nil { + t.Fatal("role name is empty") + } +} + +func TestGetRoles(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + roles, _, _, err := client.GetRoles() + if err != nil { + t.Fatal(err) + } + if roles == nil { + t.Fatal("roles is nil") + } + if len(roles) == 0 { + t.Fatal("roles is empty") + } +} + +func TestGetRole(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + role := testutil.Role() + client.DeleteRole(role.Name) + if _, _, err := client.GetRole(role.Name); err == nil { + t.Fatal("role should be deleted") + } + if _, err := client.CreateRole(role); err != nil { + t.Fatal(err) + } + defer client.DeleteRole(role.Name) + r, _, err := client.GetRole(role.Name) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("roles is nil") + } + if r.Name != role.Name { + t.Fatalf(`role.Name = "%s", wanted "%s"`, r.Name, role.Name) + } + if _, _, err := client.GetRole(""); err == nil { + t.Fatal("role name is required") + } +} + +func TestUpdateRole(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + role := testutil.Role() + client.DeleteRole(role.Name) + if _, _, err := client.UpdateRole(role.Name, role.NewUpdateParams()); err == nil { + t.Fatal("role should be deleted") + } + if _, err := client.CreateRole(role); err != nil { + t.Fatal(err) + } + defer client.DeleteRole(role.Name) + if _, _, err := client.UpdateRole(role.Name, role.NewUpdateParams()); err != nil { + t.Fatal(err) + } + if _, _, err := client.UpdateRole("", role.NewUpdateParams()); err == nil { + t.Fatal("role name is required") + } + name := role.Name + role.Name = "" + if _, _, err := client.UpdateRole(name, role.NewUpdateParams()); err == nil { + t.Fatal("role name is required") + } +} + +func TestDeleteRole(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + if _, err := client.DeleteRole(""); err == nil { + t.Fatal("role name is required") + } + if _, err := client.DeleteRole("h"); err == nil { + t.Fatal(`no role with name "h" is found`) + } +} diff --git a/client/stream.go b/client/stream.go new file mode 100644 index 00000000..9ffadf44 --- /dev/null +++ b/client/stream.go @@ -0,0 +1,169 @@ +package client + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/suzuki-shunsuke/go-graylog" +) + +// GetStreams returns all streams. +func (client *Client) GetStreams() ( + streams []graylog.Stream, total int, ei *ErrorInfo, err error, +) { + return client.GetStreamsContext(context.Background()) +} + +// GetStreamsContext returns all streams with a context. +func (client *Client) GetStreamsContext( + ctx context.Context, +) (streams []graylog.Stream, total int, ei *ErrorInfo, err error) { + streamsBody := &graylog.StreamsBody{} + ei, err = client.callGet( + ctx, client.Endpoints().Streams(), nil, streamsBody) + return streamsBody.Streams, streamsBody.Total, ei, err +} + +// GetStream returns a given stream. +func (client *Client) GetStream(id string) (*graylog.Stream, *ErrorInfo, error) { + return client.GetStreamContext(context.Background(), id) +} + +// GetStreamContext returns a given stream with a context. +func (client *Client) GetStreamContext( + ctx context.Context, id string, +) (*graylog.Stream, *ErrorInfo, error) { + if id == "" { + return nil, nil, errors.New("id is empty") + } + u, err := client.Endpoints().Stream(id) + if err != nil { + return nil, nil, err + } + stream := &graylog.Stream{} + ei, err := client.callGet(ctx, u.String(), nil, stream) + return stream, ei, err +} + +// CreateStream creates a stream. +func (client *Client) CreateStream(stream *graylog.Stream) (*ErrorInfo, error) { + return client.CreateStreamContext(context.Background(), stream) +} + +// CreateStreamContext creates a stream with a context. +func (client *Client) CreateStreamContext( + ctx context.Context, stream *graylog.Stream, +) (*ErrorInfo, error) { + if stream == nil { + return nil, fmt.Errorf("stream is nil") + } + ret := map[string]string{} + ei, err := client.callPost(ctx, client.Endpoints().Streams(), stream, &ret) + if err != nil { + return ei, err + } + if id, ok := ret["stream_id"]; ok { + stream.ID = id + return ei, nil + } + return ei, errors.New(`response doesn't have the field "stream_id"`) +} + +// GetEnabledStreams returns all enabled streams. +func (client *Client) GetEnabledStreams() ( + streams []graylog.Stream, total int, ei *ErrorInfo, err error, +) { + return client.GetEnabledStreamsContext(context.Background()) +} + +// GetEnabledStreamsContext returns all enabled streams with a context. +func (client *Client) GetEnabledStreamsContext( + ctx context.Context, +) (streams []graylog.Stream, total int, ei *ErrorInfo, err error) { + streamsBody := &graylog.StreamsBody{} + ei, err = client.callGet( + ctx, client.Endpoints().EnabledStreams(), nil, streamsBody) + return streamsBody.Streams, streamsBody.Total, ei, err +} + +// UpdateStream updates a stream. +func (client *Client) UpdateStream(stream *graylog.Stream) (*ErrorInfo, error) { + return client.UpdateStreamContext(context.Background(), stream) +} + +// UpdateStreamContext updates a stream with a context. +func (client *Client) UpdateStreamContext( + ctx context.Context, stream *graylog.Stream, +) (*ErrorInfo, error) { + if stream == nil { + return nil, fmt.Errorf("stream is nil") + } + if stream.ID == "" { + return nil, errors.New("id is empty") + } + u, err := client.Endpoints().Stream(stream.ID) + if err != nil { + return nil, err + } + body := *stream + body.ID = "" + return client.callPut(ctx, u.String(), &body, stream) +} + +// DeleteStream deletes a stream. +func (client *Client) DeleteStream(id string) (*ErrorInfo, error) { + return client.DeleteStreamContext(context.Background(), id) +} + +// DeleteStreamContext deletes a stream with a context. +func (client *Client) DeleteStreamContext( + ctx context.Context, id string, +) (*ErrorInfo, error) { + if id == "" { + return nil, errors.New("id is empty") + } + u, err := client.Endpoints().Stream(id) + if err != nil { + return nil, err + } + return client.callDelete(ctx, u.String(), nil, nil) +} + +// PauseStream pauses a stream. +func (client *Client) PauseStream(id string) (*ErrorInfo, error) { + return client.PauseStreamContext(context.Background(), id) +} + +// PauseStreamContext pauses a stream with a context. +func (client *Client) PauseStreamContext( + ctx context.Context, id string, +) (*ErrorInfo, error) { + if id == "" { + return nil, errors.New("id is empty") + } + u, err := client.Endpoints().PauseStream(id) + if err != nil { + return nil, err + } + return client.callPost(ctx, u.String(), nil, nil) +} + +// ResumeStream resumes a stream. +func (client *Client) ResumeStream(id string) (*ErrorInfo, error) { + return client.ResumeStreamContext(context.Background(), id) +} + +// ResumeStreamContext resumes a stream with a context. +func (client *Client) ResumeStreamContext( + ctx context.Context, id string, +) (*ErrorInfo, error) { + if id == "" { + return nil, errors.New("id is empty") + } + u, err := client.Endpoints().ResumeStream(id) + if err != nil { + return nil, err + } + return client.callPost(ctx, u.String(), nil, nil) +} diff --git a/client/stream_rule.go b/client/stream_rule.go new file mode 100644 index 00000000..9313e851 --- /dev/null +++ b/client/stream_rule.go @@ -0,0 +1,139 @@ +package client + +import ( + "context" + + "github.com/pkg/errors" + "github.com/suzuki-shunsuke/go-graylog" +) + +// GetStreamRuleTypes returns all available stream types +// GET /streams/{streamid}/rules/types Get all available stream types + +type streamRuleIDBody struct { + StreamRuleID string `json:"streamrule_id"` +} + +// GetStreamRules returns a list of all stream rules +func (client *Client) GetStreamRules(streamID string) ( + streamRules []graylog.StreamRule, total int, ei *ErrorInfo, err error, +) { + return client.GetStreamRulesContext(context.Background(), streamID) +} + +// GetStreamRulesContext returns a list of all stream rules with a context. +func (client *Client) GetStreamRulesContext( + ctx context.Context, streamID string, +) (streamRules []graylog.StreamRule, total int, ei *ErrorInfo, err error) { + // GET /streams/{streamid}/rules Get a list of all stream rules + u, err := client.Endpoints().StreamRules(streamID) + if err != nil { + return nil, 0, nil, err + } + body := &graylog.StreamRulesBody{} + ei, err = client.callGet(ctx, u.String(), nil, body) + return body.StreamRules, body.Total, ei, err +} + +// CreateStreamRule creates a stream +func (client *Client) CreateStreamRule(rule *graylog.StreamRule) (*ErrorInfo, error) { + return client.CreateStreamRuleContext(context.Background(), rule) +} + +// CreateStreamRuleContext creates a stream with a context +func (client *Client) CreateStreamRuleContext( + ctx context.Context, rule *graylog.StreamRule, +) (*ErrorInfo, error) { + // POST /streams/{streamid}/rules Create a stream rule + if rule == nil { + return nil, errors.New("rule is required") + } + u, err := client.Endpoints().StreamRules(rule.StreamID) + if err != nil { + return nil, err + } + + cr := *rule + cr.StreamID = "" + body := &streamRuleIDBody{} + ei, err := client.callPost(ctx, u.String(), &cr, body) + rule.ID = body.StreamRuleID + return ei, err +} + +// UpdateStreamRule updates a stream rule +func (client *Client) UpdateStreamRule(rule *graylog.StreamRule) (*ErrorInfo, error) { + return client.UpdateStreamRuleContext(context.Background(), rule) +} + +// UpdateStreamRuleContext updates a stream rule +func (client *Client) UpdateStreamRuleContext( + ctx context.Context, rule *graylog.StreamRule, +) (*ErrorInfo, error) { + // PUT /streams/{streamid}/rules/{streamRuleID} Update a stream rule + if rule == nil { + return nil, errors.New("rule is required") + } + if rule.StreamID == "" { + return nil, errors.New("streamID is empty") + } + if rule.ID == "" { + return nil, errors.New("streamRuleID is empty") + } + u, err := client.Endpoints().StreamRule(rule.StreamID, rule.ID) + if err != nil { + return nil, err + } + cr := *rule + cr.StreamID = "" + cr.ID = "" + return client.callPut(ctx, u.String(), &cr, nil) +} + +// DeleteStreamRule deletes a stream rule +func (client *Client) DeleteStreamRule(streamID, ruleID string) (*ErrorInfo, error) { + return client.DeleteStreamRuleContext(context.Background(), streamID, ruleID) +} + +// DeleteStreamRuleContext deletes a stream rule with a context +func (client *Client) DeleteStreamRuleContext( + ctx context.Context, streamID, ruleID string, +) (*ErrorInfo, error) { + // DELETE /streams/{streamid}/rules/{streamRuleID} Delete a stream rule + if streamID == "" { + return nil, errors.New("stream id is required") + } + if ruleID == "" { + return nil, errors.New("stream rule id is required") + } + u, err := client.Endpoints().StreamRule(streamID, ruleID) + if err != nil { + return nil, err + } + return client.callDelete(ctx, u.String(), nil, nil) +} + +// GetStreamRule returns a stream rule +func (client *Client) GetStreamRule(streamID, ruleID string) (*graylog.StreamRule, *ErrorInfo, error) { + return client.GetStreamRuleContext(context.Background(), streamID, ruleID) +} + +// GetStreamRuleContext returns a stream rule with a context +func (client *Client) GetStreamRuleContext( + ctx context.Context, streamID, ruleID string, +) (*graylog.StreamRule, *ErrorInfo, error) { + // GET /streams/{streamid}/rules/{streamRuleID} Get a single stream rules + if streamID == "" { + return nil, nil, errors.New("stream id is required") + } + if ruleID == "" { + return nil, nil, errors.New("stream rule id is required") + } + u, err := client.Endpoints().StreamRule(streamID, ruleID) + if err != nil { + return nil, nil, err + } + rule := &graylog.StreamRule{} + ei, err := client.callGet(ctx, u.String(), nil, rule) + return rule, ei, err +} diff --git a/client/stream_rule_test.go b/client/stream_rule_test.go new file mode 100644 index 00000000..643664ed --- /dev/null +++ b/client/stream_rule_test.go @@ -0,0 +1,23 @@ +package client_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/test" +) + +func TestGetStreamRules(t *testing.T) { + test.TestGetStreamRules(t) +} + +func TestCreateStreamRule(t *testing.T) { + test.TestCreateStreamRule(t) +} + +func TestUpdateStreamRule(t *testing.T) { + test.TestUpdateStreamRule(t) +} + +func TestDeleteStreamRule(t *testing.T) { + test.TestDeleteStreamRule(t) +} diff --git a/client/stream_test.go b/client/stream_test.go new file mode 100644 index 00000000..98a39c4b --- /dev/null +++ b/client/stream_test.go @@ -0,0 +1,151 @@ +package client_test + +import ( + "os" + "testing" + + "github.com/satori/go.uuid" + "github.com/suzuki-shunsuke/go-graylog/test" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestGetStreams(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + if _, _, _, err := client.GetStreams(); err != nil { + t.Fatal(err) + } +} + +func TestCreateStream(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + // nil check + if _, err := client.CreateStream(nil); err == nil { + t.Fatal("stream is nil") + } + // success + u, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet(u.String()) + if _, err := client.CreateIndexSet(is); err != nil { + t.Fatal(err) + } + testutil.WaitAfterCreateIndexSet(server) + // clean + defer func(id string) { + if _, err := client.DeleteIndexSet(id); err != nil { + t.Fatal(err) + } + testutil.WaitAfterDeleteIndexSet(server) + }(is.ID) + + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := client.CreateStream(stream); err != nil { + t.Fatal(err) + } + // clean + defer client.DeleteStream(stream.ID) +} + +func TestGetEnabledStreams(t *testing.T) { + test.TestGetEnabledStreams(t) +} + +func TestGetStream(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + u, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet(u.String()) + if _, err := client.CreateIndexSet(is); err != nil { + t.Fatal(err) + } + testutil.WaitAfterCreateIndexSet(server) + defer func(id string) { + client.DeleteIndexSet(id) + testutil.WaitAfterDeleteIndexSet(server) + }(is.ID) + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := client.CreateStream(stream); err != nil { + t.Fatal(err) + } + defer client.DeleteStream(stream.ID) + + r, _, err := client.GetStream(stream.ID) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("stream is nil") + } + if r.ID != stream.ID { + t.Fatalf(`stream.ID = "%s", wanted "%s"`, r.ID, stream.ID) + } + if _, _, err := client.GetStream(""); err == nil { + t.Fatal("id is required") + } + if _, _, err := client.GetStream("h"); err == nil { + t.Fatal("stream should not be found") + } +} + +func TestUpdateStream(t *testing.T) { + test.TestUpdateStream(t) +} + +func TestDeleteStream(t *testing.T) { + if err := os.Setenv("GRAYLOG_WEB_ENDPOINT_URI", "http://localhost:9000/api"); err != nil { + t.Fatal(err) + } + defer os.Unsetenv("GRAYLOG_WEB_ENDPOINT_URI") + + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + // id required + if _, err := client.DeleteStream(""); err == nil { + t.Fatal("id is required") + } + // invalid id + if _, err := client.DeleteStream("h"); err == nil { + t.Fatal(`no stream with id "h" is found`) + } +} + +func TestPauseStream(t *testing.T) { + test.TestPauseStream(t) +} + +func TestResumeStream(t *testing.T) { + test.TestResumeStream(t) +} diff --git a/client/user.go b/client/user.go new file mode 100644 index 00000000..c9b7a41a --- /dev/null +++ b/client/user.go @@ -0,0 +1,99 @@ +package client + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/suzuki-shunsuke/go-graylog" +) + +// CreateUser creates a new user account. +func (client *Client) CreateUser(user *graylog.User) (*ErrorInfo, error) { + return client.CreateUserContext(context.Background(), user) +} + +// CreateUserContext creates a new user account with a context. +func (client *Client) CreateUserContext( + ctx context.Context, user *graylog.User, +) (*ErrorInfo, error) { + if user == nil { + return nil, fmt.Errorf("user is nil") + } + return client.callPost(ctx, client.Endpoints().Users(), user, nil) +} + +// GetUsers returns all users. +func (client *Client) GetUsers() ([]graylog.User, *ErrorInfo, error) { + return client.GetUsersContext(context.Background()) +} + +// GetUsersContext returns all users with a context. +func (client *Client) GetUsersContext(ctx context.Context) ([]graylog.User, *ErrorInfo, error) { + users := &graylog.UsersBody{} + ei, err := client.callGet( + ctx, client.Endpoints().Users(), nil, users) + return users.Users, ei, err +} + +// GetUser returns a given user. +func (client *Client) GetUser(name string) (*graylog.User, *ErrorInfo, error) { + return client.GetUserContext(context.Background(), name) +} + +// GetUserContext returns a given user with a context. +func (client *Client) GetUserContext( + ctx context.Context, name string, +) (*graylog.User, *ErrorInfo, error) { + if name == "" { + return nil, nil, errors.New("name is empty") + } + u, err := client.Endpoints().User(name) + if err != nil { + return nil, nil, err + } + user := &graylog.User{} + ei, err := client.callGet(ctx, u.String(), nil, user) + return user, ei, err +} + +// UpdateUser updates a given user. +func (client *Client) UpdateUser(prms *graylog.UserUpdateParams) (*ErrorInfo, error) { + return client.UpdateUserContext(context.Background(), prms) +} + +// UpdateUserContext updates a given user with a context. +func (client *Client) UpdateUserContext( + ctx context.Context, prms *graylog.UserUpdateParams, +) (*ErrorInfo, error) { + if prms == nil { + return nil, fmt.Errorf("user is nil") + } + if prms.Username == "" { + return nil, errors.New("name is empty") + } + u, err := client.Endpoints().User(prms.Username) + if err != nil { + return nil, err + } + return client.callPut(ctx, u.String(), prms, nil) +} + +// DeleteUser deletes a given user. +func (client *Client) DeleteUser(name string) (*ErrorInfo, error) { + return client.DeleteUserContext(context.Background(), name) +} + +// DeleteUserContext deletes a given user with a context. +func (client *Client) DeleteUserContext( + ctx context.Context, name string, +) (*ErrorInfo, error) { + if name == "" { + return nil, errors.New("name is empty") + } + u, err := client.Endpoints().User(name) + if err != nil { + return nil, err + } + return client.callDelete(ctx, u.String(), nil, nil) +} diff --git a/client/user_test.go b/client/user_test.go new file mode 100644 index 00000000..27b32fee --- /dev/null +++ b/client/user_test.go @@ -0,0 +1,134 @@ +package client_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestDeleteUser(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + if _, err := client.DeleteUser(""); err == nil { + t.Fatal("username is required") + } + if _, err := client.DeleteUser("h"); err == nil { + t.Fatal(`no user with name "h" is found`) + } +} + +func TestCreateUser(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + // nil check + if _, err := client.CreateUser(nil); err == nil { + t.Fatal("user is nil") + } + user := testutil.User() + client.DeleteUser(user.Username) + if _, err := client.CreateUser(user); err != nil { + t.Fatal(err) + } + defer client.DeleteUser(user.Username) + // error check + user.Username = "" + if _, err := client.CreateUser(user); err == nil { + t.Fatal("user name is empty") + } +} + +func TestGetUsers(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + users, _, err := client.GetUsers() + if err != nil { + t.Fatal(err) + } + if users == nil { + t.Fatal("users is nil") + } + if len(users) == 0 { + t.Fatal("users is empty") + } +} + +func TestGetUser(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + user := testutil.User() + client.DeleteUser(user.Username) + if _, _, err := client.GetUser(user.Username); err == nil { + t.Fatal("user should be deleted") + } + if _, err := client.CreateUser(user); err != nil { + t.Fatal(err) + } + defer client.DeleteUser(user.Username) + u, _, err := client.GetUser(user.Username) + if err != nil { + t.Fatal(err) + } + if u == nil { + t.Fatal("user is nil") + } + if u.Username != user.Username { + t.Fatalf(`user.Username = "%s", wanted "%s"`, u.Username, user.Username) + } + if _, _, err := client.GetUser(""); err == nil { + t.Fatal("user name is required") + } +} + +func TestUpdateUser(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + user := testutil.User() + client.DeleteUser(user.Username) + if _, err := client.UpdateUser(user.NewUpdateParams()); err == nil { + t.Fatal("user should be deleted") + } + if _, err := client.CreateUser(user); err != nil { + t.Fatal(err) + } + defer client.DeleteUser(user.Username) + if _, err := client.UpdateUser(user.NewUpdateParams()); err != nil { + t.Fatal(err) + } + user.Username = "" + if _, err := client.UpdateUser(user.NewUpdateParams()); err == nil { + t.Fatal("user name is required") + } + if _, err := client.UpdateUser(nil); err == nil { + t.Fatal("user is required") + } +} diff --git a/client/util.go b/client/util.go new file mode 100644 index 00000000..5483bed4 --- /dev/null +++ b/client/util.go @@ -0,0 +1,81 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/pkg/errors" +) + +func (client *Client) callGet( + ctx context.Context, endpoint string, input, output interface{}) (*ErrorInfo, error) { + return client.callAPI(ctx, http.MethodGet, endpoint, input, output) +} + +func (client *Client) callPost( + ctx context.Context, endpoint string, input, output interface{}) (*ErrorInfo, error) { + return client.callAPI(ctx, http.MethodPost, endpoint, input, output) +} + +func (client *Client) callPut( + ctx context.Context, endpoint string, input, output interface{}) (*ErrorInfo, error) { + return client.callAPI(ctx, http.MethodPut, endpoint, input, output) +} + +func (client *Client) callDelete( + ctx context.Context, endpoint string, input, output interface{}) (*ErrorInfo, error) { + return client.callAPI(ctx, http.MethodDelete, endpoint, input, output) +} + +func (client *Client) callAPI( + ctx context.Context, method, endpoint string, input, output interface{}, +) (*ErrorInfo, error) { + // prepare request + var ( + req *http.Request + err error + ) + if input != nil { + reqBody := &bytes.Buffer{} + if err := json.NewEncoder(reqBody).Encode(input); err != nil { + return nil, errors.Wrap(err, "failed to encode request body") + } + req, err = http.NewRequest(method, endpoint, reqBody) + } else { + req, err = http.NewRequest(method, endpoint, nil) + } + if err != nil { + return nil, errors.Wrap(err, "failed to call http.NewRequest") + } + ei := &ErrorInfo{Request: req} + req.SetBasicAuth(client.Name(), client.Password()) + req.WithContext(ctx) + req.Header.Set("Content-Type", "application/json") + hc := &http.Client{} + // request + resp, err := hc.Do(req) + if err != nil { + return ei, errors.Wrap( + err, fmt.Sprintf("failed to call Graylog API: %s %s", method, endpoint)) + } + defer resp.Body.Close() + ei.Response = resp + + if resp.StatusCode >= 400 { + if err := json.NewDecoder(resp.Body).Decode(ei); err != nil { + return ei, errors.Wrap( + err, "failed to parse response body as ErrorInfo") + } + return ei, errors.New(ei.Message) + } + if output != nil { + if err := json.NewDecoder(ei.Response.Body).Decode(output); err != nil { + return ei, errors.Wrap( + err, "failed to decode response body") + } + } + return ei, nil +} diff --git a/codecov-test.sh b/codecov-test.sh new file mode 100644 index 00000000..0b5d2917 --- /dev/null +++ b/codecov-test.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# https://github.com/codecov/example-go#caveat-multiple-files + +set -e +echo "" > coverage.txt + +for d in $(go list ./... | grep -v vendor | grep -v terraform); do + go test -race -coverprofile=profile.out -covermode=atomic $d + if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out + fi +done + +go test -v -race -coverprofile=profile.out -covermode=atomic ./terraform/... +if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out +fi diff --git a/codecov.sh b/codecov.sh new file mode 100644 index 00000000..638c1300 --- /dev/null +++ b/codecov.sh @@ -0,0 +1,3 @@ +if [ -n "$CODECOV_TOKEN" ]; then + bash <(curl -s https://codecov.io/bash) +fi diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 00000000..28fe5c5b --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1 @@ +module.exports = {extends: ['@commitlint/config-conventional']} diff --git a/doc.go b/doc.go new file mode 100644 index 00000000..a74ba2ee --- /dev/null +++ b/doc.go @@ -0,0 +1,7 @@ +/* +Package graylog provides Golang's structs which represents Graylog resource such as roles and users. +Client and mock server and terraform provider are provided as subpackages. + +https://godoc.org/github.com/suzuki-shunsuke/go-graylog/#pkg-subdirectories +*/ +package graylog diff --git a/docker-compose.yml.tmpl b/docker-compose.yml.tmpl new file mode 100644 index 00000000..2d660e80 --- /dev/null +++ b/docker-compose.yml.tmpl @@ -0,0 +1,43 @@ +--- +version: '2' +services: + # MongoDB: https://hub.docker.com/_/mongo/ + mongodb: + image: mongo:3 + # Elasticsearch: https://www.elastic.co/guide/en/elasticsearch/reference/5.6/docker.html + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:5.6.3 + environment: + - http.host=0.0.0.0 + - transport.host=localhost + - network.host=0.0.0.0 + # Disable X-Pack security: https://www.elastic.co/guide/en/elasticsearch/reference/5.6/security-settings.html#general-security-settings + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms1024m -Xmx1024m" + ulimits: + memlock: + soft: -1 + hard: -1 + mem_limit: 2g + # Graylog: https://hub.docker.com/r/graylog/graylog/ + graylog: + image: graylog/graylog:2.4.3-1 + environment: + # CHANGE ME! + - GRAYLOG_PASSWORD_SECRET=somepasswordpepper + # Password: admin + - GRAYLOG_ROOT_PASSWORD_SHA2=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 + - GRAYLOG_WEB_ENDPOINT_URI=http://127.0.0.1:9000/api + links: + - mongodb:mongo + - elasticsearch + depends_on: + - mongodb + - elasticsearch + ports: + # Graylog web interface and REST API + - 9000:9000 + # GELF TCP + - 12201:12201 + # GELF UDP + - 12201:12201/udp diff --git a/env.sh.tmpl b/env.sh.tmpl new file mode 100644 index 00000000..934ccdd5 --- /dev/null +++ b/env.sh.tmpl @@ -0,0 +1,3 @@ +export GRAYLOG_AUTH_NAME=admin +export GRAYLOG_AUTH_PASSWORD=admin +export GRAYLOG_WEB_ENDPOINT_URI=http://localhost:9000/api diff --git a/index_set.go b/index_set.go new file mode 100644 index 00000000..dc89daf0 --- /dev/null +++ b/index_set.go @@ -0,0 +1,215 @@ +package graylog + +import ( + "time" + + "github.com/suzuki-shunsuke/go-ptr" +) + +const ( + // rotation_strategy_class + MessageCountRotationStrategy string = "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategy" + SizeBasedRotationStrategy string = "org.graylog2.indexer.rotation.strategies.SizeBasedRotationStrategy" + TimeBasedRotationStrategy string = "org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategy" + // rotation_strategy.type + MessageCountRotationStrategyConfig string = "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategyConfig" + SizeBasedRotationStrategyConfig string = "org.graylog2.indexer.rotation.strategies.SizeBasedRotationStrategyConfig" + TimeBasedRotationStrategyConfig string = "org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategyConfig" + // retention_strategy_class + DeletionRetentionStrategy string = "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy" + ClosingRetentionStrategy string = "org.graylog2.indexer.retention.strategies.ClosingRetentionStrategy" + NoopRetentionStrategy string = "org.graylog2.indexer.retention.strategies.NoopRetentionStrategy" + // retention_strategy_class.type + DeletionRetentionStrategyConfig string = "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig" + ClosingRetentionStrategyConfig string = "org.graylog2.indexer.retention.strategies.ClosingRetentionStrategyConfig" + NoopRetentionStrategyConfig string = "org.graylog2.indexer.retention.strategies.NoopRetentionStrategyConfig" + CreationDateFormat string = "2006-01-02T15:04:05.000Z" +) + +// IndexSet represents a Graylog's Index Set. +// http://docs.graylog.org/en/2.4/pages/configuration/index_model.html#index-set-configuration +type IndexSet struct { + // required + Title string `json:"title,omitempty" v-create:"required"` + // ^[a-z0-9][a-z0-9_+-]*$ + IndexPrefix string `json:"index_prefix,omitempty" v-create:"required,indexprefixregexp"` + // ex. "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategy" + RotationStrategyClass string `json:"rotation_strategy_class,omitempty" v-create:"required"` + RotationStrategy *RotationStrategy `json:"rotation_strategy,omitempty" v-create:"required"` + // ex. "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy" + RetentionStrategyClass string `json:"retention_strategy_class,omitempty" v-create:"required"` + RetentionStrategy *RetentionStrategy `json:"retention_strategy,omitempty" v-create:"required"` + // ex. "2018-02-20T11:37:19.305Z" + CreationDate string `json:"creation_date,omitempty"` + IndexAnalyzer string `json:"index_analyzer,omitempty" v-create:"required"` + Shards int `json:"shards,omitempty" v-create:"required"` + IndexOptimizationMaxNumSegments int `json:"index_optimization_max_num_segments,omitempty" v-create:"required"` + + ID string `json:"id,omitempty" v-create:"isdefault"` + + Description string `json:"description,omitempty"` + Replicas int `json:"replicas,omitempty"` + IndexOptimizationDisabled bool `json:"index_optimization_disabled,omitempty"` + Writable bool `json:"writable,omitempty"` + Default bool `json:"default,omitempty"` + Stats *IndexSetStats `json:"-"` +} + +// NewUpdateParams +func (is *IndexSet) NewUpdateParams() *IndexSetUpdateParams { + return &IndexSetUpdateParams{ + Title: is.Title, + IndexPrefix: is.IndexPrefix, + RotationStrategyClass: is.RotationStrategyClass, + RotationStrategy: is.RotationStrategy, + RetentionStrategyClass: is.RetentionStrategyClass, + RetentionStrategy: is.RetentionStrategy, + IndexAnalyzer: is.IndexAnalyzer, + Shards: is.Shards, + IndexOptimizationMaxNumSegments: is.IndexOptimizationMaxNumSegments, + ID: is.ID, + + Description: ptr.PStr(is.Description), + Replicas: ptr.PInt(is.Replicas), + IndexOptimizationDisabled: ptr.PBool(is.IndexOptimizationDisabled), + Writable: ptr.PBool(is.Writable), + } +} + +// IndexSetUpdateParams represents a Graylog's Index Set Update API's parameter. +// http://docs.graylog.org/en/2.4/pages/configuration/index_model.html#index-set-configuration +type IndexSetUpdateParams struct { + Title string `json:"title" v-update:"required"` + IndexPrefix string `json:"index_prefix" v-update:"required,indexprefixregexp"` + RotationStrategyClass string `json:"rotation_strategy_class" v-update:"required"` + RotationStrategy *RotationStrategy `json:"rotation_strategy" v-update:"required"` + RetentionStrategyClass string `json:"retention_strategy_class" v-update:"required"` + RetentionStrategy *RetentionStrategy `json:"retention_strategy" v-update:"required"` + IndexAnalyzer string `json:"index_analyzer" v-update:"required"` + Shards int `json:"shards" v-update:"required"` + IndexOptimizationMaxNumSegments int `json:"index_optimization_max_num_segments" v-update:"required"` + ID string `json:"id" v-update:"required,objectid"` + + Description *string `json:"description,omitempty"` + Replicas *int `json:"replicas,omitempty"` + IndexOptimizationDisabled *bool `json:"index_optimization_disabled,omitempty"` + Writable *bool `json:"writable,omitempty"` +} + +// SetCreateDefaultValues sets the default values of Create Index Set API. +func (is *IndexSet) SetCreateDefaultValues() { + if is.CreationDate == "" { + is.SetCreationTime(time.Now()) + } + if is.Shards == 0 { + is.Shards = 4 + } + if is.IndexAnalyzer == "" { + is.IndexAnalyzer = "standard" + } +} + +// CreationTime returns a creation date converted to time.Time. +func (is *IndexSet) CreationTime() (time.Time, error) { + return time.Parse(CreationDateFormat, is.CreationDate) +} + +// SetCreationTime sets a creation date with time.Time. +func (is *IndexSet) SetCreationTime(t time.Time) { + is.CreationDate = t.Format(CreationDateFormat) +} + +// RotationStrategy represents a Graylog's Index Set Rotation Strategy. +type RotationStrategy struct { + // ex. "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategyConfig" + Type string `json:"type,omitempty"` + // ex. 20000000 + // Maximum number of documents in an index before it gets rotated + MaxDocsPerIndex int `json:"max_docs_per_index,omitempty"` + // time based + // How long an index gets written to before it is rotated. (i.e. "P1D" for 1 day, "PT6H" for 6 hours) + RotationPeriod string `json:"rotation_period,omitempty"` + // size based + // Maximum size of an index before it gets rotated + MaxSize int `json:"max_size,omitempty"` +} + +// RetentionStrategy represents a Graylog's Index Set Retention Strategy. +type RetentionStrategy struct { + // ex. "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig" + Type string `json:"type,omitempty"` + // ex. 20 + MaxNumberOfIndices int `json:"max_number_of_indices,omitempty"` +} + +type IndexSetsBody struct { + IndexSets []IndexSet `json:"index_sets"` + Stats map[string]IndexSetStats `json:"stats"` + Total int `json:"total"` +} + +// NewMessageCountRotationStrategy returns a new message count based RotationStrategy. +func NewMessageCountRotationStrategy(count int) *RotationStrategy { + if count <= 0 { + count = 20000000 + } + return &RotationStrategy{ + Type: MessageCountRotationStrategyConfig, + MaxDocsPerIndex: count, + } +} + +// NewSizeBasedRotationStrategy returns a new size based RotationStrategy. +func NewSizeBasedRotationStrategy(size int) *RotationStrategy { + if size <= 0 { + size = 1073741824 + } + return &RotationStrategy{ + Type: SizeBasedRotationStrategyConfig, + MaxSize: size, + } +} + +// NewTimeBasedRotationStrategy returns a new time based RotationStrategy. +func NewTimeBasedRotationStrategy(period string) *RotationStrategy { + if period == "" { + period = "P1D" + } + return &RotationStrategy{ + Type: TimeBasedRotationStrategyConfig, + RotationPeriod: period, + } +} + +// NewDeletionRetentionStrategy returns a new deletion RetentionStrategy. +func NewDeletionRetentionStrategy(num int) *RetentionStrategy { + if num <= 0 { + num = 20 + } + return &RetentionStrategy{ + Type: DeletionRetentionStrategyConfig, + MaxNumberOfIndices: num, + } +} + +// NewClosingRetentionStrategy returns a new closing RetentionStrategy. +func NewClosingRetentionStrategy(num int) *RetentionStrategy { + if num <= 0 { + num = 20 + } + return &RetentionStrategy{ + Type: ClosingRetentionStrategyConfig, + MaxNumberOfIndices: num, + } +} + +// NewNoopRetentionStrategy returns a new noop RetentionStrategy. +func NewNoopRetentionStrategy(num int) *RetentionStrategy { + if num <= 0 { + num = 20 + } + return &RetentionStrategy{ + Type: NoopRetentionStrategyConfig, + MaxNumberOfIndices: num, + } +} diff --git a/index_set_stats.go b/index_set_stats.go new file mode 100644 index 00000000..4325df07 --- /dev/null +++ b/index_set_stats.go @@ -0,0 +1,8 @@ +package graylog + +// IndexSetStats represents a Graylog's Index Set Stats. +type IndexSetStats struct { + Indices int `json:"indices"` + Documents int `json:"documents"` + Size int `json:"size"` +} diff --git a/index_set_test.go b/index_set_test.go new file mode 100644 index 00000000..68f3fec0 --- /dev/null +++ b/index_set_test.go @@ -0,0 +1,104 @@ +package graylog_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestIndexSetNewUpdateParams(t *testing.T) { + is := testutil.IndexSet("hoge") + prms := is.NewUpdateParams() + if is.Title != prms.Title { + t.Fatalf(`prms.Title = "%s", wanted "%s"`, prms.Title, is.Title) + } +} + +func TestSetCreateDefaultValues(t *testing.T) { + is := &graylog.IndexSet{} + is.SetCreateDefaultValues() + if is.CreationDate == "" { + t.Fatal("is.CreationDate must be set") + } + if is.Shards == 0 { + t.Fatal("is.Shards must be set") + } + if is.IndexAnalyzer != "standard" { + t.Fatalf(`is.IndexAnalyzer = "%s", wanted "standard"`, is.IndexAnalyzer) + } +} + +func TestCreationTime(t *testing.T) { + is := &graylog.IndexSet{} + is.SetCreateDefaultValues() + if _, err := is.CreationTime(); err != nil { + t.Fatal(err) + } +} + +func TestNewMessageCountRotationStrategy(t *testing.T) { + s := graylog.NewMessageCountRotationStrategy(0) + if s.MaxDocsPerIndex == 0 { + t.Fatal("s.MaxDocsPerIndex must not be 0") + } + s = graylog.NewMessageCountRotationStrategy(10) + if s.MaxDocsPerIndex != 10 { + t.Fatalf("s.MaxDocsPerIndex = %d, wanted 10", s.MaxDocsPerIndex) + } +} + +func TestNewSizeBasedRotationStrategy(t *testing.T) { + s := graylog.NewSizeBasedRotationStrategy(0) + if s.MaxSize == 0 { + t.Fatal("s.MaxSize must not be 0") + } + s = graylog.NewSizeBasedRotationStrategy(10) + if s.MaxSize != 10 { + t.Fatalf("s.MaxSize = %d, wanted 10", s.MaxSize) + } +} + +func TestNewTimeBasedRotationStrategy(t *testing.T) { + s := graylog.NewTimeBasedRotationStrategy("") + if s.RotationPeriod == "" { + t.Fatal(`s.RotationPeriod must not be ""`) + } + s = graylog.NewTimeBasedRotationStrategy("a") + if s.RotationPeriod != "a" { + t.Fatalf(`s.RotationPeriod = "%s", wanted "a"`, s.RotationPeriod) + } +} + +func TestNewDeletionRetentionStrategy(t *testing.T) { + s := graylog.NewDeletionRetentionStrategy(0) + if s.MaxNumberOfIndices == 0 { + t.Fatal("s.MaxNumberOfIndices must not be 0") + } + s = graylog.NewDeletionRetentionStrategy(10) + if s.MaxNumberOfIndices != 10 { + t.Fatalf(`s.MaxNumberOfIndices = %d, wanted 10`, s.MaxNumberOfIndices) + } +} + +func TestNewClosingRetentionStrategy(t *testing.T) { + s := graylog.NewClosingRetentionStrategy(0) + if s.MaxNumberOfIndices == 0 { + t.Fatal("s.MaxNumberOfIndices must not be 0") + } + s = graylog.NewClosingRetentionStrategy(10) + if s.MaxNumberOfIndices != 10 { + t.Fatalf(`s.MaxNumberOfIndices = %d, wanted 10`, s.MaxNumberOfIndices) + } +} + +func TestNewNoopRetentionStrategy(t *testing.T) { + s := graylog.NewNoopRetentionStrategy(0) + if s.MaxNumberOfIndices == 0 { + t.Fatal("s.MaxNumberOfIndices must not be 0") + } + s = graylog.NewNoopRetentionStrategy(10) + if s.MaxNumberOfIndices != 10 { + t.Fatalf(`s.MaxNumberOfIndices = %d, wanted 10`, s.MaxNumberOfIndices) + } +} diff --git a/input.go b/input.go new file mode 100644 index 00000000..5a75b450 --- /dev/null +++ b/input.go @@ -0,0 +1,105 @@ +package graylog + +import ( + "encoding/json" + + "github.com/suzuki-shunsuke/go-graylog/util" + "github.com/suzuki-shunsuke/go-ptr" +) + +// Input represents Graylog Input. +type Input struct { + // Select a name of your new input that describes it. + Title string `json:"title,omitempty" v-create:"required"` + // https://github.com/Graylog2/graylog2-server/issues/3480 + // update input overwrite attributes + Attrs InputAttrs `json:"attributes,omitempty" v-create:"required"` + + ID string `json:"id,omitempty" v-create:"isdefault"` + + // Should this input start on all nodes + Global bool `json:"global,omitempty"` + // On which node should this input start + // ex. "2ad6b340-3e5f-4a96-ae81-040cfb8b6024" + Node string `json:"node,omitempty"` + // ex. 2018-02-24T03:02:26.001Z + CreatedAt string `json:"created_at,omitempty" v-create:"isdefault"` + // ex. "admin" + CreatorUserID string `json:"creator_user_id,omitempty" v-create:"isdefault"` + // ContextPack `json:"context_pack,omitempty"` + // StaticFields `json:"static_fields,omitempty"` +} + +func (input Input) Type() string { + if input.Attrs == nil { + return "" + } + return input.Attrs.InputType() +} + +// NewUpdateParams converts Input to InputUpdateParams. +func (input *Input) NewUpdateParams() *InputUpdateParams { + return &InputUpdateParams{ + ID: input.ID, + Title: input.Title, + Type: input.Type(), + Attrs: input.Attrs, + Node: input.Node, + Global: ptr.PBool(input.Global), + } +} + +// InputUpdateParams represents Graylog Input update API's parameter. +type InputUpdateParams struct { + ID string `json:"id,omitempty" v-update:"required,objectid"` + Title string `json:"title,omitempty" v-update:"required"` + Type string `json:"type,omitempty" v-update:"required"` + Attrs InputAttrs `json:"attributes,omitempty" v-update:"required"` + Global *bool `json:"global,omitempty"` + Node string `json:"node,omitempty"` +} + +func (input *Input) ToData() (*InputData, error) { + d := &InputData{ + Title: input.Title, + Type: input.Type(), + ID: input.ID, + Global: input.Global, + Node: input.Node, + CreatedAt: input.CreatedAt, + CreatorUserID: input.CreatorUserID, + Attrs: map[string]interface{}{}, + } + if input.Attrs == nil { + return d, nil + } + return d, util.MSDecode(input.Attrs, &d.Attrs) +} + +// UnmarshalJSON is the implementation of the json.Unmarshaler interface. +func (input *Input) UnmarshalJSON(b []byte) error { + d, err := input.ToData() + if err != nil { + return err + } + if err := json.Unmarshal(b, d); err != nil { + return err + } + return d.ToInput(input) +} + +// MarshalJSON is the implementation of the json.Marshaler interface. +func (input *Input) MarshalJSON() ([]byte, error) { + d, err := input.ToData() + if err != nil { + return nil, err + } + return json.Marshal(d) +} + +// InputsBody represents Get Inputs API's response body. +// Basically users don't use this struct, but this struct is public because some sub packages use this struct. +type InputsBody struct { + Inputs []Input `json:"inputs"` + Total int `json:"total"` +} diff --git a/input_attrs.go b/input_attrs.go new file mode 100644 index 00000000..de675561 --- /dev/null +++ b/input_attrs.go @@ -0,0 +1,164 @@ +package graylog + +import ( + "fmt" + "reflect" + "strings" + + "github.com/suzuki-shunsuke/go-set" +) + +var ( + InputAttrsIntFieldSet = set.NewStrSet() + InputAttrsBoolFieldSet = set.NewStrSet() + InputAttrsStrFieldSet = set.NewStrSet() + inputAttrsList = []NewInputAttrs{ + NewInputAWSFlowLogsAttrs, + NewInputAWSCloudWatchLogsAttrs, + NewInputAWSCloudTrailAttrs, + NewInputBeatsAttrs, + NewInputCEFAMQPAttrs, + NewInputCEFKafkaAttrs, + NewInputCEFTCPAttrs, + NewInputCEFUDPAttrs, + NewInputFakeHTTPMessageAttrs, + NewInputGELFAMQPAttrs, + NewInputGELFHTTPAttrs, + NewInputGELFKafkaAttrs, + NewInputGELFTCPAttrs, + NewInputGELFUDPAttrs, + NewInputJSONPathAttrs, + NewInputNetFlowUDPAttrs, + NewInputRawAMQPAttrs, + NewInputSyslogAMQPAttrs, + NewInputSyslogKafkaAttrs, + NewInputSyslogTCPAttrs, + NewInputSyslogUDPAttrs, + } + // initialize at init function for preventing initialization loop + attrsSet = &inputAttrsSet{} +) + +func init() { + attrsSet = &inputAttrsSet{ + data: map[string]NewInputAttrs{}, + GetUnknownType: func(data map[string]NewInputAttrs, t string) InputAttrs { + return &InputUnknownAttrs{inputType: t} + }, + GetByType: func(data map[string]NewInputAttrs, t string) InputAttrs { + if data == nil { + return attrsSet.GetUnknownType(data, t) + } + a, ok := data[t] + if !ok { + return attrsSet.GetUnknownType(data, t) + } + return a() + }, + } + if err := SetInputAttrs(inputAttrsList...); err != nil { + panic(err) + } + for _, attrs := range inputAttrsList { + a := attrs() + ts := reflect.ValueOf(a).Elem().Type() + n := ts.NumField() + for i := 0; i < n; i++ { + f := ts.Field(i) + tag := strings.Split(f.Tag.Get("json"), ",")[0] + switch f.Type.Kind() { + case reflect.String: + InputAttrsStrFieldSet.Add(tag) + case reflect.Int: + InputAttrsIntFieldSet.Add(tag) + case reflect.Bool: + InputAttrsBoolFieldSet.Add(tag) + default: + panic(fmt.Sprintf("invalid type: %v", f.Type.Kind())) + } + } + } +} + +// GetUnknownTypeInputAttrsIntf returns an unknown type InputAttrs. +type GetUnknownTypeInputAttrsIntf func(map[string]NewInputAttrs, string) InputAttrs + +// GetInputAttrsByTypeIntf returns a given type InputAttrs. +type GetInputAttrsByTypeIntf func(map[string]NewInputAttrs, string) InputAttrs + +// SetFuncGetUnknownTypeInputAttrs customizes NewInputAttrsByType's behavior. +func SetFuncGetUnknownTypeInputAttrs(f GetUnknownTypeInputAttrsIntf) { + attrsSet.GetUnknownType = f +} + +// GetFuncGetUnknownTypeInputAttrs returns the global GetUnknownTypeInputAttrs function. +// Mainly this is used to prevent global pollution at test. +// +// f := graylog.GetFuncGetUnknownTypeInputAttrs() +// // change the global function temporary +// defer graylog.SetFuncGetUnknownTypeInputAttrs(f) +// graylog.SetFuncGetUnknownTypeInputAttrs(customFunc) +func GetFuncGetUnknownTypeInputAttrs() GetUnknownTypeInputAttrsIntf { + if attrsSet == nil { + return nil + } + return attrsSet.GetUnknownType +} + +// SetFuncGetInputAttrsByType customizes NewInputAttrsByType's behavior. +func SetFuncGetInputAttrsByType(f GetInputAttrsByTypeIntf) { + attrsSet.GetByType = f +} + +// GetFuncGetInputAttrsByType returns the global GetInputAttrsByType function. +// Mainly this is used to prevent global pollution at test. +// +// f := graylog.GetFuncGetInputAttrsByType() +// // change the global function temporary +// defer graylog.SetFuncGetInputAttrsByType(f) +// graylog.SetFuncGetInputAttrsByType(customFunc) +func GetFuncGetInputAttrsByType() GetInputAttrsByTypeIntf { + if attrsSet == nil { + return nil + } + return attrsSet.GetByType +} + +// NewInputAttrsByType returns a new InputAttrs. +func NewInputAttrsByType(t string) InputAttrs { + return attrsSet.GetByType(attrsSet.data, t) +} + +// SetInputAttrs sets InputAttrs. +// You can add the custom InputAttrs and override existing InputAttrs. +func SetInputAttrs(args ...NewInputAttrs) error { + if attrsSet == nil { + attrsSet = &inputAttrsSet{data: map[string]NewInputAttrs{}} + } + if attrsSet.data == nil { + attrsSet.data = map[string]NewInputAttrs{} + } + for _, f := range args { + a := f() + if reflect.TypeOf(a).Kind() != reflect.Ptr { + return fmt.Errorf("NewInputAttrs must return pointer") + } + attrsSet.data[a.InputType()] = f + } + return nil +} + +// NewInputAttrs is the constructor of InputAttrs. +type NewInputAttrs func() InputAttrs + +// InputAttrs represents Input Attributes. +// A receiver must be a pointer. +type InputAttrs interface { + InputType() string +} + +type inputAttrsSet struct { + data map[string]NewInputAttrs + GetUnknownType GetUnknownTypeInputAttrsIntf + GetByType GetInputAttrsByTypeIntf +} diff --git a/input_attrs_test.go b/input_attrs_test.go new file mode 100644 index 00000000..04ed7d80 --- /dev/null +++ b/input_attrs_test.go @@ -0,0 +1,54 @@ +package graylog_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog" +) + +func TestNewInputAttrsByType(t *testing.T) { + attrs := graylog.NewInputAttrsByType("hoge") + if attrs.InputType() != "hoge" { + t.Fatalf(`attrs.InputType() = "%s", wanted "hoge"`, attrs.InputType()) + } +} + +type CustomInputAttrs struct { + Type string + Foo string +} + +func (attrs CustomInputAttrs) InputType() string { + return attrs.Type +} + +func TestSetFuncGetUnknownType(t *testing.T) { + f := graylog.GetFuncGetUnknownTypeInputAttrs() + defer graylog.SetFuncGetUnknownTypeInputAttrs(f) + graylog.SetFuncGetUnknownTypeInputAttrs(func(data map[string]graylog.NewInputAttrs, t string) graylog.InputAttrs { + return &CustomInputAttrs{Type: t, Foo: "foo"} + }) + attrs := graylog.NewInputAttrsByType("hoge") + a, ok := attrs.(*CustomInputAttrs) + if !ok { + t.Fatalf("attrs is not CustomInputAttrs") + } + if a.Foo != "foo" { + t.Fatalf(`a.Foo = "%s", wanted "foo"`, a.Foo) + } +} +func TestSetFuncGetInputAttrsByType(t *testing.T) { + f := graylog.GetFuncGetInputAttrsByType() + defer graylog.SetFuncGetInputAttrsByType(f) + graylog.SetFuncGetInputAttrsByType(func(data map[string]graylog.NewInputAttrs, t string) graylog.InputAttrs { + return &CustomInputAttrs{Type: t, Foo: "foo"} + }) + attrs := graylog.NewInputAttrsByType(graylog.InputTypeBeats) + a, ok := attrs.(*CustomInputAttrs) + if !ok { + t.Fatalf("attrs is not CustomInputAttrs") + } + if a.Foo != "foo" { + t.Fatalf(`a.Foo = "%s", wanted "foo"`, a.Foo) + } +} diff --git a/input_aws_flow_logs.go b/input_aws_flow_logs.go new file mode 100644 index 00000000..ad454194 --- /dev/null +++ b/input_aws_flow_logs.go @@ -0,0 +1,25 @@ +package graylog + +const ( + InputTypeAWSFlowLogs string = "org.graylog.aws.inputs.flowlogs.FlowLogsInput" +) + +// NewInputAWSFlowLogsAttrs is the constructor of InputAWSFlowLogsAttrs. +func NewInputAWSFlowLogsAttrs() InputAttrs { + return &InputAWSFlowLogsAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputAWSFlowLogsAttrs) InputType() string { + return InputTypeAWSFlowLogs +} + +// InputAWSFlowLogsAttrs represents AWS flow logs Input's attributes. +type InputAWSFlowLogsAttrs struct { + AWSRegion string `json:"aws_region,omitempty"` + AWSAssumeRoleArn string `json:"aws_assume_role_arn,omitempty"` + AWSAccessKey string `json:"aws_access_key,omitempty"` + AWSSecretKey string `json:"aws_secret_key,omitempty"` + KinesisStreamName string `json:"kinesis_stream_name,omitempty"` + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` +} diff --git a/input_aws_logs.go b/input_aws_logs.go new file mode 100644 index 00000000..43eb5428 --- /dev/null +++ b/input_aws_logs.go @@ -0,0 +1,26 @@ +package graylog + +const ( + InputTypeAWSCloudWatchLogs string = "org.graylog.aws.inputs.cloudwatch.CloudWatchLogsInput" +) + +// NewInputAWSCloudWatchLogsAttrs is the constructor of InputAWSCloudWatchLogsAttrs. +func NewInputAWSCloudWatchLogsAttrs() InputAttrs { + return &InputAWSCloudWatchLogsAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputAWSCloudWatchLogsAttrs) InputType() string { + return InputTypeAWSCloudWatchLogs +} + +// InputAWSCloudWatchLogsAttrs represents AWS logs Input's attributes. +type InputAWSCloudWatchLogsAttrs struct { + AWSRegion string `json:"aws_region,omitempty"` + AWSAssumeRoleArn string `json:"aws_assume_role_arn,omitempty"` + AWSAccessKey string `json:"aws_access_key,omitempty"` + AWSSecretKey string `json:"aws_secret_key,omitempty"` + KinesisStreamName string `json:"kinesis_stream_name,omitempty"` + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` + OverrideSource string `json:"override_source,omitempty"` +} diff --git a/input_beats.go b/input_beats.go new file mode 100644 index 00000000..bd0b37fc --- /dev/null +++ b/input_beats.go @@ -0,0 +1,30 @@ +package graylog + +const ( + InputTypeBeats string = "org.graylog.plugins.beats.BeatsInput" +) + +// NewInputBeatsAttrs is the constructor of InputBeatsAttrs. +func NewInputBeatsAttrs() InputAttrs { + return &InputBeatsAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputBeatsAttrs) InputType() string { + return InputTypeBeats +} + +// InputBeatsAttrs represents Beats Input's attributes. +type InputBeatsAttrs struct { + BindAddress string `json:"bind_address,omitempty" v-create:"required" v-update:"required"` + OverrideSource string `json:"override_source,omitempty"` + TLSKeyFile string `json:"tls_key_file,omitempty"` + TLSKeyPassword string `json:"tls_key_password,omitempty"` + TLSClientAuthCertFile string `json:"tls_client_auth_cert_file,omitempty"` + TLSClientAuth string `json:"tls_client_auth,omitempty"` + TLSCertFile string `json:"tls_cert_file,omitempty"` + TLSEnable bool `json:"tls_enable,omitempty"` + TCPKeepAlive bool `json:"tcp_keepalive,omitempty"` + Port int `json:"port,omitempty" v-create:"required" v-update:"required"` + RecvBufferSize int `json:"recv_buffer_size,omitempty" v-create:"required" v-update:"required"` +} diff --git a/input_cef_amqp.go b/input_cef_amqp.go new file mode 100644 index 00000000..73c94be5 --- /dev/null +++ b/input_cef_amqp.go @@ -0,0 +1,37 @@ +package graylog + +const ( + InputTypeCEFAMQP string = "org.graylog.plugins.cef.input.CEFAmqpInput" +) + +// NewInputCEFAMQPAttrs is the constructor of InputCEFAMQPAttrs. +func NewInputCEFAMQPAttrs() InputAttrs { + return &InputCEFAMQPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputCEFAMQPAttrs) InputType() string { + return InputTypeCEFAMQP +} + +// InputCEFAMQPAttrs represents CEF AMQP Input's attributes. +type InputCEFAMQPAttrs struct { + Exchange string `json:"exchange,omitempty"` + Timezone string `json:"timezone,omitempty"` + BrokerPassword string `json:"broker_password,omitempty"` + Locale string `json:"locale,omitempty"` + BrokerHostname string `json:"broker_hostname,omitempty"` + Queue string `json:"queue,omitempty"` + BrokerVHost string `json:"broker_vhost,omitempty"` + BrokerUsername string `json:"broker_username,omitempty"` + RoutingKey string `json:"routing_key,omitempty"` + Heartbeat int `json:"heartbeat,omitempty"` + ParallelQueues int `json:"parallel_queues,omitempty"` + Prefetch int `json:"prefetch,omitempty"` + BrokerPort int `json:"broker_port,omitempty"` + ExchangeBind bool `json:"exchange_bind,omitempty"` + RequeueInvalidMessages bool `json:"requeue_invalid_messages,omitempty"` + UseFullNames bool `json:"use_full_names,omitempty"` + TLS bool `json:"tls,omitempty"` + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` +} diff --git a/input_cef_kafka.go b/input_cef_kafka.go new file mode 100644 index 00000000..193bbfad --- /dev/null +++ b/input_cef_kafka.go @@ -0,0 +1,29 @@ +package graylog + +const ( + InputTypeCEFKafka string = "org.graylog.plugins.cef.input.CEFKafkaInput" +) + +// NewInputCEFKafkaAttrs is the constructor of InputCEFKafkaAttrs. +func NewInputCEFKafkaAttrs() InputAttrs { + return &InputCEFKafkaAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputCEFKafkaAttrs) InputType() string { + return InputTypeCEFKafka +} + +// InputCEFKafkaAttrs represents CEF Kafka Input's attributes. +type InputCEFKafkaAttrs struct { + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` + UseFullNames bool `json:"use_full_names,omitempty"` + Locale string `json:"locale,omitempty"` + Zookeeper string `json:"zookeeper,omitempty"` + Timezone string `json:"timezone,omitempty"` + TopicFilter string `json:"topic_filter,omitempty"` + OffsetReset string `json:"offset_reset,omitempty"` + Threads int `json:"threads,omitempty"` + FetchWaitMax int `json:"fetch_wait_max,omitempty"` + FetchMinBytes int `json:"fetch_min_bytes,omitempty"` +} diff --git a/input_cef_tcp.go b/input_cef_tcp.go new file mode 100644 index 00000000..314d0f03 --- /dev/null +++ b/input_cef_tcp.go @@ -0,0 +1,34 @@ +package graylog + +const ( + InputTypeCEFTCP string = "org.graylog.plugins.cef.input.CEFTCPInput" +) + +// NewInputCEFTCPAttrs is the constructor of InputCEFTCPAttrs. +func NewInputCEFTCPAttrs() InputAttrs { + return &InputCEFTCPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputCEFTCPAttrs) InputType() string { + return InputTypeCEFTCP +} + +// InputCEFTCPAttrs represents CEF TCP Input's attributes. +type InputCEFTCPAttrs struct { + UseNullDelimiter bool `json:"use_null_delimiter,omitempty"` + UseFullNames bool `json:"use_full_names,omitempty"` + TLSEnable bool `json:"tls_enable,omitempty"` + TCPKeepAlive bool `json:"tcp_keepalive,omitempty"` + MaxMessageSize int `json:"max_message_size,omitempty"` + Port int `json:"port,omitempty" v-create:"required" v-update:"required"` + RecvBufferSize int `json:"recv_buffer_size,omitempty" v-create:"required" v-update:"required"` + Timezone string `json:"timezone,omitempty"` + Locale string `json:"locale,omitempty"` + BindAddress string `json:"bind_address,omitempty" v-create:"required" v-update:"required"` + TLSKeyFile string `json:"tls_key_file,omitempty"` + TLSClientAuth string `json:"tls_client_auth,omitempty"` + TLSKeyPassword string `json:"tls_key_password,omitempty"` + TLSClientAuthCertFile string `json:"tls_client_auth_cert_file,omitempty"` + TLSCertFile string `json:"tls_cert_file,omitempty"` +} diff --git a/input_cef_udp.go b/input_cef_udp.go new file mode 100644 index 00000000..64841ee1 --- /dev/null +++ b/input_cef_udp.go @@ -0,0 +1,25 @@ +package graylog + +const ( + InputTypeCEFUDP string = "org.graylog.plugins.cef.input.CEFUDPInput" +) + +// NewInputCEFUDPAttrs is the constructor of InputCEFUDPAttrs. +func NewInputCEFUDPAttrs() InputAttrs { + return &InputCEFUDPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputCEFUDPAttrs) InputType() string { + return InputTypeCEFUDP +} + +// InputCEFUDPAttrs represents CEF UDP Input's attributes. +type InputCEFUDPAttrs struct { + Locale string `json:"locale,omitempty"` + UseFullNames bool `json:"use_full_names,omitempty"` + Timezone string `json:"timezone,omitempty"` + BindAddress string `json:"bind_address,omitempty" v-create:"required" v-update:"required"` + Port int `json:"port,omitempty" v-create:"required" v-update:"required"` + RecvBufferSize int `json:"recv_buffer_size,omitempty" v-create:"required" v-update:"required"` +} diff --git a/input_cloud_trail.go b/input_cloud_trail.go new file mode 100644 index 00000000..9c7894bc --- /dev/null +++ b/input_cloud_trail.go @@ -0,0 +1,28 @@ +package graylog + +const ( + InputTypeAWSCloudTrail string = "org.graylog.aws.inputs.cloudtrail.CloudTrailInput" +) + +// NewInputAWSCloudTrailAttrs is the constructor of InputAWSCloudTrailAttrs. +func NewInputAWSCloudTrailAttrs() InputAttrs { + return &InputAWSCloudTrailAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputAWSCloudTrailAttrs) InputType() string { + return InputTypeAWSCloudTrail +} + +// InputCloudTrailAttrs represents aws cloud trail Input's attributes. +type InputAWSCloudTrailAttrs struct { + CreatorUserID string `json:"creator_user_id,omitempty" v-create:"isdefault"` + AWSAssumeRoleArn string `json:"aws_assume_role_arn,omitempty"` + AWSAccessKey string `json:"aws_access_key,omitempty"` + AWSSecretKey string `json:"aws_secret_key,omitempty"` + AWSSQSRegion string `json:"aws_sqs_region,omitempty"` + AWSSQSQueueName string `json:"aws_sqs_queue_name,omitempty"` + AWSS3Region string `json:"aws_s3_region,omitempty"` + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` + OverrideSource string `json:"override_source,omitempty"` +} diff --git a/input_data.go b/input_data.go new file mode 100644 index 00000000..37400d86 --- /dev/null +++ b/input_data.go @@ -0,0 +1,78 @@ +package graylog + +import ( + "fmt" + + "github.com/suzuki-shunsuke/go-graylog/util" +) + +// InputUpdateParamsData represents InputUpdateParams's data. +// This is used for data conversion of InputUpdateParams. +// ex. json.Unmarshal +type InputUpdateParamsData struct { + ID string `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Type string `json:"type,omitempty"` + Node string `json:"node,omitempty"` + Global *bool `json:"global,omitempty"` + Attrs map[string]interface{} `json:"attributes,omitempty"` +} + +// InputData represents data of Input. +// This is used for data conversion of Input. +// ex. json.Unmarshal +type InputData struct { + Title string `json:"title,omitempty"` + Type string `json:"type,omitempty"` + ID string `json:"id,omitempty"` + Node string `json:"node,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + CreatorUserID string `json:"creator_user_id,omitempty"` + Global bool `json:"global,omitempty"` + Attrs map[string]interface{} `json:"attributes,omitempty"` +} + +// ToInputUpdateParams copies InputUpdateParamsData's data to InputUpdateParams. +func (d *InputUpdateParamsData) ToInputUpdateParams(input *InputUpdateParams) error { + input.Title = d.Title + input.Type = d.Type + input.ID = d.ID + input.Global = d.Global + input.Node = d.Node + attrs := NewInputAttrsByType(input.Type) + if _, ok := attrs.(*InputUnknownAttrs); ok { + input.Attrs = &InputUnknownAttrs{inputType: input.Type, Data: d.Attrs} + return nil + } + if err := util.MSDecode(d.Attrs, attrs); err != nil { + return err + } + input.Attrs = attrs + return nil +} + +// ToInput copies InputData's data to Input. +func (d *InputData) ToInput(input *Input) error { + if input.Type() != "" && input.Type() != d.Type { + return fmt.Errorf("input type is different") + } + if input.Attrs != nil && input.Attrs.InputType() != d.Type { + return fmt.Errorf("input type is different") + } + input.Title = d.Title + input.ID = d.ID + input.Global = d.Global + input.Node = d.Node + input.CreatedAt = d.CreatedAt + input.CreatorUserID = d.CreatorUserID + attrs := NewInputAttrsByType(d.Type) + if _, ok := attrs.(*InputUnknownAttrs); ok { + input.Attrs = &InputUnknownAttrs{inputType: input.Type(), Data: d.Attrs} + return nil + } + if err := util.MSDecode(d.Attrs, attrs); err != nil { + return err + } + input.Attrs = attrs + return nil +} diff --git a/input_data_test.go b/input_data_test.go new file mode 100644 index 00000000..6dca4875 --- /dev/null +++ b/input_data_test.go @@ -0,0 +1,44 @@ +package graylog_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog" +) + +func TestInputUpdatePramsDataToInputUpdateParams(t *testing.T) { + data := &graylog.InputUpdateParamsData{} + prms := &graylog.InputUpdateParams{} + if err := data.ToInputUpdateParams(prms); err != nil { + t.Fatal(err) + } + data = &graylog.InputUpdateParamsData{ + Type: graylog.InputTypeBeats, + } + if err := data.ToInputUpdateParams(prms); err != nil { + t.Fatal(err) + } +} + +func TestInputDataToInput(t *testing.T) { + input := &graylog.Input{} + data := &graylog.InputData{ + Type: "hoge", + Attrs: map[string]interface{}{ + "bind_address": "0.0.0.0", + }} + // if err := data.ToInput(input); err != nil { + // t.Fatal(err) + // } + data.Type = graylog.InputTypeBeats + if err := data.ToInput(input); err != nil { + t.Fatal(err) + } + attrs, ok := input.Attrs.(*graylog.InputBeatsAttrs) + if !ok { + t.Fatal("attrs must be beats attrs") + } + if attrs.BindAddress != "0.0.0.0" { + t.Fatalf(`bind_address = "%s", wanted "%s"`, attrs.BindAddress, "0.0.0.0") + } +} diff --git a/input_fake_http_message.go b/input_fake_http_message.go new file mode 100644 index 00000000..d32f24c8 --- /dev/null +++ b/input_fake_http_message.go @@ -0,0 +1,24 @@ +package graylog + +const ( + InputTypeFakeHTTPMessage string = "org.graylog2.inputs.random.FakeHttpMessageInput" +) + +// NewInputFakeHTTPMessageAttrs is the constructor of InputFakeHTTPMessageAttrs. +func NewInputFakeHTTPMessageAttrs() InputAttrs { + return &InputFakeHTTPMessageAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputFakeHTTPMessageAttrs) InputType() string { + return InputTypeFakeHTTPMessage +} + +// InputFakeHTTPMessageAttrs represents fake HTTP message Input's attributes. +type InputFakeHTTPMessageAttrs struct { + Sleep int `json:"sleep,omitempty"` + SleepDeviation int `json:"sleep_deviation,omitempty"` + Source string `json:"source,omitempty"` + OverrideSource string `json:"override_source,omitempty"` + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` +} diff --git a/input_gelf_amqp.go b/input_gelf_amqp.go new file mode 100644 index 00000000..75a70eb0 --- /dev/null +++ b/input_gelf_amqp.go @@ -0,0 +1,36 @@ +package graylog + +const ( + InputTypeGELFAMQP string = "org.graylog2.inputs.gelf.amqp.GELFAMQPInput" +) + +// NewInputGELFAMQPAttrs is the constructor of InputGELFAMQPAttrs. +func NewInputGELFAMQPAttrs() InputAttrs { + return &InputGELFAMQPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputGELFAMQPAttrs) InputType() string { + return InputTypeGELFAMQP +} + +// InputGELFAMQPAttrs represents GELF AMQP Input's attributes. +type InputGELFAMQPAttrs struct { + ExchangeBind bool `json:"exchange_bind,omitempty"` + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` + TLS bool `json:"tls,omitempty"` + RequeueInvalidMessages bool `json:"requeue_invalid_messages,omitempty"` + BrokerVHost string `json:"broker_vhost,omitempty"` + BrokerUsername string `json:"broker_username,omitempty"` + Queue string `json:"queue,omitempty"` + RoutingKey string `json:"routing_key,omitempty"` + OverrideSource string `json:"override_source,omitempty"` + BrokerHostname string `json:"broker_hostname,omitempty"` + Exchange string `json:"exchange,omitempty"` + BrokerPassword string `json:"broker_password,omitempty"` + Prefetch int `json:"prefetch,omitempty"` + Heartbeat int `json:"heartbeat,omitempty"` + DecompressSizeLimit int `json:"decompress_size_limit,omitempty"` + BrokerPort int `json:"broker_port,omitempty"` + ParallelQueues int `json:"parallel_queues,omitempty"` +} diff --git a/input_gelf_http.go b/input_gelf_http.go new file mode 100644 index 00000000..4ac48ea7 --- /dev/null +++ b/input_gelf_http.go @@ -0,0 +1,34 @@ +package graylog + +const ( + InputTypeGELFHTTP string = "org.graylog2.inputs.gelf.http.GELFHttpInput" +) + +// NewInputGELFHTTPAttrs is the constructor of InputGELFHTTPAttrs. +func NewInputGELFHTTPAttrs() InputAttrs { + return &InputGELFHTTPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputGELFHTTPAttrs) InputType() string { + return InputTypeGELFHTTP +} + +// InputGELFHTTPAttrs represents GELF HTTP Input's attributes. +type InputGELFHTTPAttrs struct { + IdleWriterTimeOut int `json:"idle_writer_timeout,omitempty"` + RecvBufferSize int `json:"recv_buffer_size,omitempty" v-create:"required" v-update:"required"` + MaxChunkSize int `json:"max_chunk_size,omitempty"` + Port int `json:"port,omitempty" v-create:"required" v-update:"required"` + DecompressSizeLimit int `json:"decompress_size_limit,omitempty"` + TLSClientAuthCertFile string `json:"tls_client_auth_cert_file,omitempty"` + BindAddress string `json:"bind_address,omitempty" v-create:"required" v-update:"required"` + TLSCertFile string `json:"tls_cert_file,omitempty"` + TLSKeyFile string `json:"tls_key_file,omitempty"` + TLSKeyPassword string `json:"tls_key_password,omitempty"` + TLSClientAuth string `json:"tls_client_auth,omitempty"` + OverrideSource string `json:"override_source,omitempty"` + TCPKeepAlive bool `json:"tcp_keepalive,omitempty"` + EnableCORS bool `json:"enable_cors,omitempty"` + TLSEnable bool `json:"tls_enable,omitempty"` +} diff --git a/input_gelf_kafka.go b/input_gelf_kafka.go new file mode 100644 index 00000000..c8a099d0 --- /dev/null +++ b/input_gelf_kafka.go @@ -0,0 +1,28 @@ +package graylog + +const ( + InputTypeGELFKafka string = "org.graylog2.inputs.gelf.kafka.GELFKafkaInput" +) + +// NewInputGELFKafkaAttrs is the constructor of InputGELFKafkaAttrs. +func NewInputGELFKafkaAttrs() InputAttrs { + return &InputGELFKafkaAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputGELFKafkaAttrs) InputType() string { + return InputTypeGELFKafka +} + +// InputGELFKafkaAttrs represents GELF Kafka Input's attributes. +type InputGELFKafkaAttrs struct { + OverrideSource string `json:"override_source,omitempty"` + DecompressSizeLimit int `json:"decompress_size_limit,omitempty"` + TopicFilter string `json:"topic_filter,omitempty"` + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` + FetchWaitMax int `json:"fetch_wait_max,omitempty"` + FetchMinBytes int `json:"fetch_min_bytes,omitempty"` + OffsetReset string `json:"offset_reset,omitempty"` + Threads int `json:"threads,omitempty"` + Zookeeper string `json:"zookeeper,omitempty"` +} diff --git a/input_gelf_tcp.go b/input_gelf_tcp.go new file mode 100644 index 00000000..5d2d7921 --- /dev/null +++ b/input_gelf_tcp.go @@ -0,0 +1,33 @@ +package graylog + +const ( + InputTypeGELFTCP string = "org.graylog2.inputs.gelf.tcp.GELFTCPInput" +) + +// NewInputGELFTCPAttrs is the constructor of InputGELFTCPAttrs. +func NewInputGELFTCPAttrs() InputAttrs { + return &InputGELFTCPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputGELFTCPAttrs) InputType() string { + return InputTypeGELFTCP +} + +// InputGELFTCPAttrs represents GELF TCP Input's attributes. +type InputGELFTCPAttrs struct { + MaxMessageSize int `json:"max_message_size,omitempty"` + DecompressSizeLimit int `json:"decompress_size_limit,omitempty"` + Port int `json:"port,omitempty" v-create:"required" v-update:"required"` + RecvBufferSize int `json:"recv_buffer_size,omitempty" v-create:"required" v-update:"required"` + BindAddress string `json:"bind_address,omitempty" v-create:"required" v-update:"required"` + OverrideSource string `json:"override_source,omitempty"` + TLSKeyFile string `json:"tls_key_file,omitempty"` + TLSKeyPassword string `json:"tls_key_password,omitempty"` + TLSClientAuthCertFile string `json:"tls_client_auth_cert_file,omitempty"` + TLSClientAuth string `json:"tls_client_auth,omitempty"` + TLSCertFile string `json:"tls_cert_file,omitempty"` + UseNullDelimiter bool `json:"use_null_delimiter,omitempty"` + TLSEnable bool `json:"tls_enable,omitempty"` + TCPKeepAlive bool `json:"tcp_keepalive,omitempty"` +} diff --git a/input_gelf_udp.go b/input_gelf_udp.go new file mode 100644 index 00000000..33dbd056 --- /dev/null +++ b/input_gelf_udp.go @@ -0,0 +1,24 @@ +package graylog + +const ( + InputTypeGELFUDP string = "org.graylog2.inputs.gelf.udp.GELFUDPInput" +) + +// NewInputGELFUDPAttrs is the constructor of InputGELFUDPAttrs. +func NewInputGELFUDPAttrs() InputAttrs { + return &InputGELFUDPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputGELFUDPAttrs) InputType() string { + return InputTypeGELFUDP +} + +// InputGELFUDPAttrs represents GELF UDP Input's attributes. +type InputGELFUDPAttrs struct { + DecompressSizeLimit int `json:"decompress_size_limit,omitempty"` + OverrideSource string `json:"override_source,omitempty"` + BindAddress string `json:"bind_address,omitempty" v-create:"required" v-update:"required"` + Port int `json:"port,omitempty" v-create:"required" v-update:"required"` + RecvBufferSize int `json:"recv_buffer_size,omitempty" v-create:"required" v-update:"required"` +} diff --git a/input_json_path.go b/input_json_path.go new file mode 100644 index 00000000..451136a8 --- /dev/null +++ b/input_json_path.go @@ -0,0 +1,27 @@ +package graylog + +const ( + InputTypeJSONPath string = "org.graylog2.inputs.misc.jsonpath.JsonPathInput" +) + +// NewInputJSONPathAttrs is the constructor of InputJSONPathAttrs. +func NewInputJSONPathAttrs() InputAttrs { + return &InputJSONPathAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputJSONPathAttrs) InputType() string { + return InputTypeJSONPath +} + +// InputJSONPathAttrs represents JSON path Input's attributes. +type InputJSONPathAttrs struct { + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` + OverrideSource string `json:"override_source,omitempty"` + Headers string `json:"headers,omitempty"` + Path string `json:"path,omitempty"` + TargetURL string `json:"target_url,omitempty"` + Interval int `json:"interval,omitempty"` + Source string `json:"source,omitempty"` + Timeunit string `json:"timeunit,omitempty"` +} diff --git a/input_net_flow_udp.go b/input_net_flow_udp.go new file mode 100644 index 00000000..493f9d2e --- /dev/null +++ b/input_net_flow_udp.go @@ -0,0 +1,24 @@ +package graylog + +const ( + InputTypeNetFlowUDP string = "org.graylog.plugins.netflow.inputs.NetFlowUdpInput" +) + +// NewInputNetFlowUDPAttrs is the constructor of InputNetFlowUDPAttrs. +func NewInputNetFlowUDPAttrs() InputAttrs { + return &InputNetFlowUDPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputNetFlowUDPAttrs) InputType() string { + return InputTypeNetFlowUDP +} + +// InputNetFlowUDPAttrs represents net flow UDP Input's attributes. +type InputNetFlowUDPAttrs struct { + NetFlow9DefinitionsPath string `json:"netflow9_definitions_path,omitempty"` + OverrideSource string `json:"override_source,omitempty"` + BindAddress string `json:"bind_address,omitempty" v-create:"required" v-update:"required"` + Port int `json:"port,omitempty" v-create:"required" v-update:"required"` + RecvBufferSize int `json:"recv_buffer_size,omitempty" v-create:"required" v-update:"required"` +} diff --git a/input_raw_amqp.go b/input_raw_amqp.go new file mode 100644 index 00000000..f3166e8d --- /dev/null +++ b/input_raw_amqp.go @@ -0,0 +1,35 @@ +package graylog + +const ( + InputTypeRawAMQP string = "org.graylog2.inputs.raw.amqp.RawAMQPInput" +) + +// NewInputRawAMQPAttrs is the constructor of InputRawAMQPAttrs. +func NewInputRawAMQPAttrs() InputAttrs { + return &InputRawAMQPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputRawAMQPAttrs) InputType() string { + return InputTypeRawAMQP +} + +// InputRawAMQPAttrs represents raw AMQP Input's attributes. +type InputRawAMQPAttrs struct { + ParallelQueues int `json:"parallel_queues,omitempty"` + HeartBeat int `json:"heartbeat,omitempty"` + BrokerPort int `json:"broker_port,omitempty"` + Prefetch int `json:"prefetch,omitempty"` + RequeueInvalidMessages bool `json:"requeue_invalid_messages,omitempty"` + TLS bool `json:"tls,omitempty"` + ExchangeBind bool `json:"exchange_bind,omitempty"` + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` + Exchange string `json:"exchange,omitempty"` + RoutingKey string `json:"routing_key,omitempty"` + BrokerHostname string `json:"broker_hostname,omitempty"` + Queue string `json:"queue,omitempty"` + BrokerPassword string `json:"broker_password,omitempty"` + BrokerVHost string `json:"broker_vhost,omitempty"` + BrokerUsername string `json:"broker_username,omitempty"` + OverrideSource string `json:"override_source,omitempty"` +} diff --git a/input_syslog_amqp.go b/input_syslog_amqp.go new file mode 100644 index 00000000..45ee16cd --- /dev/null +++ b/input_syslog_amqp.go @@ -0,0 +1,39 @@ +package graylog + +const ( + InputTypeSyslogAMQP string = "org.graylog2.inputs.syslog.amqp.SyslogAMQPInput" +) + +// NewInputSyslogAMQPAttrs is the constructor of InputSyslogAMQPAttrs. +func NewInputSyslogAMQPAttrs() InputAttrs { + return &InputSyslogAMQPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputSyslogAMQPAttrs) InputType() string { + return InputTypeSyslogAMQP +} + +// InputSyslogAMQPAttrs represents SyslogAMQP Input's attributes. +type InputSyslogAMQPAttrs struct { + Heartbeat int `json:"heartbeat,omitempty"` + Prefetch int `json:"prefetch,omitempty"` + BrokerPort int `json:"broker_port,omitempty"` + ParallelQueues int `json:"parallel_queues,omitempty"` + BrokerVHost string `json:"broker_vhost,omitempty"` + BrokerUsername string `json:"broker_username,omitempty"` + BrokerPassword string `json:"broker_password,omitempty"` + Exchange string `json:"exchange,omitempty"` + OverrideSource string `json:"override_source,omitempty"` + RoutingKey string `json:"routing_key,omitempty"` + BrokerHostname string `json:"broker_hostname,omitempty"` + Queue string `json:"queue,omitempty"` + ExchangeBind bool `json:"exchange_bind,omitempty"` + ForceRDNS bool `json:"force_rdns,omitempty"` + StoreFullMessage bool `json:"store_full_message,omitempty"` + ExpandStructuredData bool `json:"expand_structured_data,omitempty"` + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` + TLS bool `json:"tls,omitempty"` + AllowOverrideDate bool `json:"allow_override_date,omitempty"` + RequeueInvalidMessages bool `json:"requeue_invalid_messages,omitempty"` +} diff --git a/input_syslog_kafka.go b/input_syslog_kafka.go new file mode 100644 index 00000000..4a43cbe6 --- /dev/null +++ b/input_syslog_kafka.go @@ -0,0 +1,31 @@ +package graylog + +const ( + InputTypeSyslogKafka string = "org.graylog2.inputs.syslog.kafka.SyslogKafkaInput" +) + +// NewInputSyslogKafkaAttrs is the constructor of InputSyslogKafkaAttrs. +func NewInputSyslogKafkaAttrs() InputAttrs { + return &InputSyslogKafkaAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputSyslogKafkaAttrs) InputType() string { + return InputTypeSyslogKafka +} + +// InputSyslogKafkaAttrs represents SyslogKafka Input's attributes. +type InputSyslogKafkaAttrs struct { + ForceRDNS bool `json:"force_rdns,omitempty"` + StoreFullMessage bool `json:"store_full_message,omitempty"` + ExpandStructuredData bool `json:"expand_structured_data,omitempty"` + AllowOverrideDate bool `json:"allow_override_date,omitempty"` + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` + OverrideSource string `json:"override_source,omitempty"` + TopicFilter string `json:"topic_filter,omitempty"` + FetchWaitMax int `json:"fetch_wait_max,omitempty"` + OffsetReset string `json:"offset_reset,omitempty"` + Zookeeper string `json:"zookeeper,omitempty"` + FetchMinBytes int `json:"fetch_min_bytes,omitempty"` + Threads int `json:"threads,omitempty"` +} diff --git a/input_syslog_tcp.go b/input_syslog_tcp.go new file mode 100644 index 00000000..b7cbcf75 --- /dev/null +++ b/input_syslog_tcp.go @@ -0,0 +1,22 @@ +package graylog + +const ( + InputTypeSyslogTCP string = "org.graylog2.inputs.syslog.tcp.SyslogTCPInput" +) + +// NewInputSyslogTCPAttrs is the constructor of InputSyslogTCPAttrs. +func NewInputSyslogTCPAttrs() InputAttrs { + return &InputSyslogTCPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputSyslogTCPAttrs) InputType() string { + return InputTypeSyslogTCP +} + +// InputSyslogTCPAttrs represents SyslogTCP Input's attributes. +type InputSyslogTCPAttrs struct { + Port int `json:"port,omitempty" v-create:"required" v-update:"required"` + BindAddress string `json:"bind_address,omitempty" v-create:"required" v-update:"required"` + RecvBufferSize int `json:"recv_buffer_size,omitempty" v-create:"required" v-update:"required"` +} diff --git a/input_syslog_udp.go b/input_syslog_udp.go new file mode 100644 index 00000000..c7eee634 --- /dev/null +++ b/input_syslog_udp.go @@ -0,0 +1,35 @@ +package graylog + +const ( + InputTypeSyslogUDP string = "org.graylog2.inputs.syslog.udp.SyslogUDPInput" +) + +// NewInputSyslogUDPAttrs is the constructor of InputSyslogUDPAttrs. +func NewInputSyslogUDPAttrs() InputAttrs { + return &InputSyslogUDPAttrs{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputSyslogUDPAttrs) InputType() string { + return InputTypeSyslogUDP +} + +// InputSyslogUDPAttrs represents SyslogUDP Input's attributes. +type InputSyslogUDPAttrs struct { + BindAddress string `json:"bind_address,omitempty" v-create:"required" v-update:"required"` + Port int `json:"port,omitempty" v-create:"required" v-update:"required"` + RecvBufferSize int `json:"recv_buffer_size,omitempty" v-create:"required" v-update:"required"` + TCPKeepAlive bool `json:"tcp_keepalive,omitempty"` + TLSEnable bool `json:"tls_enable,omitempty"` + ThrottlingAllowed bool `json:"throttling_allowed,omitempty"` + EnableCORS bool `json:"enable_cors,omitempty"` + UseNullDelimiter bool `json:"use_null_delimiter,omitempty"` + ExchangeBind bool `json:"exchange_bind,omitempty"` + ForceRDNS bool `json:"force_rdns,omitempty"` + StoreFullMessage bool `json:"store_full_message,omitempty"` + ExpandStructuredData bool `json:"expand_structured_data,omitempty"` + AllowOverrideDate bool `json:"allow_override_date,omitempty"` + RequeueInvalidMessages bool `json:"requeue_invalid_messages,omitempty"` + UseFullNames bool `json:"use_full_names,omitempty"` + TLS bool `json:"tls,omitempty"` +} diff --git a/input_test.go b/input_test.go new file mode 100644 index 00000000..ebd6b66d --- /dev/null +++ b/input_test.go @@ -0,0 +1,32 @@ +package graylog_test + +import ( + "encoding/json" + "testing" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestInputUnmarshalJSON(t *testing.T) { + input := testutil.Input() + attrs := input.Attrs.(*graylog.InputBeatsAttrs) + if attrs.BindAddress == "" { + t.Fatal(`attrs.BindAddress == ""`) + } + if err := json.Unmarshal([]byte(`{"id": "fooo"}`), input); err != nil { + t.Fatal(err) + } + attrs = input.Attrs.(*graylog.InputBeatsAttrs) + if attrs.BindAddress == "" { + t.Fatal(`attrs.BindAddress == ""`) + } +} + +func TestInputNewUpdateParams(t *testing.T) { + input := testutil.Input() + prms := input.NewUpdateParams() + if input.ID != prms.ID { + t.Fatalf(`prms.ID = "%s", wanted "%s"`, prms.ID, input.ID) + } +} diff --git a/input_unknown_attrs.go b/input_unknown_attrs.go new file mode 100644 index 00000000..be9b8ee2 --- /dev/null +++ b/input_unknown_attrs.go @@ -0,0 +1,12 @@ +package graylog + +// InputUnknownAttrs represents unknown type's Input Attrs. +type InputUnknownAttrs struct { + inputType string + Data map[string]interface{} +} + +// InputType is the implementation of the InputAttrs interface. +func (attrs InputUnknownAttrs) InputType() string { + return attrs.inputType +} diff --git a/lint.sh b/lint.sh new file mode 100644 index 00000000..dc386160 --- /dev/null +++ b/lint.sh @@ -0,0 +1,6 @@ +echo "+ gometalinter" +npm run metalint || exit 1 +echo "+ golint" +golint ./mockserver ./client/... ./terraform/... ./mockserver/logic ./mockserver/store ./mockserver/store/plain ./mockserver/handler ./validator || exit 1 +echo "+ staticcheck (failure is ignored)" +staticcheck ./... || echo "staticcheck failure is ignored" diff --git a/mockserver/README.md b/mockserver/README.md new file mode 100644 index 00000000..6e03b866 --- /dev/null +++ b/mockserver/README.md @@ -0,0 +1,13 @@ +# go-graylog mockserver + +[![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/suzuki-shunsuke/go-graylog/mockserver) +[![Build Status](https://travis-ci.org/suzuki-shunsuke/go-graylog.svg?branch=master)](https://travis-ci.org/suzuki-shunsuke/go-graylog) +[![codecov](https://codecov.io/gh/suzuki-shunsuke/go-graylog/branch/master/graph/badge.svg)](https://codecov.io/gh/suzuki-shunsuke/go-graylog) +[![Go Report Card](https://goreportcard.com/badge/github.com/suzuki-shunsuke/go-graylog)](https://goreportcard.com/report/github.com/suzuki-shunsuke/go-graylog) +[![GitHub last commit](https://img.shields.io/github/last-commit/suzuki-shunsuke/go-graylog.svg)](https://github.com/suzuki-shunsuke/go-graylog) +[![GitHub tag](https://img.shields.io/github/tag/suzuki-shunsuke/go-graylog.svg)](https://github.com/suzuki-shunsuke/go-graylog/releases) +[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/suzuki-shunsuke/go-graylog/master/LICENSE) + +[Graylog](https://www.graylog.org/) API mockserver for Golang. + +See https://github.com/suzuki-shunsuke/go-graylog diff --git a/mockserver/doc.go b/mockserver/doc.go new file mode 100644 index 00000000..a8512d81 --- /dev/null +++ b/mockserver/doc.go @@ -0,0 +1,4 @@ +/* +Package mockserver provides Graylog API mock server. +*/ +package mockserver diff --git a/mockserver/exec/main.go b/mockserver/exec/main.go new file mode 100644 index 00000000..86a665d7 --- /dev/null +++ b/mockserver/exec/main.go @@ -0,0 +1,118 @@ +// Run Graylog mock server. +// +// Usage +// $ graylog-mock-server [--port ] [--log-level debug|info|warn|error|fatal|panic] [--data ] +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog/mockserver" + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" +) + +const version = "0.1.0" + +var help string + +func init() { + help = fmt.Sprintf(` +graylog-mock-server - Run Graylog mock server. + +USAGE: + graylog-mock-server [options] + +VERSION: + %s + +OPTIONS: + --port value port number. If you don't set this option, a free port is assigned and the assigned port number is output to the console when the mock server runs. + --log-level value the log level of logrus which the mock server uses internally. (default: "info") + --data value data file path. When the server runs data of the file is loaded and when data of the server is changed data is saved at the file. If this option is not set, no data is loaded and saved. + --help, -h show help + --version, -v print the version +`, version) +} + +func action(dataPath, logLevel string, port int) error { + var ( + server *mockserver.Server + err error + ) + if port == 0 { + server, err = mockserver.NewServer( + "", plain.NewStore(dataPath)) + } else { + server, err = mockserver.NewServer( + fmt.Sprintf(":%d", port), plain.NewStore(dataPath)) + } + if err != nil { + return errors.Wrap(err, "failed to create a mock server") + } + lvl, err := log.ParseLevel(logLevel) + if err != nil { + return fmt.Errorf( + `invalid log-level %s. +log-level must be any of debug|info|warn|error|fatal|panic`, logLevel) + } + + server.Logger().SetLevel(lvl) + if err := server.Load(); err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to load data at %s", dataPath)) + } + server.Start() + defer server.Close() + server.Logger().Infof( + "Start Graylog mock server: %s\nCtrl + C to stop server", server.Endpoint()) + signalChan := make(chan os.Signal, 1) + signal.Notify( + signalChan, syscall.SIGHUP, syscall.SIGINT, + syscall.SIGTERM, syscall.SIGQUIT) + exitChan := make(chan int) + go func() { + for { + s := <-signalChan + switch s { + default: + exitChan <- 0 + } + } + }() + + <-exitChan + return nil +} + +func main() { + var portFlag = flag.Int( + "port", 0, + "port number. If you don't set this option, a free port is assigned and the assigned port number is output to the console when the mock server runs.") + var dataFlag = flag.String( + "data", "", + "data file path. When the server runs data of the file is loaded and when data of the server is changed data is saved at the file. If this option is not set, no data is loaded and saved.") + var logLevelFlag = flag.String( + "log-level", "info", + `the log level of logrus which the mock server uses internally. (default: "info")`) + var helpFlag = flag.Bool("help", false, "Show help.") + var versionFlag = flag.Bool("version", false, "Print the version.") + flag.Parse() + + if *helpFlag { + fmt.Println(help) + return + } + if *versionFlag { + fmt.Println(version) + return + } + + if err := action(*dataFlag, *logLevelFlag, *portFlag); err != nil { + log.Fatal(err) + } +} diff --git a/mockserver/handler/doc.go b/mockserver/handler/doc.go new file mode 100644 index 00000000..300693f2 --- /dev/null +++ b/mockserver/handler/doc.go @@ -0,0 +1,5 @@ +/* +Package handler provides handlers of Graylog API mock server. +Basically enduser does not use the package directly. +*/ +package handler diff --git a/mockserver/handler/error.go b/mockserver/handler/error.go new file mode 100644 index 00000000..0ad18dd6 --- /dev/null +++ b/mockserver/handler/error.go @@ -0,0 +1,12 @@ +package handler + +// APIError represents a Graylog API's error response body. +type APIError struct { + Type string `json:"type"` + Message string `json:"message"` +} + +// NewAPIError returns a new APIError. +func NewAPIError(msg string) *APIError { + return &APIError{Type: "ApiError", Message: msg} +} diff --git a/mockserver/handler/error_test.go b/mockserver/handler/error_test.go new file mode 100644 index 00000000..898dc11f --- /dev/null +++ b/mockserver/handler/error_test.go @@ -0,0 +1,17 @@ +package handler_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/handler" +) + +func TestNewAPIError(t *testing.T) { + e := handler.NewAPIError("test") + if e.Type != "ApiError" { + t.Fatalf(`e.Type = "%s", wanted "ApiError"`, e.Type) + } + if e.Message != "test" { + t.Fatalf(`e.Message = "%s", wanted "test"`, e.Message) + } +} diff --git a/mockserver/handler/handler.go b/mockserver/handler/handler.go new file mode 100644 index 00000000..63c3cd90 --- /dev/null +++ b/mockserver/handler/handler.go @@ -0,0 +1,83 @@ +package handler + +import ( + "encoding/json" + "net/http" + + "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" +) + +// Handler is the graylog REST API's handler. +// the argument `user` is the authenticated user and are mainly used for the authorization. +type Handler func(user *graylog.User, lgc *logic.Logic, w http.ResponseWriter, r *http.Request, ps httprouter.Params) (interface{}, int, error) + +func wrapHandle(lgc *logic.Logic, handler Handler) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + lgc.Logger().WithFields(log.Fields{ + "path": r.URL.Path, "method": r.Method, + }).Info("request start") + w.Header().Set("Content-Type", "application/json") + // authentication + var user *graylog.User + if lgc.Auth() { + authName, authPass, ok := r.BasicAuth() + if !ok { + lgc.Logger().WithFields(log.Fields{ + "path": r.URL.Path, "method": r.Method, + }).Warn("request basic authentication header is not set") + w.WriteHeader(401) + return + } + var ( + sc int + err error + ) + user, sc, err = lgc.Authenticate(authName, authPass) + if err != nil { + w.WriteHeader(sc) + if sc == 401 { + return + } + ae := NewAPIError(err.Error()) + b, err := json.Marshal(ae) + if err != nil { + w.Write([]byte(`{"message":"failed to authenticate"}`)) + return + } + w.Write(b) + return + } + lgc.Logger().WithFields(log.Fields{ + "path": r.URL.Path, "method": r.Method, + "user_name": user.Username, + }).Info("request user name") + } + + body, sc, err := handler(user, lgc, w, r, ps) + if err != nil { + w.WriteHeader(sc) + + ae := NewAPIError(err.Error()) + b, err := json.Marshal(ae) + if err != nil { + w.Write([]byte(`{"message":"failed to marshal an APIError"}`)) + return + } + w.Write(b) + return + } + if body == nil { + return + } + b, err := json.Marshal(body) + if err == nil { + w.Write(b) + return + } + w.WriteHeader(500) + w.Write([]byte(`{"message":"500 Internal Server Error"}`)) + } +} diff --git a/mockserver/handler/index_set.go b/mockserver/handler/index_set.go new file mode 100644 index 00000000..87766f21 --- /dev/null +++ b/mockserver/handler/index_set.go @@ -0,0 +1,238 @@ +package handler + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/util" + "github.com/suzuki-shunsuke/go-set" +) + +// HandleGetIndexSets is the handler of Get Index Sets API. +func HandleGetIndexSets( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, _ httprouter.Params, +) (interface{}, int, error) { + // GET /system/indices/index_sets Get a list of all index sets + skip := 0 + limit := 0 + stats := false + query := r.URL.Query() + s, ok := query["skip"] + var err error + if ok && len(s) > 0 { + skip, err = strconv.Atoi(s[0]) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "param_name": "skip", "value": s[0], + }).Warn("failed to convert string to integer") + // Unfortunately, graylog returns 404 + // https://github.com/Graylog2/graylog2-server/issues/4721 + return nil, 404, fmt.Errorf("HTTP 404 Not Found") + } + } + l, ok := query["limit"] + if ok && len(l) > 0 { + limit, err = strconv.Atoi(l[0]) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "param_name": "limit", "value": l[0], + }).Warn("failed to convert string to integer") + // Unfortunately, graylog returns 404 + // https://github.com/Graylog2/graylog2-server/issues/4721 + return nil, 404, fmt.Errorf("HTTP 404 Not Found") + } + } + st, ok := query["stats"] + if ok && len(st) > 0 { + stats, err = strconv.ParseBool(st[0]) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "param_name": "stats", "value": st[0], + }).Warn("failed to convert string to bool") + // Unfortunately, graylog ignores invalid stats parameter + // TODO send issue + stats = false + } + } + + arr, total, sc, err := lgc.GetIndexSets(skip, limit) + if err != nil { + logic.LogWE(sc, lgc.Logger().WithFields(log.Fields{ + "error": err, "skip": skip, "limit": limit, "status_code": sc, + }), "failed to get index sets") + return arr, sc, err + } + if stats { + stats, sc, err := lgc.GetIndexSetStatsMap() + if err != nil { + return nil, sc, err + } + return &graylog.IndexSetsBody{ + IndexSets: arr, Total: total, Stats: stats}, sc, nil + } + return &graylog.IndexSetsBody{ + IndexSets: arr, Total: total, + Stats: map[string]graylog.IndexSetStats{}, + }, sc, nil +} + +// HandleGetIndexSet is the handler of Get an Index Set API. +func HandleGetIndexSet( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // GET /system/indices/index_sets/{id} Get index set + id := ps.ByName("indexSetID") + if id == "stats" { + return HandleGetTotalIndexSetStats(user, lgc, w, r, ps) + } + if sc, err := lgc.Authorize(user, "indexsets:read", id); err != nil { + return nil, sc, err + } + return lgc.GetIndexSet(id) +} + +// HandleCreateIndexSet is the handler of Create an Index Set API. +func HandleCreateIndexSet( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, _ httprouter.Params, +) (interface{}, int, error) { + // POST /system/indices/index_sets Create index set + if sc, err := lgc.Authorize(user, "indexsets:create"); err != nil { + return nil, sc, err + } + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Required: set.NewStrSet( + "title", "index_prefix", "rotation_strategy_class", "rotation_strategy", + "retention_strategy_class", "retention_strategy", "creation_date", + "index_analyzer", "shards", "index_optimization_max_num_segments"), + Optional: set.NewStrSet("description", "replicas", "index_optimization_disabled", "writable"), + Ignored: set.NewStrSet("default"), + ExtForbidden: true, + }) + if err != nil { + return body, sc, err + } + + is := &graylog.IndexSet{} + if err := util.MSDecode(body, is); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Info("Failed to parse request body as indexSet") + return nil, 400, err + } + + lgc.Logger().WithFields(log.Fields{ + "body": body, "index_set": is, + }).Debug("request body") + if is.ID == "" { + sc, err = lgc.AddIndexSet(is) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return is, sc, nil + } + is, sc, err = lgc.UpdateIndexSet(is.NewUpdateParams()) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return is, sc, nil +} + +// HandleUpdateIndexSet is the handler of Update an Index Set API. +func HandleUpdateIndexSet( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // PUT /system/indices/index_sets/{id} Update index set + id := ps.ByName("indexSetID") + prms := &graylog.IndexSetUpdateParams{} + if sc, err := lgc.Authorize(user, "indexsets:edit", id); err != nil { + return nil, sc, err + } + + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Required: set.NewStrSet( + "title", "index_prefix", "rotation_strategy_class", "rotation_strategy", + "retention_strategy_class", "retention_strategy", + "index_analyzer", "shards", "index_optimization_max_num_segments"), + Optional: set.NewStrSet("description", "replicas", "index_optimization_disabled", "writable"), + Ignored: set.NewStrSet("default", "creation_date"), + }) + if err != nil { + return nil, sc, err + } + + if err := util.MSDecode(body, prms); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Warn("Failed to parse request body as indexSetUpdateParams") + return nil, 400, err + } + prms.ID = id + lgc.Logger().WithFields(log.Fields{ + "body": body, "index_set": prms, + }).Debug("request body") + is, sc, err := lgc.UpdateIndexSet(prms) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return is, sc, nil +} + +// HandleDeleteIndexSet is the handler of Delete an Index Set API. +func HandleDeleteIndexSet( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // DELETE /system/indices/index_sets/{id} Delete index set + id := ps.ByName("indexSetID") + if sc, err := lgc.Authorize(user, "indexsets:delete", id); err != nil { + return nil, sc, err + } + sc, err := lgc.DeleteIndexSet(id) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return nil, sc, nil +} + +// HandleSetDefaultIndexSet is the handler of Set the default Index Set API. +func HandleSetDefaultIndexSet( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // PUT /system/indices/index_sets/{id}/default Set default index set + id := ps.ByName("indexSetID") + if sc, err := lgc.Authorize(user, "indexsets:edit", id); err != nil { + return nil, sc, err + } + is, sc, err := lgc.SetDefaultIndexSet(id) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return is, 200, nil +} diff --git a/mockserver/handler/index_set_stats.go b/mockserver/handler/index_set_stats.go new file mode 100644 index 00000000..4ac3025c --- /dev/null +++ b/mockserver/handler/index_set_stats.go @@ -0,0 +1,30 @@ +package handler + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" +) + +// HandleGetIndexSetStats is the handler of Get Index Set Statistics API. +func HandleGetIndexSetStats( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // GET /system/indices/index_sets/{id}/stats Get index set statistics + // TODO authorization + id := ps.ByName("indexSetID") + return lgc.GetIndexSetStats(id) +} + +// HandleGetTotalIndexSetStats is the handler of Get Index Set Statistics of all Index Sets API. +func HandleGetTotalIndexSetStats( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // GET /system/indices/index_sets/stats Get stats of all index sets + // TODO authorization + return lgc.GetTotalIndexSetStats() +} diff --git a/mockserver/handler/index_set_stats_test.go b/mockserver/handler/index_set_stats_test.go new file mode 100644 index 00000000..e8e07281 --- /dev/null +++ b/mockserver/handler/index_set_stats_test.go @@ -0,0 +1,67 @@ +package handler_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestHandleGetIndexSetStats(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + iss, _, _, _, err := client.GetIndexSets(0, 0, false) + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet("hoge") + if len(iss) == 0 { + if _, err := client.CreateIndexSet(is); err != nil { + t.Fatal(err) + } + // clean + defer func(id string) { + if _, err := client.DeleteIndexSet(id); err != nil { + t.Fatal(err) + } + }(is.ID) + } else { + is = &(iss[0]) + } + + if _, _, err := client.GetIndexSetStats(is.ID); err != nil { + t.Fatal(err) + } + if _, _, err := client.GetIndexSetStats(""); err == nil { + t.Fatal("index set id is required") + } + // if _, _, err := client.GetIndexSetStats("h"); err == nil { + // t.Fatal(`no index set whose id is "h"`) + // } +} + +func TestGetTotalIndexSetsStats(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + is, f, err := testutil.GetIndexSet(client, server, "hoge") + if err != nil { + t.Fatal(err) + } + if f != nil { + defer f(is.ID) + } + if _, _, err := client.GetTotalIndexSetsStats(); err != nil { + t.Fatal(err) + } +} diff --git a/mockserver/handler/index_set_test.go b/mockserver/handler/index_set_test.go new file mode 100644 index 00000000..b89e46ae --- /dev/null +++ b/mockserver/handler/index_set_test.go @@ -0,0 +1,239 @@ +package handler_test + +import ( + "bytes" + "net/http" + "reflect" + "testing" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/testutil" + "github.com/suzuki-shunsuke/go-ptr" +) + +func TestHandleGetIndexSets(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + indexSets, _, _, _, err := client.GetIndexSets(0, 0, false) + if err != nil { + t.Fatal(err) + } + if len(indexSets) == 0 { + t.Fatal("len(indexSets) == 0") + } + // TODO run by nobody +} + +func TestHandleGetIndexSet(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + + is := testutil.IndexSet("hoge") + if _, err := server.AddIndexSet(is); err != nil { + t.Fatal(err) + } + act, _, err := client.GetIndexSet(is.ID) + if err != nil { + t.Fatal("Failed to GetIndexSet", err) + } + if !reflect.DeepEqual(*act, *is) { + t.Fatalf("client.GetIndexSet() == %v, wanted %v", act, is) + } + if _, _, err := client.GetIndexSet(""); err == nil { + t.Fatal("index set id is required") + } + if _, _, err := client.GetIndexSet("h"); err == nil { + t.Fatal(`no index set whose id is "h"`) + } +} + +func TestHandleCreateIndexSet(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + is := testutil.IndexSet("hoge") + if _, err := client.CreateIndexSet(is); err != nil { + t.Fatal(err) + } + if is.ID == "" { + t.Fatal("IndexSet's id is empty") + } + is.ID = "" + if _, err := client.CreateIndexSet(is); err == nil { + t.Fatal("index prefix should conflict") + } + + body := bytes.NewBuffer([]byte("hoge")) + req, err := http.NewRequest( + http.MethodPost, client.Endpoints().IndexSets(), body) + if err != nil { + t.Fatal(err) + } + req.SetBasicAuth(client.Name(), client.Password()) + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 400 { + t.Fatalf("resp.StatusCode == %d, wanted 400", resp.StatusCode) + } +} + +func TestHandleUpdateIndexSet(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + is := testutil.IndexSet("hoge") + if _, err = server.AddIndexSet(is); err != nil { + t.Fatal(err) + } + id := is.ID + prms := is.NewUpdateParams() + if _, _, err := client.UpdateIndexSet(prms); err != nil { + t.Fatal(err) + } + prms.ID = "" + if _, _, err := client.UpdateIndexSet(prms); err == nil { + t.Fatal("index set id is required") + } + prms.ID = "h" + if _, _, err := client.UpdateIndexSet(prms); err == nil { + t.Fatal(`no index set whose id is "h"`) + } + prms.ID = id + title := prms.Title + prms.Title = "" + if _, _, err := client.UpdateIndexSet(prms); err == nil { + t.Fatal("title is required") + } + prms.Title = title + prms.IndexPrefix = "graylog" + if _, _, err := server.UpdateIndexSet(prms); err == nil { + t.Fatal("index prefix should be conflict") + } + if _, _, err := client.UpdateIndexSet(nil); err == nil { + t.Fatal("index set is nil") + } + + body := bytes.NewBuffer([]byte("hoge")) + u, err := client.Endpoints().IndexSet(id) + if err != nil { + t.Fatal(err) + } + req, err := http.NewRequest(http.MethodPut, u.String(), body) + if err != nil { + t.Fatal(err) + } + req.SetBasicAuth(client.Name(), client.Password()) + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 400 { + t.Fatalf("resp.StatusCode == %d, wanted 400", resp.StatusCode) + } +} + +func TestHandleDeleteIndexSet(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + indexSets, _, _, err := server.GetIndexSets(0, 0) + if err != nil { + t.Fatal(err) + } + indexSet := indexSets[0] + if _, err = client.DeleteIndexSet(indexSet.ID); err == nil { + t.Fatal("default index set should not be deleted") + } + is := testutil.IndexSet("hoge") + if _, err := server.AddIndexSet(is); err != nil { + t.Fatal(err) + } + if _, err = client.DeleteIndexSet(is.ID); err != nil { + t.Fatal("Failed to DeleteIndexSet", err) + } + if _, err = client.DeleteIndexSet(""); err == nil { + t.Fatal("index set id is required") + } + if _, err = client.DeleteIndexSet("h"); err == nil { + t.Fatal(`no index set whose id is "h"`) + } +} + +func TestHandleSetDefaultIndexSet(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + iss, _, _, _, err := client.GetIndexSets(0, 0, false) + if err != nil { + t.Fatal(err) + } + var defIs, is *graylog.IndexSet + for _, i := range iss { + if i.Default { + defIs = &i + } else { + is = &i + } + } + if is == nil { + is = testutil.IndexSet("hoge") + is.Default = false + is.Writable = true + if _, err := client.CreateIndexSet(is); err != nil { + t.Fatal(err) + } + testutil.WaitAfterCreateIndexSet(server) + defer func(id string) { + if _, err := client.DeleteIndexSet(id); err != nil { + t.Fatal(err) + } + testutil.WaitAfterDeleteIndexSet(server) + }(is.ID) + } + is, _, err = client.SetDefaultIndexSet(is.ID) + if err != nil { + t.Fatal("Failed to UpdateIndexSet", err) + } + defer func(id string) { + if _, _, err = client.SetDefaultIndexSet(id); err != nil { + t.Fatal(err) + } + }(defIs.ID) + if !is.Default { + t.Fatal("updatedIndexSet.Default == false") + } + if _, _, err := client.SetDefaultIndexSet(""); err == nil { + t.Fatal("index set id is required") + } + if _, _, err := client.SetDefaultIndexSet("h"); err == nil { + t.Fatal(`no index set whose id is "h"`) + } + + prms := is.NewUpdateParams() + prms.Writable = ptr.PBool(false) + if _, _, err := client.UpdateIndexSet(prms); err == nil { + t.Fatal("Default index set must be writable.") + } +} diff --git a/mockserver/handler/input.go b/mockserver/handler/input.go new file mode 100644 index 00000000..a52026a6 --- /dev/null +++ b/mockserver/handler/input.go @@ -0,0 +1,152 @@ +package handler + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/util" + "github.com/suzuki-shunsuke/go-set" +) + +// HandleGetInput is the handler of Get an Input API. +func HandleGetInput( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // GET /system/inputs/{inputID} Get information of a single input on this node + id := ps.ByName("inputID") + if sc, err := lgc.Authorize(user, "inputs:read", id); err != nil { + return nil, sc, err + } + return lgc.GetInput(id) +} + +// HandleGetInputs is the handler of Get Inputs API. +func HandleGetInputs( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, _ httprouter.Params, +) (interface{}, int, error) { + // GET /system/inputs Get all inputs + arr, total, sc, err := lgc.GetInputs() + if err != nil { + return arr, sc, err + } + inputs := &graylog.InputsBody{Inputs: arr, Total: total} + return inputs, sc, nil +} + +// HandleCreateInput is the handler of Create an Input API. +func HandleCreateInput( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, _ httprouter.Params, +) (interface{}, int, error) { + // POST /system/inputs Launch input on this node + if sc, err := lgc.Authorize(user, "inputs:create"); err != nil { + return nil, sc, err + } + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Required: set.NewStrSet("title", "type", "configuration"), + Optional: set.NewStrSet("global", "node"), + ExtForbidden: true, + }) + if err != nil { + return nil, sc, err + } + // change configuration to attributes + // https://github.com/Graylog2/graylog2-server/issues/3480 + body["attributes"] = body["configuration"] + delete(body, "configuration") + d := &graylog.InputData{} + if err := util.MSDecode(body, d); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Info("Failed to parse request body as InputData") + return nil, 400, err + } + input := &graylog.Input{} + if err := d.ToInput(input); err != nil { + return nil, 400, err + } + sc, err = lgc.AddInput(input) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return &map[string]string{"id": input.ID}, sc, nil +} + +// HandleUpdateInput is the handler of Update an Input API. +func HandleUpdateInput( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // PUT /system/inputs/{inputID} Update input on this node + id := ps.ByName("inputID") + if sc, err := lgc.Authorize(user, "inputs:edit", id); err != nil { + return nil, sc, err + } + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Required: set.NewStrSet("title", "type", "configuration"), + Optional: set.NewStrSet("global", "node"), + ExtForbidden: true, + }) + if err != nil { + return nil, sc, err + } + // change configuration to attributes + // https://github.com/Graylog2/graylog2-server/issues/3480 + body["attributes"] = body["configuration"] + delete(body, "configuration") + d := &graylog.InputUpdateParamsData{} + if err := util.MSDecode(body, d); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Info("Failed to parse request body as InputUpdateParamsData") + return nil, 400, err + } + prms := &graylog.InputUpdateParams{} + if err := d.ToInputUpdateParams(prms); err != nil { + return nil, 400, err + } + + lgc.Logger().WithFields(log.Fields{ + "body": body, "input": prms, "id": id, + }).Debug("request body") + + prms.ID = id + input, sc, err := lgc.UpdateInput(prms) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return input, sc, nil +} + +// HandleDeleteInput is the handler of Delete an Input API. +func HandleDeleteInput( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // DELETE /system/inputs/{inputID} Terminate input on this node + id := ps.ByName("inputID") + if sc, err := lgc.Authorize(user, "inputs:terminate", id); err != nil { + return nil, sc, err + } + sc, err := lgc.DeleteInput(id) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return nil, sc, nil +} diff --git a/mockserver/handler/input_test.go b/mockserver/handler/input_test.go new file mode 100644 index 00000000..8658c031 --- /dev/null +++ b/mockserver/handler/input_test.go @@ -0,0 +1,185 @@ +package handler_test + +import ( + "bytes" + "net/http" + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestHandleGetInput(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + input := testutil.Input() + if _, err := server.AddInput(input); err != nil { + t.Fatal(err) + } + act, _, err := client.GetInput(input.ID) + if err != nil { + t.Fatal(err) + } + if input.Node != act.Node { + t.Fatalf("Node == %s, wanted %s", act.Node, input.Node) + } + + if _, _, err := client.GetInput(""); err == nil { + t.Fatal("input id is required") + } + + if _, _, err := client.GetInput("h"); err == nil { + t.Fatal(`no input whose id is "h"`) + } +} + +func TestHandleGetInputs(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + act, _, _, err := client.GetInputs() + if err != nil { + t.Fatal(err) + } + if act == nil { + t.Fatal("client.GetInputs() returns nil") + } + if len(act) != 1 { + t.Fatalf("len(act) == %d, wanted 1", len(act)) + } +} + +func TestHandleCreateInput(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + input := testutil.Input() + if _, err := client.CreateInput(input); err != nil { + t.Fatal(err) + } + if input.ID == "" { + t.Fatal(`client.CreateInput() == ""`) + } + if _, err := client.CreateInput(nil); err == nil { + t.Fatal("input is nil") + } + + body := bytes.NewBuffer([]byte("hoge")) + req, err := http.NewRequest( + http.MethodPost, client.Endpoints().Inputs(), body) + if err != nil { + t.Fatal(err) + } + req.SetBasicAuth(client.Name(), client.Password()) + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 400 { + t.Fatalf("resp.StatusCode == %d, wanted 400", resp.StatusCode) + } +} + +func TestHandleUpdateInput(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + input := testutil.Input() + if _, err := server.AddInput(input); err != nil { + t.Fatal(err) + } + id := input.ID + input.Title += " updated" + if _, _, err := client.UpdateInput(input.NewUpdateParams()); err != nil { + t.Fatal(err) + } + act, _, err := server.GetInput(id) + if err != nil { + t.Fatal(err) + } + if act == nil { + t.Fatal("input is not found") + } + if act.Title != input.Title { + t.Fatalf(`UpdateInput title "%s" != "%s"`, act.Title, input.Title) + } + + input.ID = "" + if _, _, err := client.UpdateInput(input.NewUpdateParams()); err == nil { + t.Fatal("input id is required") + } + + input.ID = "h" + if _, _, err := client.UpdateInput(input.NewUpdateParams()); err == nil { + t.Fatal(`no input whose id is "h"`) + } + + input.ID = id + input.Attrs = nil + if _, _, err := client.UpdateInput(input.NewUpdateParams()); err == nil { + t.Fatal("input attributes is required") + } + input.Attrs = act.Attrs + input.Title = "" + if _, _, err := client.UpdateInput(input.NewUpdateParams()); err == nil { + t.Fatal("input title is required") + } + + input.Title = act.Title + + if _, _, err := client.UpdateInput(nil); err == nil { + t.Fatal("input is required") + } + + body := bytes.NewBuffer([]byte("hoge")) + u, err := client.Endpoints().Input(id) + if err != nil { + t.Fatal(err) + } + req, err := http.NewRequest( + http.MethodPut, u.String(), body) + if err != nil { + t.Fatal(err) + } + req.SetBasicAuth(client.Name(), client.Password()) + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 400 { + t.Fatalf("resp.StatusCode == %d, wanted 400", resp.StatusCode) + } +} + +func TestHandleDeleteInput(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + input := testutil.Input() + if _, err = server.AddInput(input); err != nil { + t.Fatal(err) + } + if _, err = client.DeleteInput(input.ID); err != nil { + t.Fatal("Failed to DeleteInput", err) + } + + if _, err := client.DeleteInput(""); err == nil { + t.Fatal("input id is required") + } + + if _, err := client.DeleteInput("h"); err == nil { + t.Fatal(`no input whose id is "h"`) + } +} diff --git a/mockserver/handler/not_found.go b/mockserver/handler/not_found.go new file mode 100644 index 00000000..27d9b461 --- /dev/null +++ b/mockserver/handler/not_found.go @@ -0,0 +1,23 @@ +package handler + +import ( + "fmt" + "net/http" + + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" +) + +// HandleNotFound is the generator of the NotFound handler. +func HandleNotFound(lgc *logic.Logic) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + lgc.Logger().WithFields(log.Fields{ + "path": r.URL.Path, "method": r.Method, + "message": "404 Page Not Found", + }).Info("request start") + w.WriteHeader(404) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(fmt.Sprintf( + `{"message":"Page Not Found %s %s"}`, r.Method, r.URL.Path))) + } +} diff --git a/mockserver/handler/not_found_test.go b/mockserver/handler/not_found_test.go new file mode 100644 index 00000000..cb2d80fa --- /dev/null +++ b/mockserver/handler/not_found_test.go @@ -0,0 +1,26 @@ +package handler_test + +import ( + "fmt" + "net/http" + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestHandleNotFound(t *testing.T) { + server, _, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + endpoint := fmt.Sprintf("%s/dummy", server.Endpoint()) + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + t.Fatal(err) + } + hc := &http.Client{} + if _, err := hc.Do(req); err != nil { + t.Fatal(err) + } +} diff --git a/mockserver/handler/role.go b/mockserver/handler/role.go new file mode 100644 index 00000000..8dfc1403 --- /dev/null +++ b/mockserver/handler/role.go @@ -0,0 +1,136 @@ +package handler + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/util" + "github.com/suzuki-shunsuke/go-set" +) + +// HandleGetRole is the handler of GET Role API. +func HandleGetRole( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // GET /roles/{rolename} Retrieve permissions for a single role + name := ps.ByName("rolename") + lgc.Logger().WithFields(log.Fields{ + "handler": "handleGetRole", "rolename": name}).Info("request start") + if sc, err := lgc.Authorize(user, "roles:read", name); err != nil { + return nil, sc, err + } + return lgc.GetRole(name) +} + +// HandleGetRoles is the handler of GET Roles API. +func HandleGetRoles( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, _ httprouter.Params, +) (interface{}, int, error) { + // GET /roles List all roles + arr, total, sc, err := lgc.GetRoles() + if err != nil { + return arr, sc, err + } + return &graylog.RolesBody{Roles: arr, Total: total}, sc, nil +} + +// HandleCreateRole is the handler of Create Role API. +func HandleCreateRole( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, _ httprouter.Params, +) (interface{}, int, error) { + // POST /roles Create a new role + if sc, err := lgc.Authorize(user, "roles:create"); err != nil { + return nil, sc, err + } + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Required: set.NewStrSet("name", "permissions"), + Optional: set.NewStrSet("description"), + Ignored: set.NewStrSet("read_only"), + ExtForbidden: true, + }) + if err != nil { + return nil, sc, err + } + + role := &graylog.Role{} + if err := util.MSDecode(body, &role); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Warn("Failed to parse request body as Role") + return nil, 400, err + } + + if sc, err := lgc.AddRole(role); err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return role, sc, nil +} + +// HandleUpdateRole is the handler of Update Role API. +func HandleUpdateRole( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // PUT /roles/{rolename} Update an existing role + name := ps.ByName("rolename") + if sc, err := lgc.Authorize(user, "roles:edit", name); err != nil { + return nil, sc, err + } + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Required: set.NewStrSet("name", "permissions"), + Optional: set.NewStrSet("description"), + Ignored: set.NewStrSet("read_only"), + ExtForbidden: true, + }) + if err != nil { + return nil, sc, err + } + + prms := &graylog.RoleUpdateParams{} + if err := util.MSDecode(body, prms); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Info("Failed to parse request body as Role") + return nil, 400, err + } + + role, sc, err := lgc.UpdateRole(name, prms) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return role, sc, nil +} + +// HandleDeleteRole is the handler of Delete Role API. +func HandleDeleteRole( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // DELETE /roles/{rolename} Remove the named role and dissociate any users from it + name := ps.ByName("rolename") + if sc, err := lgc.Authorize(user, "roles:delete", name); err != nil { + return nil, sc, err + } + sc, err := lgc.DeleteRole(name) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return nil, sc, nil +} diff --git a/mockserver/handler/role_member.go b/mockserver/handler/role_member.go new file mode 100644 index 00000000..6065c01c --- /dev/null +++ b/mockserver/handler/role_member.go @@ -0,0 +1,57 @@ +package handler + +import ( + "fmt" + "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" +) + +type membersBody struct { + Role string `json:"role"` + Users []graylog.User `json:"users"` +} + +// HandleRoleMembers is the handler of Get the role's members API. +func HandleRoleMembers( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // GET /roles/{rolename}/members Retrieve the role's members + name := ps.ByName("rolename") + ok, err := lgc.HasRole(name) + if err != nil { + return nil, 500, err + } + if !ok { + return nil, 404, fmt.Errorf("no role found with name %s", name) + } + arr, sc, err := lgc.RoleMembers(name) + if err != nil { + return nil, sc, err + } + users := &membersBody{Users: arr, Role: name} + return users, sc, nil +} + +// HandleAddUserToRole is the handler of Add a user to a role API. +func HandleAddUserToRole( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // PUT /roles/{rolename}/members/{username} Add a user to a role + sc, err := lgc.AddUserToRole(ps.ByName("username"), ps.ByName("rolename")) + return nil, sc, err +} + +// HandleRemoveUserFromRole is the handler of Remove a user from a role API. +func HandleRemoveUserFromRole( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // DELETE /roles/{rolename}/members/{username} Remove a user from a role + sc, err := lgc.RemoveUserFromRole(ps.ByName("username"), ps.ByName("rolename")) + return nil, sc, err +} diff --git a/mockserver/handler/role_member_test.go b/mockserver/handler/role_member_test.go new file mode 100644 index 00000000..35c71843 --- /dev/null +++ b/mockserver/handler/role_member_test.go @@ -0,0 +1,19 @@ +package handler_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/test" +) + +func TestHandleRoleMembers(t *testing.T) { + test.TestGetRoleMembers(t) +} + +func TestHandleAddUserToRole(t *testing.T) { + test.TestAddUserToRole(t) +} + +func TestHandleRemoveUserFromRole(t *testing.T) { + test.TestRemoveUserFromRole(t) +} diff --git a/mockserver/handler/role_test.go b/mockserver/handler/role_test.go new file mode 100644 index 00000000..9da05e6c --- /dev/null +++ b/mockserver/handler/role_test.go @@ -0,0 +1,172 @@ +package handler_test + +import ( + "bytes" + "net/http" + "testing" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/testutil" + "github.com/suzuki-shunsuke/go-set" +) + +func TestHandleGetRole(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + role, _, err := client.GetRole("Admin") + if err != nil { + t.Fatal("Failed to GetRole", err) + } + if role.Name != "Admin" { + t.Fatalf(`role name is "%s", wanted "Admin"`, role.Name) + } + if _, _, err := client.GetRole(""); err == nil { + t.Fatal("role name is required") + } + if _, _, err := client.GetRole("h"); err == nil { + t.Fatal(`no role whose name is "h"`) + } +} + +func TestHandleGetRoles(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + roles, _, _, err := client.GetRoles() + if err != nil { + t.Fatal("Failed to GetRoles", err) + } + if roles == nil { + t.Fatal("client.GetRoles() is nil") + } + if len(roles) != 1 { + t.Fatalf("len(roles) == %d, wanted 1", len(roles)) + } +} + +func TestHandleCreateRole(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + role := &graylog.Role{Name: "foo", Permissions: set.NewStrSet("*")} + if _, err := client.CreateRole(role); err != nil { + t.Fatal("Failed to CreateRole", err) + } + if role.Name != "foo" { + t.Fatalf("role.Name == %s, wanted foo", role.Name) + } + ei, err := client.CreateRole(role) + if err == nil { + t.Fatal("user name must be unique") + } + if ei.Response.StatusCode != 400 { + t.Fatal("status code must be 400") + } + + role.Name = "" + if _, err := client.CreateRole(role); err == nil { + t.Fatal("user name is required") + } + + role.Name = "bar" + role.Permissions = nil + if _, err := client.CreateRole(role); err == nil { + t.Fatal("user permissions are required") + } + + body := bytes.NewBuffer([]byte("hoge")) + req, err := http.NewRequest( + http.MethodPost, client.Endpoints().Roles(), body) + if err != nil { + t.Fatal(err) + } + req.SetBasicAuth(client.Name(), client.Password()) + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 400 { + t.Fatalf("resp.StatusCode == %d, wanted 400", resp.StatusCode) + } +} + +func TestHandleUpdateRole(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + name := "foo" + role, err := testutil.GetRoleOrCreate(client, name) + if err != nil { + t.Fatal(err) + } + role.Description += " changed!" + if _, _, err := client.UpdateRole(name, role.NewUpdateParams()); err != nil { + t.Fatal(err) + } + if _, _, err := client.UpdateRole("", role.NewUpdateParams()); err == nil { + t.Fatal("role name is required") + } + if _, _, err := client.UpdateRole("h", role.NewUpdateParams()); err == nil { + t.Fatal(`no role whose name is "h"`) + } + + role.Name = "" + if _, _, err := client.UpdateRole(name, role.NewUpdateParams()); err == nil { + t.Fatal("role name is required") + } + role.Name = name + role.Permissions = nil + if _, _, err := client.UpdateRole(name, role.NewUpdateParams()); err == nil { + t.Fatal("role permissions is required") + } + + body := bytes.NewBuffer([]byte("hoge")) + u, err := client.Endpoints().Role(name) + if err != nil { + t.Fatal(err) + } + req, err := http.NewRequest(http.MethodPut, u.String(), body) + if err != nil { + t.Fatal(err) + } + req.SetBasicAuth(client.Name(), client.Password()) + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 400 { + t.Fatalf("resp.StatusCode == %d, wanted 400", resp.StatusCode) + } +} + +func TestHandleDeleteRole(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + role, err := testutil.GetRoleOrCreate(client, "foo") + if err != nil { + t.Fatal(err) + } + if _, err = client.DeleteRole(role.Name); err != nil { + t.Fatal("Failed to DeleteRole", err) + } + if _, err = client.DeleteRole(""); err == nil { + t.Fatal("role name is required") + } + if _, err = client.DeleteRole("h"); err == nil { + t.Fatal(`no role whose name is "h"`) + } +} diff --git a/mockserver/handler/router.go b/mockserver/handler/router.go new file mode 100644 index 00000000..ec5c5009 --- /dev/null +++ b/mockserver/handler/router.go @@ -0,0 +1,67 @@ +package handler + +import ( + "github.com/julienschmidt/httprouter" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" +) + +// NewRouter returns a new HTTP router. +func NewRouter(lgc *logic.Logic) *httprouter.Router { + router := httprouter.New() + + router.GET("/api/roles/:rolename", wrapHandle(lgc, HandleGetRole)) + router.PUT("/api/roles/:rolename", wrapHandle(lgc, HandleUpdateRole)) + router.DELETE("/api/roles/:rolename", wrapHandle(lgc, HandleDeleteRole)) + router.GET("/api/roles", wrapHandle(lgc, HandleGetRoles)) + router.POST("/api/roles", wrapHandle(lgc, HandleCreateRole)) + + router.GET("/api/users/:username", wrapHandle(lgc, HandleGetUser)) + router.PUT("/api/users/:username", wrapHandle(lgc, HandleUpdateUser)) + router.DELETE("/api/users/:username", wrapHandle(lgc, HandleDeleteUser)) + router.GET("/api/users", wrapHandle(lgc, HandleGetUsers)) + router.POST("/api/users", wrapHandle(lgc, HandleCreateUser)) + + router.GET("/api/roles/:rolename/members", wrapHandle(lgc, HandleRoleMembers)) + router.PUT("/api/roles/:rolename/members/:username", wrapHandle(lgc, HandleAddUserToRole)) + router.DELETE( + "/api/roles/:rolename/members/:username", wrapHandle(lgc, HandleRemoveUserFromRole)) + + router.GET("/api/system/inputs", wrapHandle(lgc, HandleGetInputs)) + router.GET("/api/system/inputs/:inputID", wrapHandle(lgc, HandleGetInput)) + router.POST("/api/system/inputs", wrapHandle(lgc, HandleCreateInput)) + router.PUT("/api/system/inputs/:inputID", wrapHandle(lgc, HandleUpdateInput)) + router.DELETE("/api/system/inputs/:inputID", wrapHandle(lgc, HandleDeleteInput)) + + router.GET("/api/system/indices/index_sets", wrapHandle(lgc, HandleGetIndexSets)) + router.GET( + "/api/system/indices/index_sets/:indexSetID", wrapHandle(lgc, HandleGetIndexSet)) + router.POST("/api/system/indices/index_sets", wrapHandle(lgc, HandleCreateIndexSet)) + router.PUT( + "/api/system/indices/index_sets/:indexSetID", wrapHandle(lgc, HandleUpdateIndexSet)) + router.DELETE( + "/api/system/indices/index_sets/:indexSetID", wrapHandle(lgc, HandleDeleteIndexSet)) + router.PUT( + "/api/system/indices/index_sets/:indexSetID/default", + wrapHandle(lgc, HandleSetDefaultIndexSet)) + + router.GET( + "/api/system/indices/index_sets/:indexSetID/stats", + wrapHandle(lgc, HandleGetIndexSetStats)) + + router.GET("/api/streams", wrapHandle(lgc, HandleGetStreams)) + router.POST("/api/streams", wrapHandle(lgc, HandleCreateStream)) + router.GET("/api/streams/:streamID", wrapHandle(lgc, HandleGetStream)) + router.PUT("/api/streams/:streamID", wrapHandle(lgc, HandleUpdateStream)) + router.DELETE("/api/streams/:streamID", wrapHandle(lgc, HandleDeleteStream)) + router.POST("/api/streams/:streamID/pause", wrapHandle(lgc, HandlePauseStream)) + router.POST("/api/streams/:streamID/resume", wrapHandle(lgc, HandleResumeStream)) + + router.GET("/api/streams/:streamID/rules", wrapHandle(lgc, HandleGetStreamRules)) + router.POST("/api/streams/:streamID/rules", wrapHandle(lgc, HandleCreateStreamRule)) + router.PUT("/api/streams/:streamID/rules/:streamRuleID", wrapHandle(lgc, HandleUpdateStreamRule)) + router.DELETE("/api/streams/:streamID/rules/:streamRuleID", wrapHandle(lgc, HandleDeleteStreamRule)) + router.GET("/api/streams/:streamID/rules/:streamRuleID", wrapHandle(lgc, HandleGetStreamRule)) + + router.NotFound = HandleNotFound(lgc) + return router +} diff --git a/mockserver/handler/stream.go b/mockserver/handler/stream.go new file mode 100644 index 00000000..01e8da15 --- /dev/null +++ b/mockserver/handler/stream.go @@ -0,0 +1,177 @@ +package handler + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/util" + "github.com/suzuki-shunsuke/go-set" +) + +// HandleGetStreams is the handler of Get Streams API. +func HandleGetStreams( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, _ httprouter.Params, +) (interface{}, int, error) { + // GET /streams Get a list of all streams + arr, total, sc, err := lgc.GetStreams() + if err != nil { + return nil, sc, err + } + + return &graylog.StreamsBody{Streams: arr, Total: total}, sc, nil +} + +// HandleGetStream is the handler of Get a Stream API. +func HandleGetStream( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // GET /streams/{streamID} Get a single stream + id := ps.ByName("streamID") + if id == "enabled" { + return HandleGetEnabledStreams(user, lgc, w, r, ps) + } + if sc, err := lgc.Authorize(user, "streams:read", id); err != nil { + return nil, sc, err + } + return lgc.GetStream(id) +} + +// HandleCreateStream is the handler of Create a Stream API. +func HandleCreateStream( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, _ httprouter.Params, +) (interface{}, int, error) { + // POST /streams Create index set + if sc, err := lgc.Authorize(user, "streams:create"); err != nil { + return nil, sc, err + } + // empty description is ignored + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Required: set.NewStrSet("title", "index_set_id"), + Optional: set.NewStrSet("rules", "description", "content_pack", "matching_type", "remove_matches_from_default_stream"), + ExtForbidden: true, + }) + if err != nil { + return nil, sc, err + } + + stream := &graylog.Stream{} + if err := util.MSDecode(body, stream); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Info("Failed to parse request body as stream") + return nil, 400, err + } + + sc, err = lgc.AddStream(stream) + if err != nil { + return nil, sc, err + } + return map[string]string{"stream_id": stream.ID}, sc, nil +} + +// HandleUpdateStream is the handler of Update a Stream API. +func HandleUpdateStream( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // PUT /streams/{streamID} Update a stream + prms := &graylog.StreamUpdateParams{ID: ps.ByName("streamID")} + if sc, err := lgc.Authorize(user, "streams:edit", prms.ID); err != nil { + return nil, sc, err + } + + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Required: nil, + Optional: set.NewStrSet( + "title", "index_set_id", "description", "outputs", "matching_type", + "rules", "alert_conditions", "alert_receivers", + "remove_matches_from_default_stream"), + Ignored: set.NewStrSet("creator_user_id", "created_at", "disabled", "is_default"), + ExtForbidden: false, + }) + if err != nil { + return nil, sc, err + } + + if err := util.MSDecode(body, prms); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Warn("Failed to parse request body as stream") + return nil, 400, err + } + + stream, sc, err := lgc.UpdateStream(prms) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return stream, sc, nil +} + +// HandleDeleteStream is the handler of Delete a Stream API. +func HandleDeleteStream( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // DELETE /streams/{streamID} Delete a stream + id := ps.ByName("streamID") + // TODO authorization + sc, err := lgc.DeleteStream(id) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return nil, sc, nil +} + +// HandleGetEnabledStreams is the handler of Get all enabled streams API. +func HandleGetEnabledStreams( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, _ httprouter.Params, +) (interface{}, int, error) { + // GET /streams/enabled Get a list of all enabled streams + arr, total, sc, err := lgc.GetEnabledStreams() + if err != nil { + return nil, sc, err + } + return &graylog.StreamsBody{Streams: arr, Total: total}, sc, nil +} + +// HandlePauseStream is the handler of Pause a Stream API. +func HandlePauseStream( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // POST /streams/{streamID}/pause Pause a stream + id := ps.ByName("streamID") + if sc, err := lgc.Authorize(user, "streams:changestate", id); err != nil { + return nil, sc, err + } + sc, err := lgc.PauseStream(id) + return nil, sc, err +} + +// HandleResumeStream is the handler of Resume a Stream API. +func HandleResumeStream( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + id := ps.ByName("streamID") + if sc, err := lgc.Authorize(user, "streams:changestate", id); err != nil { + return nil, sc, err + } + sc, err := lgc.ResumeStream(id) + return nil, sc, err +} diff --git a/mockserver/handler/stream_rule.go b/mockserver/handler/stream_rule.go new file mode 100644 index 00000000..5f3853ef --- /dev/null +++ b/mockserver/handler/stream_rule.go @@ -0,0 +1,147 @@ +package handler + +import ( + "fmt" + "net/http" + + "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/util" + "github.com/suzuki-shunsuke/go-set" +) + +// HandleGetStreamRules is the handler of Get Stream Rules API. +func HandleGetStreamRules( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // GET /streams/{streamid}/rules Get a list of all stream rules + streamID := ps.ByName("streamID") + arr, total, sc, err := lgc.GetStreamRules(streamID) + if err != nil { + return nil, sc, err + } + return &graylog.StreamRulesBody{StreamRules: arr, Total: total}, sc, nil +} + +// HandleGetStreamRule is the handler of Get a Stream Rule API. +func HandleGetStreamRule( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // GET /streams/{streamid}/rules/{streamRuleId} Get a single stream rules + // TODO authorization + return lgc.GetStreamRule( + ps.ByName("streamID"), ps.ByName("streamRuleID")) +} + +// HandleCreateStreamRule is the handler of Create a Stream Rule API. +func HandleCreateStreamRule( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // POST /streams/{streamid}/rules Create a stream rule + streamID := ps.ByName("streamID") + ok, err := lgc.HasStream(streamID) + if err != nil { + return nil, 500, err + } + if !ok { + return nil, 404, fmt.Errorf("stream <%s> not found", streamID) + } + + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Required: set.NewStrSet("value", "field"), + Optional: set.NewStrSet("type", "description", "inverted"), + ExtForbidden: true, + }) + if err != nil { + return nil, sc, err + } + + rule := &graylog.StreamRule{} + if err := util.MSDecode(body, rule); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Info("Failed to parse request body as StreamRule") + return nil, 400, err + } + lgc.Logger().WithFields(log.Fields{ + "body": body, "stream_rule": rule, + }).Debug("request body") + + rule.StreamID = streamID + sc, err = lgc.AddStreamRule(rule) + if err != nil { + logic.LogWE(sc, lgc.Logger().WithFields(log.Fields{ + "error": err, "rule": rule, "status_code": sc, + }), "Faield to add rule to mock server") + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return map[string]string{"streamrule_id": rule.ID}, sc, nil +} + +// type 400 {"type": "ApiError", "message": "Unknown stream rule type 0"} + +// HandleUpdateStreamRule is the handler of Update a Stream Rule API. +func HandleUpdateStreamRule( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // PUT /streams/{streamid}/rules/{streamRuleID} Update a stream rule + streamID := ps.ByName("streamID") + ruleID := ps.ByName("streamRuleID") + + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Required: set.NewStrSet("value", "field"), + Optional: set.NewStrSet("type", "description", "inverted"), + ExtForbidden: true, + }) + if err != nil { + return nil, sc, err + } + prms := &graylog.StreamRuleUpdateParams{} + if err := util.MSDecode(body, prms); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Info("Failed to parse request body as StreamRuleUpdateParams") + return nil, 400, err + } + lgc.Logger().WithFields(log.Fields{ + "body": body, "stream_rule": prms, + }).Debug("request body") + + prms.StreamID = streamID + prms.ID = ruleID + sc, err = lgc.UpdateStreamRule(prms) + if err != nil { + logic.LogWE(sc, lgc.Logger().WithFields(log.Fields{ + "error": err, "rule": &prms, "status_code": sc, + }), "faield to update stream rule") + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return map[string]string{"streamrule_id": prms.ID}, sc, nil +} + +// HandleDeleteStreamRule is the handler of Delete a Stream Rule API. +func HandleDeleteStreamRule( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // DELETE /streams/{streamid}/rules/{streamRuleId} Delete a stream rule + streamID := ps.ByName("streamID") + id := ps.ByName("streamRuleID") + // TODO authorization + sc, err := lgc.DeleteStreamRule(streamID, id) + return nil, sc, err +} diff --git a/mockserver/handler/stream_rule_test.go b/mockserver/handler/stream_rule_test.go new file mode 100644 index 00000000..ead3436a --- /dev/null +++ b/mockserver/handler/stream_rule_test.go @@ -0,0 +1,96 @@ +package handler_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/test" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestHandleGetStreamRules(t *testing.T) { + test.TestGetStreamRules(t) +} + +func TestHandleGetStreamRule(t *testing.T) { + test.TestGetStreamRule(t) +} + +func TestHandleCreateStreamRule(t *testing.T) { + test.TestCreateStreamRule(t) + server, _, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + streams, _, _, err := server.GetStreams() + if err != nil { + t.Fatal(err) + } + stream := streams[0] + rules, _, _, err := server.GetStreamRules(stream.ID) + if err != nil { + t.Fatal(err) + } + rule := rules[0] + rule.ID = "" + rule.StreamID = "" + if _, err := server.AddStreamRule(&rule); err == nil { + t.Fatal("stream id is required") + } + rule.StreamID = "h" + if _, err := server.AddStreamRule(&rule); err == nil { + t.Fatal(`no stream with id "h" is found`) + } +} + +func TestHandleUpdateStreamRule(t *testing.T) { + test.TestUpdateStreamRule(t) + server, _, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + streams, _, _, err := server.GetStreams() + if err != nil { + t.Fatal(err) + } + stream := streams[0] + rules, _, _, err := server.GetStreamRules(stream.ID) + if err != nil { + t.Fatal(err) + } + rule := rules[0] + rule.StreamID = "" + if _, err := server.UpdateStreamRule(rule.NewUpdateParams()); err == nil { + t.Fatal("stream id is required") + } + rule.StreamID = "h" + if _, err := server.UpdateStreamRule(rule.NewUpdateParams()); err == nil { + t.Fatal(`no stream with id "h" is found`) + } +} + +func TestHandleDeleteStreamRule(t *testing.T) { + test.TestDeleteStreamRule(t) + server, _, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + streams, _, _, err := server.GetStreams() + if err != nil { + t.Fatal(err) + } + stream := streams[0] + rules, _, _, err := server.GetStreamRules(stream.ID) + if err != nil { + t.Fatal(err) + } + rule := rules[0] + if _, err := server.DeleteStreamRule("h", rule.ID); err == nil { + t.Fatal(`no stream with id "h" is found`) + } + if _, err := server.DeleteStreamRule(rule.StreamID, "h"); err == nil { + t.Fatal(`no stream rule with id "h" is found`) + } +} diff --git a/mockserver/handler/stream_test.go b/mockserver/handler/stream_test.go new file mode 100644 index 00000000..8a51559e --- /dev/null +++ b/mockserver/handler/stream_test.go @@ -0,0 +1,277 @@ +package handler_test + +import ( + "bytes" + "fmt" + "io" + "net/http" + "testing" + + "github.com/suzuki-shunsuke/go-graylog" + . "github.com/suzuki-shunsuke/go-graylog/mockserver" + "github.com/suzuki-shunsuke/go-graylog/test" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func testUpdateStreamStatusCode( + endpoint, name, password string, body io.Reader, statusCode int, +) error { + req, err := http.NewRequest( + http.MethodPut, endpoint, body) + if err != nil { + return err + } + req.SetBasicAuth(name, password) + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + return err + } + if resp.StatusCode != statusCode { + return fmt.Errorf("resp.StatusCode == %d, wanted 400", resp.StatusCode) + } + return nil +} + +func TestGetStream(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + _, stream, err := addDummyStream(server) + if err != nil { + t.Fatal(err) + } + + act, _, err := client.GetStream(stream.ID) + if err != nil { + t.Fatal("Failed to GetStream", err) + } + if act.Title != stream.Title { + t.Fatalf("act.Title == %s, wanted %s", act.Title, stream.Title) + } + if _, _, err := client.GetStream(""); err == nil { + t.Fatal("id is required") + } + if _, _, err := client.GetStream("h"); err == nil { + t.Fatal(`no stream whose id is "h"`) + } +} + +func TestGetStreams(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + streams, total, _, err := client.GetStreams() + if err != nil { + t.Fatal("Failed to GetStreams", err) + } + if total != 1 { + t.Fatalf("total == %d, wanted %d", total, 1) + } + if len(streams) != 1 { + t.Fatalf("len(stream) == %d, wanted %d", len(streams), 1) + } +} + +func TestHandleCreateStream(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + stream := testutil.DummyStream() + if _, err := client.CreateStream(stream); err == nil { + t.Fatal("CreateStream() must be failed") + } + indexSet := testutil.IndexSet("hoge") + if _, err := server.AddIndexSet(indexSet); err != nil { + t.Fatal(err) + } + stream = testutil.Stream() + stream.IndexSetID = indexSet.ID + + if _, err := client.CreateStream(stream); err != nil { + t.Fatal("Failed to CreateStream", err) + } + if stream.ID == "" { + t.Fatal(`stream id is empty`) + } + if _, err := client.CreateStream(stream); err == nil { + t.Fatal("id must be empty") + } + stream.ID = "" + stream.CreatorUserID = "h" + if _, err := client.CreateStream(stream); err == nil { + t.Fatal("creator_user_id must be empty") + } + stream.CreatorUserID = "" + stream.CreatedAt = "h" + if _, err := client.CreateStream(stream); err == nil { + t.Fatal("created_at must be empty") + } + stream.CreatedAt = "" + stream.Disabled = true + if _, err := client.CreateStream(stream); err == nil { + t.Fatal("disabled must be false") + } + stream.Disabled = false + stream.IsDefault = true + if _, err := client.CreateStream(stream); err == nil { + t.Fatal("is_default must be false") + } + + copiedStream := *stream + stream.IsDefault = false + stream.Title = "" + if _, err := client.CreateStream(stream); err == nil { + t.Fatal("title is required") + } + stream.Title = copiedStream.Title + stream.IndexSetID = "" + if _, err := client.CreateStream(stream); err == nil { + t.Fatal("index_set_id is required") + } + stream.IndexSetID = copiedStream.IndexSetID + stream.AlertReceivers = &graylog.AlertReceivers{} + if _, err := client.CreateStream(stream); err == nil { + t.Fatal("alert_receiver is required") + } + if _, err := client.CreateStream(nil); err == nil { + t.Fatal("stream is nil") + } + + body := bytes.NewBuffer([]byte("hoge")) + req, err := http.NewRequest( + http.MethodPost, client.Endpoints().Streams(), body) + if err != nil { + t.Fatal(err) + } + req.SetBasicAuth(client.Name(), client.Password()) + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 400 { + t.Fatalf("resp.StatusCode == %d, wanted 400", resp.StatusCode) + } +} + +func TestServerHandleUpdateStream(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + is := testutil.IndexSet("hoge") + if _, err := server.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := server.AddStream(stream); err != nil { + t.Fatal(err) + } + u, err := client.Endpoints().Stream(stream.ID) + if err != nil { + t.Fatal(err) + } + endpoint := u.String() + + body := bytes.NewBuffer([]byte("hoge")) + if err := testUpdateStreamStatusCode(endpoint, client.Name(), client.Password(), body, 400); err != nil { + t.Fatal(err) + } + + body = bytes.NewBuffer([]byte(`{"title": 0}`)) + if err := testUpdateStreamStatusCode(endpoint, client.Name(), client.Password(), body, 400); err != nil { + t.Fatal(err) + } + + body = bytes.NewBuffer([]byte(`{"description": 0}`)) + if err := testUpdateStreamStatusCode(endpoint, client.Name(), client.Password(), body, 400); err != nil { + t.Fatal(err) + } + + body = bytes.NewBuffer([]byte(`{"matching_type": 0}`)) + if err := testUpdateStreamStatusCode(endpoint, client.Name(), client.Password(), body, 400); err != nil { + t.Fatal(err) + } + + body = bytes.NewBuffer([]byte(`{"remove_matches_from_default_stream": 0}`)) + if err := testUpdateStreamStatusCode(endpoint, client.Name(), client.Password(), body, 400); err != nil { + t.Fatal(err) + } + + body = bytes.NewBuffer([]byte(`{"index_set_id": 0}`)) + if err := testUpdateStreamStatusCode(endpoint, client.Name(), client.Password(), body, 400); err != nil { + t.Fatal(err) + } + + // nil check + if _, _, err := server.UpdateStream(nil); err == nil { + t.Fatal("stream is nil") + } + + // validation + stream.ID = "" + if _, _, err := server.UpdateStream(stream.NewUpdateParams()); err == nil { + t.Fatal("id is required") + } + // id check + stream.ID = "h" + if _, _, err := server.UpdateStream(stream.NewUpdateParams()); err == nil { + t.Fatal("id check") + } + + test.TestUpdateStream(t) +} + +func addDummyStream(server *Server) (*graylog.IndexSet, *graylog.Stream, error) { + indexSet := testutil.IndexSet("hoge") + if _, err := server.AddIndexSet(indexSet); err != nil { + return nil, nil, err + } + stream := testutil.Stream() + stream.IndexSetID = indexSet.ID + _, err := server.AddStream(stream) + return indexSet, stream, err +} + +func TestDeleteStream(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + _, stream, err := addDummyStream(server) + if err != nil { + t.Fatal(err) + } + + if _, err = client.DeleteStream(""); err == nil { + t.Fatal("id is required") + } + if _, err := client.DeleteStream("h"); err == nil { + t.Fatal(`no stream whose id is "h"`) + } + if _, err := client.DeleteStream(stream.ID); err != nil { + t.Fatal("Failed to DeleteStream", err) + } +} + +func TestGetEnabledStreams(t *testing.T) { + test.TestGetEnabledStreams(t) +} + +func TestPauseStream(t *testing.T) { + test.TestPauseStream(t) +} + +func TestResumeStream(t *testing.T) { + test.TestResumeStream(t) +} diff --git a/mockserver/handler/user.go b/mockserver/handler/user.go new file mode 100644 index 00000000..6aefa34b --- /dev/null +++ b/mockserver/handler/user.go @@ -0,0 +1,136 @@ +package handler + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/util" + "github.com/suzuki-shunsuke/go-set" +) + +// HandleGetUsers is the handler of GET Users API. +func HandleGetUsers( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, _ httprouter.Params, +) (interface{}, int, error) { + // GET /users List all users + users, sc, err := lgc.GetUsers() + for i, u := range users { + u.Password = "" + users[i] = u + } + if err != nil { + return nil, sc, err + } + // TODO authorization + return &graylog.UsersBody{Users: users}, sc, nil +} + +// HandleGetUser is the handler of GET User API. +func HandleGetUser( + u *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // GET /users/{username} Get user details + name := ps.ByName("username") + // TODO authorization + user, sc, err := lgc.GetUser(name) + if user != nil { + user.Password = "" + } + return user, sc, err +} + +// HandleCreateUser is the handler of Create User API. +func HandleCreateUser( + u *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, _ httprouter.Params, +) (interface{}, int, error) { + // POST /users Create a new user account. + if sc, err := lgc.Authorize(u, "users:create"); err != nil { + return nil, sc, err + } + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Required: set.NewStrSet("username", "email", "permissions", "full_name", "password"), + Optional: set.NewStrSet("startpage", "timezone", "session_timeout_ms", "roles"), + }) + if err != nil { + return nil, sc, err + } + + user := &graylog.User{} + if err := util.MSDecode(body, user); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Info("Failed to parse request body as User") + return nil, 400, err + } + + sc, err = lgc.AddUser(user) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return nil, sc, nil +} + +// HandleUpdateUser is the handler of Update User API. +func HandleUpdateUser( + u *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // PUT /users/{username} Modify user details. + userName := ps.ByName("username") + if sc, err := lgc.Authorize(u, "users:edit", userName); err != nil { + return nil, sc, err + } + body, sc, err := validateRequestBody( + r.Body, &validateReqBodyPrms{ + Optional: set.NewStrSet("email", "permissions", "full_name", "password", "timezone", "session_timeout_ms", "start_page", "roles"), + Ignored: set.NewStrSet("id", "preferences", "external", "read_only", "session_active", "last_activity", "client_address"), + ExtForbidden: false, + }) + if err != nil { + return nil, sc, err + } + + prms := &graylog.UserUpdateParams{Username: userName} + if err := util.MSDecode(body, prms); err != nil { + lgc.Logger().WithFields(log.Fields{ + "body": body, "error": err, + }).Info("Failed to parse request body as UserUpdateParams") + return nil, 400, err + } + sc, err = lgc.UpdateUser(prms) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return nil, sc, nil +} + +// HandleDeleteUser is the handler of Delete User API. +func HandleDeleteUser( + user *graylog.User, lgc *logic.Logic, + w http.ResponseWriter, r *http.Request, ps httprouter.Params, +) (interface{}, int, error) { + // DELETE /users/{username} Removes a user account + name := ps.ByName("username") + // TODO authorization + sc, err := lgc.DeleteUser(name) + if err != nil { + return nil, sc, err + } + if err := lgc.Save(); err != nil { + return nil, 500, err + } + return nil, sc, nil +} diff --git a/mockserver/handler/user_test.go b/mockserver/handler/user_test.go new file mode 100644 index 00000000..558d9077 --- /dev/null +++ b/mockserver/handler/user_test.go @@ -0,0 +1,185 @@ +package handler_test + +import ( + "bytes" + "net/http" + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" + "github.com/suzuki-shunsuke/go-set" +) + +func TestHandleGetUsers(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + user := testutil.DummyAdmin() + user.Roles = nil + user.Username = "foo" + if _, err := server.AddUser(user); err != nil { + t.Fatal(err) + } + users, _, err := client.GetUsers() + if err != nil { + t.Fatal("Failed to GetUsers", err) + } + if users == nil { + t.Fatal("client.GetUsers() returns nil") + } + if len(users) != 3 { + t.Fatalf("len(users) == %d, wanted 3", len(users)) + } + if users[0].Password != "" { + t.Fatalf( + "users[0].Password == %s, wanted empty", users[0].Password) + } +} + +func TestHandleGetUser(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + exp := testutil.DummyAdmin() + exp.Roles = nil + exp.Username = "foo" + if _, err := server.AddUser(exp); err != nil { + t.Fatal(err) + } + user, _, err := client.GetUser(exp.Username) + if err != nil { + t.Fatal("Failed to GetUser", err) + } + if user.Password != "" { + t.Fatalf("user.Password = %s, wanted empty", user.Password) + } + if _, _, err := client.GetUser(""); err == nil { + t.Fatal("username should be required.") + } + if _, _, err := client.GetUser("h"); err == nil { + t.Fatal(`no user whoname name is "h"`) + } +} + +func TestHandleCreateUser(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + user := testutil.User() + user.Username += "foo" + if _, err := client.CreateUser(user); err != nil { + t.Fatal("Failed to CreateUser", err) + } + if _, err := client.CreateUser(user); err == nil { + t.Fatal("User name must be unique.") + } + + userName := user.Username + user.Username = "" + if _, err := client.CreateUser(user); err == nil { + t.Fatal("Username is required.") + } + user.Username = userName + roleName := "no roles" + user.Roles = set.NewStrSet(roleName) + if _, err := client.CreateUser(user); err == nil { + t.Fatalf("No role found with name %s", roleName) + } + + if _, err := client.CreateUser(nil); err == nil { + t.Fatal("user is nil") + } + + body := bytes.NewBuffer([]byte("hoge")) + req, err := http.NewRequest( + http.MethodPost, client.Endpoints().Users(), body) + if err != nil { + t.Fatal(err) + } + req.SetBasicAuth(client.Name(), client.Password()) + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 400 { + t.Fatalf("resp.StatusCode == %d, wanted 400", resp.StatusCode) + } +} + +func TestHandleUpdateUser(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + user := testutil.DummyAdmin() + user.Roles = nil + userName := "foo" + user.Username = userName + if _, err := server.AddUser(user); err != nil { + t.Fatal(err) + } + user.FullName = "changed!" + if _, err := client.UpdateUser(user.NewUpdateParams()); err != nil { + t.Fatal(err) + } + user.Username = "" + if _, err := client.UpdateUser(user.NewUpdateParams()); err == nil { + t.Fatal("username should be required.") + } + user.Username = "h" + if _, err := client.UpdateUser(user.NewUpdateParams()); err == nil { + t.Fatal(`no user with name is "h"`) + } + if _, err := client.UpdateUser(nil); err == nil { + t.Fatal("user is nil") + } + + body := bytes.NewBuffer([]byte("hoge")) + u, err := client.Endpoints().User(userName) + if err != nil { + t.Fatal(err) + } + req, err := http.NewRequest(http.MethodPut, u.String(), body) + if err != nil { + t.Fatal(err) + } + req.SetBasicAuth(client.Name(), client.Password()) + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != 400 { + t.Fatalf("resp.StatusCode == %d, wanted 400", resp.StatusCode) + } +} + +func TestHandleDeleteUser(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + defer server.Close() + user := testutil.DummyAdmin() + user.Username = "foo" + user.Roles = nil + if _, err := server.AddUser(user); err != nil { + t.Fatal(err) + } + if _, err := client.DeleteUser(user.Username); err != nil { + t.Fatal(err) + } + if _, err := client.DeleteUser(""); err == nil { + t.Fatal("username should be required.") + } + if _, err := client.DeleteUser("h"); err == nil { + t.Fatal(`no user whoname name is "h"`) + } +} diff --git a/mockserver/handler/util.go b/mockserver/handler/util.go new file mode 100644 index 00000000..c8b78087 --- /dev/null +++ b/mockserver/handler/util.go @@ -0,0 +1,61 @@ +package handler + +import ( + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/suzuki-shunsuke/go-set" +) + +type validateReqBodyPrms struct { + Required *set.StrSet + Optional *set.StrSet + Ignored *set.StrSet + Forbidden *set.StrSet + ExtForbidden bool +} + +// validateRequestBody validates a request body and converts it into a map. +func validateRequestBody(b io.Reader, prms *validateReqBodyPrms) (map[string]interface{}, int, error) { + dec := json.NewDecoder(b) + var a interface{} + if err := dec.Decode(&a); err != nil { + return nil, 400, fmt.Errorf( + "failed to parse the request body as JSON: %s", err) + } + body, ok := a.(map[string]interface{}) + if !ok { + return nil, 400, fmt.Errorf( + "failed to parse the request body as a JSON object: %s", a) + } + if prms.Required != nil { + for k := range prms.Required.ToMap(false) { + if _, ok := body[k]; !ok { + return body, 400, fmt.Errorf( + `in the request body the field "%s" is required`, k) + } + } + } + allowedFields := set.NewStrSet() + allowedFields.AddSets(prms.Required, prms.Optional, prms.Ignored) + for k := range body { + if prms.Required != nil && prms.Required.Has(k) { + continue + } + if prms.Optional != nil && prms.Optional.Has(k) { + continue + } + if prms.Ignored != nil && prms.Ignored.Has(k) { + delete(body, k) + continue + } + if prms.Forbidden != nil && prms.Forbidden.Has(k) || prms.ExtForbidden { + return body, 400, fmt.Errorf( + `in the request body an invalid field is found: "%s". The allowed fields: %s`, + k, strings.Join(allowedFields.ToList(), ", ")) + } + } + return body, 200, nil +} diff --git a/mockserver/logic/auth.go b/mockserver/logic/auth.go new file mode 100644 index 00000000..fe1d64c8 --- /dev/null +++ b/mockserver/logic/auth.go @@ -0,0 +1,40 @@ +package logic + +import ( + "fmt" + + "github.com/suzuki-shunsuke/go-graylog" +) + +// Authenticate authenticates a user. +func (lgc *Logic) Authenticate(name, password string) (*graylog.User, int, error) { + if name == "" || password == "" { + return nil, 401, fmt.Errorf("authentication failure") + } + if password == "session" { + // session token is not supported + return nil, 400, fmt.Errorf(`{"message":"mock server doesn't support session token"}`) + } + if password == "token" { + // access token + user, err := lgc.store.GetUserByAccessToken(name) + if err != nil { + return nil, 500, err + } + if user == nil { + return nil, 401, fmt.Errorf("authentication failure") + } + return user, 200, nil + } + user, err := lgc.store.GetUser(name) + if err != nil { + return nil, 500, err + } + if user == nil { + return nil, 401, fmt.Errorf("authentication failure") + } + if user.Password != encryptPassword(password) { + return nil, 401, fmt.Errorf("authentication failure") + } + return user, 200, nil +} diff --git a/mockserver/logic/auth_test.go b/mockserver/logic/auth_test.go new file mode 100644 index 00000000..8856931b --- /dev/null +++ b/mockserver/logic/auth_test.go @@ -0,0 +1,26 @@ +package logic_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" +) + +func TestAuthenticate(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + if _, _, err := lgc.Authenticate("", ""); err == nil { + t.Fatal("name and password are required") + } + if _, _, err := lgc.Authenticate("", "session"); err == nil { + t.Fatal("session token is not supported") + } + if _, _, err := lgc.Authenticate("hoge", "token"); err == nil { + t.Fatal(`token "hoge" should not been found`) + } + if _, _, err := lgc.Authenticate("admin", "admin"); err != nil { + t.Fatal(err) + } +} diff --git a/mockserver/logic/doc.go b/mockserver/logic/doc.go new file mode 100644 index 00000000..dc10bfd1 --- /dev/null +++ b/mockserver/logic/doc.go @@ -0,0 +1,5 @@ +/* +Package logic provides logic layers of Graylog API mock server. +Basically enduser does not use the package directly, but the struct Logic is embed to mockserver.Server . +*/ +package logic diff --git a/mockserver/logic/index_set.go b/mockserver/logic/index_set.go new file mode 100644 index 00000000..cfc41ee9 --- /dev/null +++ b/mockserver/logic/index_set.go @@ -0,0 +1,157 @@ +package logic + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/validator" +) + +// HasIndexSet returns whether the user exists. +func (lgc *Logic) HasIndexSet(id string) (bool, error) { + return lgc.store.HasIndexSet(id) +} + +// GetIndexSets returns a list of all index sets. +func (lgc *Logic) GetIndexSets(skip, limit int) ([]graylog.IndexSet, int, int, error) { + iss, total, err := lgc.store.GetIndexSets(skip, limit) + if err != nil { + return iss, total, 500, err + } + return iss, total, 200, nil +} + +// GetIndexSet returns an index set. +// If an index set is not found, returns an error. +func (lgc *Logic) GetIndexSet(id string) (*graylog.IndexSet, int, error) { + if id == "" { + return nil, 400, fmt.Errorf("index set id is empty") + } + if err := ValidateObjectID(id); err != nil { + // unfortunately graylog returns not 400 but 404. + return nil, 404, err + } + is, err := lgc.store.GetIndexSet(id) + if err != nil { + return is, 500, err + } + if is == nil { + return nil, 404, fmt.Errorf("no index set <%s> is found", id) + } + return is, 200, err +} + +// AddIndexSet adds an index set to the Mock Server. +func (lgc *Logic) AddIndexSet(is *graylog.IndexSet) (int, error) { + // Class org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategy not subtype of [simple type, class org.graylog2.plugin.indexer.rotation.RotationStrategyConfig] (through reference chain: org.graylog2.rest.resources.system.indexer.responses.IndexSetSummary["rotation_strategy"]) + if is == nil { + return 400, fmt.Errorf("index set is nil") + } + // indexPrefix unique check + ok, err := lgc.store.IsConflictIndexPrefix(is.ID, is.IndexPrefix) + if err != nil { + return 500, err + } + if ok { + return 400, fmt.Errorf( + `index prefix "%s" would conflict with an existing index set`, + is.IndexPrefix) + } + is.SetCreateDefaultValues() + if err := validator.CreateValidator.Struct(is); err != nil { + return 400, err + } + is.Default = false + if err := lgc.store.AddIndexSet(is); err != nil { + return 500, err + } + return 200, nil +} + +// UpdateIndexSet updates an index set at the Mock Server. +func (lgc *Logic) UpdateIndexSet(prms *graylog.IndexSetUpdateParams) (*graylog.IndexSet, int, error) { + if prms == nil { + return nil, 400, fmt.Errorf("index set is nil") + } + if err := validator.UpdateValidator.Struct(prms); err != nil { + return nil, 400, err + } + ok, err := lgc.HasIndexSet(prms.ID) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "id": prms.ID, + }).Error("lgc.HasIndexSet() is failure") + return nil, 500, err + } + if !ok { + return nil, 404, fmt.Errorf("no indexSet found with id <%s>", prms.ID) + } + // indexPrefix unique check + ok, err = lgc.store.IsConflictIndexPrefix(prms.ID, prms.IndexPrefix) + if err != nil { + return nil, 500, err + } + if ok { + return nil, 400, fmt.Errorf( + `index prefix "%s" would conflict with an existing index set`, + prms.IndexPrefix) + } + defID, err := lgc.store.GetDefaultIndexSetID() + if err != nil { + return nil, 500, err + } + if defID == prms.ID && prms.Writable != nil && !(*prms.Writable) { + return nil, 409, fmt.Errorf("default index set must be writable") + } + + is, err := lgc.store.UpdateIndexSet(prms) + if err != nil { + return nil, 500, err + } + return is, 200, nil +} + +// DeleteIndexSet removes a index set from the Mock Server. +func (lgc *Logic) DeleteIndexSet(id string) (int, error) { + ok, err := lgc.HasIndexSet(id) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "id": id, + }).Error("lgc.HasIndexSet() is failure") + return 500, err + } + if !ok { + return 404, fmt.Errorf("no indexSet with id <%s> is not found", id) + } + defID, err := lgc.store.GetDefaultIndexSetID() + if err != nil { + return 500, err + } + if id == defID { + return 400, fmt.Errorf("default index set <%s> cannot be deleted", id) + } + if err := lgc.store.DeleteIndexSet(id); err != nil { + return 500, err + } + return 204, nil +} + +// SetDefaultIndexSet sets a default index set +func (lgc *Logic) SetDefaultIndexSet(id string) (*graylog.IndexSet, int, error) { + is, sc, err := lgc.GetIndexSet(id) + if err != nil { + return nil, sc, err + } + if is == nil { + return nil, 404, fmt.Errorf("no indexSet found with id <%s>", id) + } + if !is.Writable { + return nil, 409, fmt.Errorf("default index set must be writable") + } + if err := lgc.store.SetDefaultIndexSetID(id); err != nil { + return nil, 500, err + } + is.Default = true + return is, 200, nil +} diff --git a/mockserver/logic/index_set_stats.go b/mockserver/logic/index_set_stats.go new file mode 100644 index 00000000..8f4cf63f --- /dev/null +++ b/mockserver/logic/index_set_stats.go @@ -0,0 +1,58 @@ +package logic + +import ( + "github.com/suzuki-shunsuke/go-graylog" +) + +// GetIndexSetStats returns an index set stats. +func (lgc *Logic) GetIndexSetStats(id string) (*graylog.IndexSetStats, int, error) { + ok, err := lgc.HasIndexSet(id) + if err != nil { + return nil, 500, err + } + if !ok { + return nil, 404, nil + } + stats, err := lgc.store.GetIndexSetStats(id) + if err != nil { + return nil, 500, err + } + if stats == nil { + return &graylog.IndexSetStats{}, 200, err + } + return stats, 200, nil +} + +// GetTotalIndexSetStats returns all index set's statistics. +func (lgc *Logic) GetTotalIndexSetStats() (*graylog.IndexSetStats, int, error) { + stats, err := lgc.store.GetTotalIndexSetStats() + if err != nil { + return stats, 500, err + } + return stats, 200, nil +} + +// GetIndexSetStatsMap returns a each Index Set's statistics. +func (lgc *Logic) GetIndexSetStatsMap() (map[string]graylog.IndexSetStats, int, error) { + m, err := lgc.store.GetIndexSetStatsMap() + if err != nil { + return m, 500, err + } + return m, 200, err +} + +// SetIndexSetStats sets an index set stats to a index set. +// func (lgc *Logic) SetIndexSetStats(id string, stats *graylog.IndexSetStats) (int, error) { +// ok, err := lgc.HasIndexSet(id) +// if err != nil { +// return 500, err +// } +// if !ok { +// return 404, fmt.Errorf("no index set with id <%s> is found", id) +// } +// +// if err := lgc.store.SetIndexSetStats(id, stats); err != nil { +// return 500, err +// } +// return 200, nil +// } diff --git a/mockserver/logic/index_set_stats_test.go b/mockserver/logic/index_set_stats_test.go new file mode 100644 index 00000000..fe3b6d49 --- /dev/null +++ b/mockserver/logic/index_set_stats_test.go @@ -0,0 +1,35 @@ +package logic_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" +) + +func TestGetIndexSetStats(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + iss, _, _, err := lgc.GetIndexSets(0, 0) + if err != nil { + t.Fatal(err) + } + if len(iss) == 0 { + t.Fatal("len(iss) == 0") + } + is := iss[0] + if _, _, err := lgc.GetIndexSetStats(is.ID); err != nil { + t.Fatal(err) + } +} + +func TestGetTotalIndexSetStats(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + if _, _, err := lgc.GetTotalIndexSetStats(); err != nil { + t.Fatal(err) + } +} diff --git a/mockserver/logic/index_set_test.go b/mockserver/logic/index_set_test.go new file mode 100644 index 00000000..1552bdd6 --- /dev/null +++ b/mockserver/logic/index_set_test.go @@ -0,0 +1,190 @@ +package logic_test + +import ( + "reflect" + "testing" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/testutil" + "github.com/suzuki-shunsuke/go-ptr" +) + +func TestAddIndexSet(t *testing.T) { + server, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet("hoge") + if _, err := server.AddIndexSet(is); err != nil { + t.Fatal(err) + } + is.ID = "" + if _, err := server.AddIndexSet(is); err == nil { + t.Fatal("index prefix should conflict") + } +} + +func TestGetIndexSets(t *testing.T) { + server, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + indexSets, _, _, err := server.GetIndexSets(0, 0) + if err != nil { + t.Fatal("Failed to GetIndexSets", err) + } + if indexSets == nil { + t.Fatal("indexSets == nil") + } + if len(indexSets) != 1 { + t.Fatalf("len(indexSets) == %d, wanted %d", len(indexSets), 1) + } +} + +func TestGetIndexSet(t *testing.T) { + server, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + is := testutil.IndexSet("hoge") + if _, err := server.AddIndexSet(is); err != nil { + t.Fatal(err) + } + act, _, err := server.GetIndexSet(is.ID) + if err != nil { + t.Fatal("Failed to GetIndexSet", err) + } + if !reflect.DeepEqual(*act, *is) { + t.Fatalf("server.GetIndexSet() == %v, wanted %v", act, is) + } + if _, _, err = server.GetIndexSet(""); err == nil { + t.Fatal("index set id is empty") + } + if _, _, err = server.GetIndexSet("h"); err == nil { + t.Fatal("no index set is found") + } +} + +func TestUpdateIndexSet(t *testing.T) { + server, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet("fuga") + if _, err := server.AddIndexSet(is); err != nil { + t.Fatal(err) + } + prms := is.NewUpdateParams() + prms.Description = ptr.PStr("changed!") + if _, _, err := server.UpdateIndexSet(prms); err != nil { + t.Fatal("UpdateIndexSet is failure", err) + } + prms.ID = "" + if _, _, err := server.UpdateIndexSet(prms); err == nil { + t.Fatal("index set id is required") + } + prms.ID = "h" + if _, _, err := server.UpdateIndexSet(prms); err == nil { + t.Fatal(`no index set whose id is "h"`) + } + prms.ID = is.ID + prms.Title = "" + if _, _, err := server.UpdateIndexSet(prms); err == nil { + t.Fatal("title is required") + } + if _, _, err := server.UpdateIndexSet(nil); err == nil { + t.Fatal("index set is nil") + } +} + +func TestDeleteIndexSet(t *testing.T) { + server, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + indexSets, _, _, err := server.GetIndexSets(0, 0) + if err != nil { + t.Fatal(err) + } + indexSet := indexSets[0] + if _, err = server.DeleteIndexSet(indexSet.ID); err == nil { + t.Fatal("default index set should not be deleted") + } + is := testutil.IndexSet("hoge") + if _, err := server.AddIndexSet(is); err != nil { + t.Fatal(err) + } + if _, err := server.DeleteIndexSet(is.ID); err != nil { + t.Fatal("Failed to DeleteIndexSet", err) + } + if _, err := server.DeleteIndexSet(""); err == nil { + t.Fatal("index set id is required") + } + if _, err := server.DeleteIndexSet("h"); err == nil { + t.Fatal(`no index set whose id is "h"`) + } +} + +func TestSetDefaultIndexSet(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + iss, _, _, _, err := client.GetIndexSets(0, 0, false) + if err != nil { + t.Fatal(err) + } + var defIs, is *graylog.IndexSet + for _, i := range iss { + if i.Default { + defIs = &i + } else { + is = &i + } + } + if is == nil { + is = testutil.IndexSet("hoge") + is.Default = false + is.Writable = true + if _, err := client.CreateIndexSet(is); err != nil { + t.Fatal(err) + } + testutil.WaitAfterCreateIndexSet(server) + defer func(id string) { + if _, err := client.DeleteIndexSet(id); err != nil { + t.Fatal(err) + } + testutil.WaitAfterDeleteIndexSet(server) + }(is.ID) + } + is, _, err = client.SetDefaultIndexSet(is.ID) + if err != nil { + t.Fatal("Failed to UpdateIndexSet", err) + } + defer func(id string) { + if _, _, err = client.SetDefaultIndexSet(id); err != nil { + t.Fatal(err) + } + }(defIs.ID) + if !is.Default { + t.Fatal("updatedIndexSet.Default == false") + } + if _, _, err := client.SetDefaultIndexSet(""); err == nil { + t.Fatal("index set id is required") + } + if _, _, err := client.SetDefaultIndexSet("h"); err == nil { + t.Fatal(`no index set whose id is "h"`) + } + + prms := is.NewUpdateParams() + prms.Writable = ptr.PBool(false) + + if _, _, err := client.UpdateIndexSet(prms); err == nil { + t.Fatal("Default index set must be writable.") + } +} diff --git a/mockserver/logic/input.go b/mockserver/logic/input.go new file mode 100644 index 00000000..783e62a4 --- /dev/null +++ b/mockserver/logic/input.go @@ -0,0 +1,97 @@ +package logic + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/validator" +) + +// HasInput returns whether the input exists. +func (lgc *Logic) HasInput(id string) (bool, error) { + return lgc.store.HasInput(id) +} + +// GetInput returns an input. +// If an input is not found, returns an error. +func (lgc *Logic) GetInput(id string) (*graylog.Input, int, error) { + if id == "" { + return nil, 400, fmt.Errorf("input id is empty") + } + if err := ValidateObjectID(id); err != nil { + // unfortunately graylog returns not 400 but 404. + return nil, 404, err + } + input, err := lgc.store.GetInput(id) + if err != nil { + return input, 500, err + } + if input == nil { + return nil, 404, fmt.Errorf("no input with id <%s> is found", id) + } + return input, 200, nil +} + +// AddInput adds an input to the mock server. +func (lgc *Logic) AddInput(input *graylog.Input) (int, error) { + if err := validator.CreateValidator.Struct(input); err != nil { + return 400, err + } + if err := lgc.store.AddInput(input); err != nil { + return 500, err + } + return 200, nil +} + +// UpdateInput updates an input at the Server. +// Required: Title, Type, Attrs +// Allowed: Global, Node +func (lgc *Logic) UpdateInput(prms *graylog.InputUpdateParams) (*graylog.Input, int, error) { + if prms == nil { + return nil, 400, fmt.Errorf("input is nil") + } + if err := validator.UpdateValidator.Struct(prms); err != nil { + return nil, 400, err + } + ok, err := lgc.HasInput(prms.ID) + if err != nil { + return nil, 500, err + } + if !ok { + return nil, 404, fmt.Errorf("the input <%s> is not found", prms.ID) + } + + input, err := lgc.store.UpdateInput(prms) + if err != nil { + return nil, 500, err + } + return input, 200, nil +} + +// DeleteInput deletes a input from the mock server. +func (lgc *Logic) DeleteInput(id string) (int, error) { + ok, err := lgc.HasInput(id) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "id": id, + }).Error("lgc.HasInput() is failure") + return 500, err + } + if !ok { + return 404, fmt.Errorf("the input <%s> is not found", id) + } + if err := lgc.store.DeleteInput(id); err != nil { + return 500, err + } + return 200, nil +} + +// GetInputs returns a list of inputs. +func (lgc *Logic) GetInputs() ([]graylog.Input, int, int, error) { + inputs, total, err := lgc.store.GetInputs() + if err != nil { + return nil, 0, 500, err + } + return inputs, total, 200, nil +} diff --git a/mockserver/logic/input_test.go b/mockserver/logic/input_test.go new file mode 100644 index 00000000..216204eb --- /dev/null +++ b/mockserver/logic/input_test.go @@ -0,0 +1,170 @@ +package logic_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestAddInput(t *testing.T) { + server, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + input := testutil.Input() + if _, err := server.AddInput(input); err != nil { + t.Fatal("Failed to AddInput", err) + } + if input.ID == "" { + t.Fatal(`server.AddInput() == ""`) + } + + if _, err := server.AddInput(nil); err == nil { + t.Fatal("input is nil") + } +} + +func TestGetInputs(t *testing.T) { + server, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + act, _, _, err := server.GetInputs() + if err != nil { + t.Fatal("Failed to GetInputs", err) + } + if act == nil { + t.Fatal("server.GetInputs() returns nil") + } + if len(act) != 1 { + t.Fatalf("len(act) == %d, wanted 1", len(act)) + } +} + +func TestGetInput(t *testing.T) { + server, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + input := testutil.Input() + if _, err := server.AddInput(input); err != nil { + t.Fatal(err) + } + act, _, err := server.GetInput(input.ID) + if err != nil { + t.Fatal("Failed to GetInput", err) + } + if input.Node != act.Node { + t.Fatalf("Node == %s, wanted %s", act.Node, input.Node) + } + + if _, _, err := server.GetInput(""); err == nil { + t.Fatal("input id is required") + } + + if _, _, err := server.GetInput("h"); err == nil { + t.Fatal(`no input whose id is "h"`) + } +} + +func TestUpdateInput(t *testing.T) { + server, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + input := testutil.Input() + if _, err := server.AddInput(input); err != nil { + t.Fatal(err) + } + id := input.ID + t.Run("normal", func(t *testing.T) { + input.Title += " updated" + if _, _, err := server.UpdateInput(input.NewUpdateParams()); err != nil { + t.Fatal(err) + } + act, _, err := server.GetInput(input.ID) + if err != nil { + t.Fatal(err) + } + if act == nil { + t.Fatal("input is not found") + } + if act.Title != input.Title { + t.Fatalf(`UpdateInput title "%s" != "%s"`, act.Title, input.Title) + } + }) + t.Run("id is required", func(t *testing.T) { + input := testutil.Input() + if _, _, err := server.UpdateInput(input.NewUpdateParams()); err == nil { + t.Fatal("input id is required") + } + }) + t.Run("invalid id", func(t *testing.T) { + input := testutil.Input() + input.ID = "h" + if _, _, err := server.UpdateInput(input.NewUpdateParams()); err == nil { + t.Fatal(`no input whose id is "h"`) + } + }) + t.Run("attributes is required", func(t *testing.T) { + input := testutil.Input() + input.ID = id + input.Attrs = nil + if _, _, err := server.UpdateInput(input.NewUpdateParams()); err == nil { + t.Fatal("input attributes is required") + } + }) + t.Run("type is required", func(t *testing.T) { + input := testutil.Input() + input.ID = id + input.Title = "" + if _, _, err := server.UpdateInput(input.NewUpdateParams()); err == nil { + t.Fatal("input title is required") + } + }) + t.Run("beats's bind_address is required", func(t *testing.T) { + input := testutil.Input() + input.ID = id + switch input.Type() { + case graylog.InputTypeBeats: + attrs, ok := input.Attrs.(*graylog.InputBeatsAttrs) + if !ok { + t.Fatal("input.Attrs's type assertion is failure") + } + attrs.BindAddress = "" + input.Attrs = attrs + if _, _, err := server.UpdateInput(input.NewUpdateParams()); err == nil { + t.Fatal("input bind_address is required") + } + } + }) + t.Run("input is required", func(t *testing.T) { + if _, _, err := server.UpdateInput(nil); err == nil { + t.Fatal("input is required") + } + }) +} + +func TestDeleteInput(t *testing.T) { + server, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + input := testutil.Input() + if _, err = server.AddInput(input); err != nil { + t.Fatal(err) + } + if _, err = server.DeleteInput(input.ID); err != nil { + t.Fatal("Failed to DeleteInput", err) + } + + if _, err := server.DeleteInput(""); err == nil { + t.Fatal("input id is required") + } + + if _, err := server.DeleteInput("h"); err == nil { + t.Fatal(`no input whose id is "h"`) + } +} diff --git a/mockserver/logic/logic.go b/mockserver/logic/logic.go new file mode 100644 index 00000000..10aa76ff --- /dev/null +++ b/mockserver/logic/logic.go @@ -0,0 +1,113 @@ +package logic + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/store" + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" +) + +// Logic represents a mock of the Graylog API. +// This is embedded to mockserver.Server. +type Logic struct { + authEnabled bool + streamRules map[string]map[string]graylog.StreamRule + + store store.Store + logger *log.Logger +} + +// Logger returns a logger. +// This logger is logrus.Logger . +// https://github.com/sirupsen/logrus +// You can change the Logic's logger configuration freely. +// +// lgc := logic.NewLogic(nil) +// logger := lgc.Logger() +// logger.SetFormatter(&log.JSONFormatter{}) +// logger.SetLevel(log.WarnLevel) +func (lgc *Logic) Logger() *log.Logger { + return lgc.logger +} + +// NewLogic returns new Server. +// The argument `store` is the store which the server uses. +// If `store` is nil, the default plain store is used and data is not persisted. +func NewLogic(store store.Store) (*Logic, error) { + if store == nil { + store = plain.NewStore("") + } + lgc := &Logic{ + // indexSetStats: map[string]graylog.IndexSetStats{}, + streamRules: map[string]map[string]graylog.StreamRule{}, + + store: store, + logger: log.New(), + // By default the authentication is enabled + authEnabled: true, + } + // By Default logLevel is warn, + // because debug and info logs are often noisy at unit tests. + lgc.logger.SetLevel(log.WarnLevel) + err := lgc.InitData() + return lgc, err +} + +// SetStore sets a store to the mock server. +func (lgc *Logic) SetStore(store store.Store) { + lgc.store = store +} + +// Save writes Mock Server's data in a file for persistence. +func (lgc *Logic) Save() error { + return lgc.store.Save() +} + +// Load reads Mock Server's data from a file. +func (lgc *Logic) Load() error { + return lgc.store.Load() +} + +// SetAuth sets whether the authentication and authentication are enabled. +// Disable the authentication. +// +// lgc.SetAuth(false) +// +// Enable the authentication. +// +// lgc.SetAuth(true) +func (lgc *Logic) SetAuth(authEnabled bool) { + lgc.authEnabled = authEnabled +} + +// Auth returns whether the authentication and authentication are enabled. +func (lgc *Logic) Auth() bool { + return lgc.authEnabled +} + +// Authorize authorizes a user. +// If the user doesn't have the permission, an error is returned. +// +// // whether the user has the permission to read all roles +// if sc, err := lgc.Authorize(user, "roles:read", ""); err != nil { +// fmt.Println(sc, err) // 403, "authorization failure" +// } +// +// // whether the user has the permission to read the role "foo" +// sc, err := lgc.Authorize(admin, "roles:read", "foo") +// fmt.Println(sc, err) // 200, nil +func (lgc *Logic) Authorize(user *graylog.User, scope string, args ...string) (int, error) { + if user == nil { + return 200, nil + } + ok, err := lgc.store.Authorize(user, scope, args...) + if err != nil { + return 500, err + } + if ok { + return 200, nil + } + return 403, fmt.Errorf("authorization failure") +} diff --git a/mockserver/logic/logic_test.go b/mockserver/logic/logic_test.go new file mode 100644 index 00000000..28795773 --- /dev/null +++ b/mockserver/logic/logic_test.go @@ -0,0 +1,92 @@ +package logic_test + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" +) + +func TestLogger(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + if logger := lgc.Logger(); logger == nil { + t.Fatal("logger is nil") + } +} + +func TestSave(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) + lgc.SetStore(plain.NewStore(tmpfile.Name())) + if err := lgc.Save(); err != nil { + t.Fatal(err) + } + if err := lgc.Load(); err != nil { + t.Fatal(err) + } +} + +func TestLoad(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + if err := lgc.Load(); err != nil { + t.Fatal(err) + } + lgc.SetStore(plain.NewStore("hoge")) + if err := lgc.Load(); err != nil { + t.Fatal(err) + } +} + +func TestSetAuth(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + lgc.SetAuth(true) + if !lgc.Auth() { + t.Fatal("auth should be true") + } + lgc.SetAuth(false) + if lgc.Auth() { + t.Fatal("auth should be false") + } +} + +func TestAuthorize(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + if _, err := lgc.Authorize(nil, "users:read", "admin"); err != nil { + t.Fatal(err) + } + user, _, err := lgc.GetUser("admin") + if err != nil { + t.Fatal(err) + } + if _, err := lgc.Authorize(user, "users:read", "admin"); err != nil { + t.Fatal(err) + } + nobody, _, err := lgc.GetUser("nobody") + if err != nil { + t.Fatal(err) + } + if _, err := lgc.Authorize(nobody, "users:read", "admin"); err == nil { + t.Fatal("authorization should be failure") + } +} diff --git a/mockserver/logic/role.go b/mockserver/logic/role.go new file mode 100644 index 00000000..bef58c7a --- /dev/null +++ b/mockserver/logic/role.go @@ -0,0 +1,99 @@ +package logic + +import ( + "fmt" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/validator" +) + +// HasRole returns whether the role with given name exists. +func (lgc *Logic) HasRole(name string) (bool, error) { + return lgc.store.HasRole(name) +} + +// GetRole returns a Role. +// If a role is not found, an error is returns. +func (lgc *Logic) GetRole(name string) (*graylog.Role, int, error) { + role, err := lgc.store.GetRole(name) + if err != nil { + return role, 500, err + } + if role == nil { + return nil, 404, fmt.Errorf(`no role with name "%s"`, name) + } + return role, 200, nil +} + +// AddRole adds a new role. +func (lgc *Logic) AddRole(role *graylog.Role) (int, error) { + if err := validator.CreateValidator.Struct(role); err != nil { + return 400, err + } + ok, err := lgc.HasRole(role.Name) + if err != nil { + return 500, err + } + if ok { + return 400, fmt.Errorf("role %s already exists", role.Name) + } + if role.Name != "Admin" && role.Name != "Reader" { + role.ReadOnly = false + } + if err := lgc.store.AddRole(role); err != nil { + return 500, err + } + return 200, nil +} + +// UpdateRole updates a role. +func (lgc *Logic) UpdateRole(name string, prms *graylog.RoleUpdateParams) (*graylog.Role, int, error) { + if err := validator.UpdateValidator.Struct(prms); err != nil { + return nil, 400, err + } + role, sc, err := lgc.GetRole(name) + if err != nil { + return nil, sc, err + } + if name != prms.Name { + ok, err := lgc.HasRole(prms.Name) + if err != nil { + return nil, 500, err + } + if ok { + return nil, 400, fmt.Errorf("the role %s has already existed", prms.Name) + } + } + if role.ReadOnly { + return nil, 400, fmt.Errorf("cannot update read only role %s", role.Name) + } + role, err = lgc.store.UpdateRole(name, prms) + if err != nil { + return nil, 500, err + } + return role, 204, nil +} + +// DeleteRole deletes a role. +func (lgc *Logic) DeleteRole(name string) (int, error) { + role, sc, err := lgc.GetRole(name) + if err != nil { + return sc, err + } + if role.ReadOnly { + return 400, fmt.Errorf("cannot delete read only role %s", name) + } + if err := lgc.store.DeleteRole(name); err != nil { + return 500, err + } + return 200, nil +} + +// GetRoles returns a list of roles. +func (lgc *Logic) GetRoles() ([]graylog.Role, int, int, error) { + roles, total, err := lgc.store.GetRoles() + if err != nil { + return nil, 0, 500, err + } + return roles, total, 200, nil +} diff --git a/mockserver/logic/role_member.go b/mockserver/logic/role_member.go new file mode 100644 index 00000000..fcce755b --- /dev/null +++ b/mockserver/logic/role_member.go @@ -0,0 +1,96 @@ +package logic + +import ( + "fmt" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-set" +) + +const ( + adminName string = "admin" +) + +// RoleMembers returns members of a given role. +func (lgc *Logic) RoleMembers(name string) ([]graylog.User, int, error) { + ok, err := lgc.HasRole(name) + if err != nil { + return nil, 500, err + } + if !ok { + return nil, 404, fmt.Errorf("no role found with name %s", name) + } + users := []graylog.User{} + us, sc, err := lgc.GetUsers() + if err != nil { + return us, sc, err + } + for _, user := range us { + if user.Roles == nil { + continue + } + for roleName := range user.Roles.ToMap(false) { + if roleName == name { + users = append(users, user) + break + } + } + } + return users, 200, nil +} + +// AddUserToRole adds a user to a role. +func (lgc *Logic) AddUserToRole(userName, roleName string) (int, error) { + ok, err := lgc.HasRole(roleName) + if err != nil { + return 500, err + } + if !ok { + return 404, fmt.Errorf("no role found with name %s", roleName) + } + + if userName == adminName { + return 500, fmt.Errorf("cannot modify local root user, this is a bug") + } + user, sc, err := lgc.GetUser(userName) + if err != nil { + return sc, err + } + if user == nil { + return 404, fmt.Errorf("no user found with name %s", userName) + } + if user.Roles == nil { + user.Roles = set.NewStrSet(roleName) + } else { + user.Roles.Add(roleName) + } + return lgc.UpdateUser(user.NewUpdateParams()) +} + +// RemoveUserFromRole removes a user from a role. +func (lgc *Logic) RemoveUserFromRole( + userName, roleName string, +) (int, error) { + ok, err := lgc.HasRole(roleName) + if err != nil { + return 500, err + } + if !ok { + return 404, fmt.Errorf(`no role found with name "%s"`, roleName) + } + + if userName == adminName { + return 500, fmt.Errorf("cannot modify local root user, this is a bug") + } + user, sc, err := lgc.GetUser(userName) + if err != nil { + return sc, err + } + if user == nil { + return 404, fmt.Errorf("no user found with name %s", userName) + } + if user.Roles != nil { + user.Roles.Remove(roleName) + } + return lgc.UpdateUser(user.NewUpdateParams()) +} diff --git a/mockserver/logic/role_member_test.go b/mockserver/logic/role_member_test.go new file mode 100644 index 00000000..a4841d7b --- /dev/null +++ b/mockserver/logic/role_member_test.go @@ -0,0 +1,66 @@ +package logic_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" +) + +func TestRoleMembers(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + t.Run("basic", func(t *testing.T) { + if _, _, err := lgc.RoleMembers("Admin"); err != nil { + t.Fatal(err) + } + }) + t.Run("name is required", func(t *testing.T) { + if _, _, err := lgc.RoleMembers(""); err == nil { + t.Fatal("name is required") + } + }) + t.Run("not found", func(t *testing.T) { + if _, _, err := lgc.RoleMembers("h"); err == nil { + t.Fatal(`no role with name "h"`) + } + }) +} + +func TestAddUserToRole(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + t.Run("user name is required", func(t *testing.T) { + if _, err := lgc.AddUserToRole("", "Admin"); err == nil { + t.Fatal("user name is required") + } + }) + t.Run("role name is required", func(t *testing.T) { + if _, err := lgc.AddUserToRole("admin", ""); err == nil { + t.Fatal("role name is required") + } + }) +} + +func TestRemoveUserFromRole(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + t.Run("user name is required", func(t *testing.T) { + if _, err := lgc.RemoveUserFromRole("", "Admin"); err == nil { + t.Fatal("user name is required") + } + }) + t.Run("role name is required", func(t *testing.T) { + if _, err := lgc.RemoveUserFromRole("admin", ""); err == nil { + t.Fatal("role name is required") + } + }) +} diff --git a/mockserver/logic/role_test.go b/mockserver/logic/role_test.go new file mode 100644 index 00000000..78adf98b --- /dev/null +++ b/mockserver/logic/role_test.go @@ -0,0 +1,138 @@ +package logic_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestAddRole(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + t.Run("basic", func(t *testing.T) { + role := testutil.Role() + if _, err := lgc.AddRole(role); err != nil { + t.Fatal(err) + } + }) + t.Run("name is required", func(t *testing.T) { + role := testutil.Role() + role.Name = "" + if _, err := lgc.AddRole(role); err == nil { + t.Fatal("role name is required") + } + }) +} + +func TestGetRoles(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + roles, _, _, err := lgc.GetRoles() + if err != nil { + t.Fatal(err) + } + if len(roles) == 0 { + t.Fatal("len(roles) == 0") + } +} + +func TestGetRole(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + t.Run("basic", func(t *testing.T) { + if _, _, err := lgc.GetRole("Admin"); err != nil { + t.Fatal(err) + } + }) + t.Run("name is required", func(t *testing.T) { + if _, _, err := lgc.GetRole(""); err == nil { + t.Fatal("name is required") + } + }) + t.Run("not found", func(t *testing.T) { + if _, _, err := lgc.GetRole("h"); err == nil { + t.Fatal(`no role with name "h"`) + } + }) +} + +func TestUpdateRole(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + role := testutil.Role() + if _, err := lgc.AddRole(role); err != nil { + t.Fatal(err) + } + name := role.Name + + t.Run("basic", func(t *testing.T) { + role.Description += " changed!" + if _, _, err := lgc.UpdateRole(name, role.NewUpdateParams()); err != nil { + t.Fatal(err) + } + }) + t.Run("name is required", func(t *testing.T) { + if _, _, err := lgc.UpdateRole("", role.NewUpdateParams()); err == nil { + t.Fatal("name is required") + } + }) + t.Run("name is required", func(t *testing.T) { + role.Name = "" + if _, _, err := lgc.UpdateRole(name, role.NewUpdateParams()); err == nil { + t.Fatal("name is required") + } + }) + t.Run("not found", func(t *testing.T) { + role.Name = name + if _, _, err := lgc.UpdateRole("h", role.NewUpdateParams()); err == nil { + t.Fatal("not found") + } + }) + t.Run("permissions is required", func(t *testing.T) { + role.Permissions = nil + if _, _, err := lgc.UpdateRole(name, role.NewUpdateParams()); err == nil { + t.Fatal("permissions is required") + } + }) + t.Run("nil", func(t *testing.T) { + if _, _, err := lgc.UpdateRole(name, nil); err == nil { + t.Fatal("role is nil") + } + }) +} + +func TestDeleteRole(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + role := testutil.Role() + if _, err := lgc.AddRole(role); err != nil { + t.Fatal(err) + } + t.Run("basic", func(t *testing.T) { + if _, err := lgc.DeleteRole(role.Name); err != nil { + t.Fatal(err) + } + }) + t.Run("name is required", func(t *testing.T) { + if _, err := lgc.DeleteRole(""); err == nil { + t.Fatal("name is required") + } + }) + t.Run("not found", func(t *testing.T) { + if _, err := lgc.DeleteRole("h"); err == nil { + t.Fatal("not found") + } + }) +} diff --git a/mockserver/logic/seed_data.go b/mockserver/logic/seed_data.go new file mode 100644 index 00000000..69ce71bc --- /dev/null +++ b/mockserver/logic/seed_data.go @@ -0,0 +1,37 @@ +package logic + +import ( + "github.com/suzuki-shunsuke/go-graylog/mockserver/seed" +) + +// InitData sets an initial data. +func (lgc *Logic) InitData() error { + role := seed.Role() + if _, err := lgc.AddRole(role); err != nil { + return err + } + if _, err := lgc.AddUser(seed.User()); err != nil { + return err + } + if _, err := lgc.AddUser(seed.Nobody()); err != nil { + return err + } + lgc.AddInput(seed.Input()) + is := seed.IndexSet() + if _, err := lgc.AddIndexSet(is); err != nil { + return err + } + is, _, err := lgc.SetDefaultIndexSet(is.ID) + if err != nil { + return err + } + stream := seed.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + return err + } + rule := seed.StreamRule() + rule.StreamID = stream.ID + _, err = lgc.AddStreamRule(rule) + return err +} diff --git a/mockserver/logic/stream.go b/mockserver/logic/stream.go new file mode 100644 index 00000000..1ecc0121 --- /dev/null +++ b/mockserver/logic/stream.go @@ -0,0 +1,152 @@ +package logic + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/validator" +) + +// HasStream returns whether the stream exists. +func (lgc *Logic) HasStream(id string) (bool, error) { + return lgc.store.HasStream(id) +} + +// GetStream returns a stream. +func (lgc *Logic) GetStream(id string) (*graylog.Stream, int, error) { + if err := ValidateObjectID(id); err != nil { + // unfortunately graylog returns not 400 but 404. + return nil, 404, err + } + stream, err := lgc.store.GetStream(id) + if err != nil { + return nil, 500, err + } + if stream == nil { + return nil, 404, fmt.Errorf("no stream found with id <%s>", id) + } + return stream, 200, nil +} + +// AddStream adds a stream to the Server. +func (lgc *Logic) AddStream(stream *graylog.Stream) (int, error) { + if err := validator.CreateValidator.Struct(stream); err != nil { + return 400, err + } + // check index set existence + is, sc, err := lgc.GetIndexSet(stream.IndexSetID) + if err != nil { + LogWE(sc, lgc.Logger().WithFields(log.Fields{ + "error": err, "index_set_id": stream.IndexSetID, "status_code": sc, + }), "failed to get an index set") + return sc, err + } + if !is.Writable { + return 400, fmt.Errorf("assigned index set must be writable") + } + if err := lgc.store.AddStream(stream); err != nil { + return 500, err + } + return 200, nil +} + +// UpdateStream updates a stream at the Server. +func (lgc *Logic) UpdateStream(prms *graylog.StreamUpdateParams) (*graylog.Stream, int, error) { + if prms == nil { + return nil, 400, fmt.Errorf("stream is nil") + } + if err := validator.UpdateValidator.Struct(prms); err != nil { + return nil, 400, err + } + stream, sc, err := lgc.GetStream(prms.ID) + if err != nil { + LogWE(sc, lgc.Logger().WithFields(log.Fields{ + "error": err, "id": prms.ID, "status_code": sc, + }), "failed to get a stream") + return nil, sc, err + } + if stream.IsDefault { + return nil, 400, fmt.Errorf("the default stream cannot be edited") + } + // check index set existence + if prms.IndexSetID != "" { + is, sc, err := lgc.GetIndexSet(prms.IndexSetID) + if err != nil { + LogWE(sc, lgc.Logger().WithFields(log.Fields{ + "error": err, "index_set_id": prms.IndexSetID, "status_code": sc, + }), "failed to get an index set") + return nil, sc, err + } + if !is.Writable { + return nil, 400, fmt.Errorf("assigned index set must be writable") + } + } + s, err := lgc.store.UpdateStream(prms) + if err != nil { + return nil, 500, err + } + return s, 200, nil +} + +// DeleteStream deletes a stream from the Server. +func (lgc *Logic) DeleteStream(id string) (int, error) { + ok, err := lgc.HasStream(id) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "id": id, + }).Error("lgc.HasStream() is failure") + return 500, err + } + if !ok { + return 404, fmt.Errorf("no stream found with id <%s>", id) + } + if err := lgc.store.DeleteStream(id); err != nil { + return 500, err + } + return 200, nil +} + +// GetStreams returns a list of all streams. +func (lgc *Logic) GetStreams() ([]graylog.Stream, int, int, error) { + streams, total, err := lgc.store.GetStreams() + if err != nil { + return nil, 0, 500, err + } + return streams, total, 200, nil +} + +// GetEnabledStreams returns all enabled streams. +func (lgc *Logic) GetEnabledStreams() ([]graylog.Stream, int, int, error) { + streams, total, err := lgc.store.GetEnabledStreams() + if err != nil { + return nil, 0, 500, err + } + return streams, total, 200, nil +} + +// PauseStream pauses a stream. +func (lgc *Logic) PauseStream(id string) (int, error) { + ok, err := lgc.HasStream(id) + if err != nil { + return 500, err + } + if !ok { + return 404, fmt.Errorf("no stream found with id <%s>", id) + } + // TODO pause + return 200, nil +} + +// ResumeStream resumes a stream. +func (lgc *Logic) ResumeStream(id string) (int, error) { + ok, err := lgc.HasStream(id) + if err != nil { + return 500, err + } + if !ok { + return 404, fmt.Errorf("no stream found with id <%s>", id) + } + // TODO resume + return 200, nil +} diff --git a/mockserver/logic/stream_rule.go b/mockserver/logic/stream_rule.go new file mode 100644 index 00000000..28c074ad --- /dev/null +++ b/mockserver/logic/stream_rule.go @@ -0,0 +1,135 @@ +package logic + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/validator" +) + +// HasStreamRule returns whether the stream sule exists. +func (lgc *Logic) HasStreamRule(streamID, streamRuleID string) (bool, error) { + return lgc.store.HasStreamRule(streamID, streamRuleID) +} + +// AddStreamRule adds a stream rule to the Server. +func (lgc *Logic) AddStreamRule(rule *graylog.StreamRule) (int, error) { + if err := validator.CreateValidator.Struct(rule); err != nil { + return 400, err + } + + s, sc, err := lgc.GetStream(rule.StreamID) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "id": rule.StreamID, "sc": sc, + }).Warn("failed to get a stream") + return sc, err + } + if s.IsDefault { + return 400, fmt.Errorf("cannot add stream rules to the default stream") + } + + if err := lgc.store.AddStreamRule(rule); err != nil { + return 500, err + } + return 201, nil +} + +// UpdateStreamRule updates a stream rule of the Server. +func (lgc *Logic) UpdateStreamRule(prms *graylog.StreamRuleUpdateParams) (int, error) { + // PUT /streams/{streamid}/rules/{streamRuleID} Update a stream rule + if err := validator.UpdateValidator.Struct(prms); err != nil { + return 400, err + } + ok, err := lgc.HasStreamRule(prms.StreamID, prms.ID) + if err != nil { + return 500, err + } + if !ok { + return 404, fmt.Errorf("no stream rule is not found: <%s>", prms.StreamID) + } + if err := lgc.store.UpdateStreamRule(prms); err != nil { + return 500, err + } + return 204, nil +} + +// DeleteStreamRule deletes a stream rule from the Server. +func (lgc *Logic) DeleteStreamRule(streamID, streamRuleID string) (int, error) { + ok, err := lgc.HasStream(streamID) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "id": streamID, + }).Error("lgc.HasStream() is failure") + return 500, err + } + if !ok { + return 404, fmt.Errorf("no stream found with id <%s>", streamID) + } + ok, err = lgc.HasStreamRule(streamID, streamRuleID) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "streamID": streamID, "streamRuleID": streamRuleID, + }).Error("lgc.HasStreamRule() is failure") + return 500, err + } + if !ok { + return 404, fmt.Errorf("no stream rule found with id <%s>", streamRuleID) + } + + if err := lgc.store.DeleteStreamRule(streamID, streamRuleID); err != nil { + return 500, err + } + return 200, nil +} + +// GetStreamRules returns a list of all stream rules of a given stream. +func (lgc *Logic) GetStreamRules(streamID string) ([]graylog.StreamRule, int, int, error) { + if err := ValidateObjectID(streamID); err != nil { + // unfortunately graylog returns not 400 but 404. + return nil, 0, 404, err + } + ok, err := lgc.HasStream(streamID) + if err != nil { + return nil, 0, 500, err + } + if !ok { + return nil, 0, 404, fmt.Errorf("no stream is not found: <%s>", streamID) + } + rules, total, err := lgc.store.GetStreamRules(streamID) + if err != nil { + return nil, 0, 500, err + } + return rules, total, 200, nil +} + +// GetStreamRule returns a stream rule. +func (lgc *Logic) GetStreamRule(streamID, streamRuleID string) (*graylog.StreamRule, int, error) { + ok, err := lgc.HasStream(streamID) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "id": streamID, + }).Error("lgc.HasStream() is failure") + return nil, 500, err + } + if !ok { + return nil, 404, fmt.Errorf("no stream found with id <%s>", streamID) + } + ok, err = lgc.HasStreamRule(streamID, streamRuleID) + if err != nil { + lgc.Logger().WithFields(log.Fields{ + "error": err, "streamID": streamID, "streamRuleID": streamRuleID, + }).Error("lgc.HasStreamRule() is failure") + return nil, 500, err + } + if !ok { + return nil, 404, fmt.Errorf("no stream rule found with id <%s>", streamRuleID) + } + + rule, err := lgc.store.GetStreamRule(streamID, streamRuleID) + if err != nil { + return rule, 500, err + } + return rule, 200, nil +} diff --git a/mockserver/logic/stream_rule_test.go b/mockserver/logic/stream_rule_test.go new file mode 100644 index 00000000..be235111 --- /dev/null +++ b/mockserver/logic/stream_rule_test.go @@ -0,0 +1,150 @@ +package logic_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestAddStreamRule(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet("hoge") + if _, err := lgc.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + t.Fatal(err) + } + rule := testutil.StreamRule() + rule.StreamID = stream.ID + if _, err := lgc.AddStreamRule(nil); err == nil { + t.Fatal("stream is nil") + } + if _, err := lgc.AddStreamRule(rule); err != nil { + t.Fatal(err) + } +} + +func TestUpdateStreamRule(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet("hoge") + if _, err := lgc.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + t.Fatal(err) + } + rule := testutil.StreamRule() + rule.StreamID = stream.ID + if _, err := lgc.AddStreamRule(rule); err != nil { + t.Fatal(err) + } + if _, err := lgc.UpdateStreamRule(nil); err == nil { + t.Fatal("stream is nil") + } + if _, err := lgc.UpdateStreamRule(rule.NewUpdateParams()); err != nil { + t.Fatal(err) + } + rule.ID = "" + if _, err := lgc.UpdateStreamRule(rule.NewUpdateParams()); err == nil { + t.Fatal("stream.ID is required") + } +} + +func TestDeleteStreamRule(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet("hoge") + if _, err := lgc.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + t.Fatal(err) + } + rule := testutil.StreamRule() + rule.StreamID = stream.ID + if _, err := lgc.AddStreamRule(rule); err != nil { + t.Fatal(err) + } + if _, err := lgc.DeleteStreamRule("", rule.ID); err == nil { + t.Fatal("stream id is required") + } + if _, err := lgc.DeleteStreamRule(rule.StreamID, ""); err == nil { + t.Fatal("stream rule id is required") + } + if _, err := lgc.DeleteStreamRule(rule.StreamID, rule.ID); err != nil { + t.Fatal(err) + } +} + +func TestGetStreamRules(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet("hoge") + if _, err := lgc.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + t.Fatal(err) + } + rule := testutil.StreamRule() + rule.StreamID = stream.ID + if _, err := lgc.AddStreamRule(rule); err != nil { + t.Fatal(err) + } + if _, _, _, err := lgc.GetStreamRules(""); err == nil { + t.Fatal("stream id is required") + } + if _, _, _, err := lgc.GetStreamRules(rule.StreamID); err != nil { + t.Fatal(err) + } +} + +func TestGetStreamRule(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet("hoge") + if _, err := lgc.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + t.Fatal(err) + } + rule := testutil.StreamRule() + rule.StreamID = stream.ID + if _, err := lgc.AddStreamRule(rule); err != nil { + t.Fatal(err) + } + if _, _, err := lgc.GetStreamRule("", rule.ID); err == nil { + t.Fatal("stream id is required") + } + if _, _, err := lgc.GetStreamRule(rule.StreamID, ""); err == nil { + t.Fatal("stream rule id is required") + } + if _, _, err := lgc.GetStreamRule(rule.StreamID, rule.ID); err != nil { + t.Fatal(err) + } +} diff --git a/mockserver/logic/stream_test.go b/mockserver/logic/stream_test.go new file mode 100644 index 00000000..9e8c4cf7 --- /dev/null +++ b/mockserver/logic/stream_test.go @@ -0,0 +1,179 @@ +package logic_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestAddStream(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + is := testutil.IndexSet("hoge") + if _, err := lgc.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + t.Fatal(err) + } + if _, err := lgc.AddStream(nil); err == nil { + t.Fatal("stream is nil") + } +} + +func TestGetStreams(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + if _, _, _, err := lgc.GetStreams(); err != nil { + t.Fatal(err) + } +} + +func TestGetEnabledStreams(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + if _, _, _, err := lgc.GetEnabledStreams(); err != nil { + t.Fatal(err) + } +} + +func TestGetStream(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + is := testutil.IndexSet("hoge") + if _, err := lgc.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + t.Fatal(err) + } + + r, _, err := lgc.GetStream(stream.ID) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("stream is nil") + } + if r.ID != stream.ID { + t.Fatalf(`stream.ID = "%s", wanted "%s"`, r.ID, stream.ID) + } + if _, _, err := lgc.GetStream(""); err == nil { + t.Fatal("id is required") + } + if _, _, err := lgc.GetStream("h"); err == nil { + t.Fatal("stream should not be found") + } +} + +func TestUpdateStream(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + is := testutil.IndexSet("hoge") + if _, err := lgc.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + t.Fatal(err) + } + if _, _, err := lgc.UpdateStream(stream.NewUpdateParams()); err != nil { + t.Fatal(err) + } + stream.ID = "" + if _, _, err := lgc.UpdateStream(stream.NewUpdateParams()); err == nil { + t.Fatal("stream id is required") + } +} + +func TestDeleteStream(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + is := testutil.IndexSet("hoge") + if _, err := lgc.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + t.Fatal(err) + } + if _, err := lgc.DeleteStream(""); err == nil { + t.Fatal("stream id is required") + } + if _, err := lgc.DeleteStream(stream.ID); err != nil { + t.Fatal(err) + } + if _, err := lgc.DeleteStream(stream.ID); err == nil { + t.Fatal("already deleted") + } +} + +func TestPauseStream(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + is := testutil.IndexSet("hoge") + if _, err := lgc.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + t.Fatal(err) + } + if _, err := lgc.PauseStream(stream.ID); err != nil { + t.Fatal(err) + } + if _, err := lgc.PauseStream(""); err == nil { + t.Fatal("stream id is required") + } +} + +func TestResumeStream(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + + is := testutil.IndexSet("hoge") + if _, err := lgc.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.IndexSetID = is.ID + if _, err := lgc.AddStream(stream); err != nil { + t.Fatal(err) + } + if _, err := lgc.ResumeStream(stream.ID); err != nil { + t.Fatal(err) + } + if _, err := lgc.ResumeStream(""); err == nil { + t.Fatal("stream id is required") + } +} diff --git a/mockserver/logic/user.go b/mockserver/logic/user.go new file mode 100644 index 00000000..69f25250 --- /dev/null +++ b/mockserver/logic/user.go @@ -0,0 +1,153 @@ +package logic + +import ( + "crypto/md5" + "fmt" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/validator" + "github.com/suzuki-shunsuke/go-ptr" +) + +func encryptPassword(password string) string { + return fmt.Sprintf("%x", md5.Sum([]byte(password))) +} + +// HasUser returns whether the user exists. +func (lgc *Logic) HasUser(username string) (bool, error) { + return lgc.store.HasUser(username) +} + +// GetUser returns a user. +func (lgc *Logic) GetUser(username string) (*graylog.User, int, error) { + user, err := lgc.store.GetUser(username) + if err != nil { + return user, 500, err + } + if user == nil { + return user, 404, fmt.Errorf(`no user "%s" is found`, username) + } + return user, 200, nil +} + +// GetUsers returns a list of users. +func (lgc *Logic) GetUsers() ([]graylog.User, int, error) { + users, err := lgc.store.GetUsers() + if err != nil { + return users, 500, err + } + return users, 200, nil +} + +func (lgc *Logic) checkUserRoles(roles []string) (int, error) { + if len(roles) != 0 { + for _, roleName := range roles { + ok, err := lgc.HasRole(roleName) + if err != nil { + return 500, err + } + if !ok { + // unfortunately, graylog 2.4.3-1 returns 500 error + // https://github.com/Graylog2/graylog2-server/issues/4665 + return 500, fmt.Errorf(`no role found with name "%s"`, roleName) + } + } + } + return 200, nil +} + +// AddUser adds a user to the Server. +func (lgc *Logic) AddUser(user *graylog.User) (int, error) { + // client side validation + if err := validator.CreateValidator.Struct(user); err != nil { + return 400, err + } + + // Check a given username has already used. + ok, err := lgc.HasUser(user.Username) + if err != nil { + return 500, err + } + if ok { + return 400, fmt.Errorf( + `the user "%s" has already existed`, user.Username) + } + + // check role exists + if user.Roles != nil { + if sc, err := lgc.checkUserRoles(user.Roles.ToList()); err != nil { + return sc, err + } + } + + user.SetDefaultValues() + user.Password = encryptPassword(user.Password) + // Add a user + if err := lgc.store.AddUser(user); err != nil { + return 500, err + } + return 201, nil +} + +// UpdateUser updates a user of the Server. +// "email", "permissions", "full_name", "password" +func (lgc *Logic) UpdateUser(prms *graylog.UserUpdateParams) (int, error) { + if prms == nil { + return 400, fmt.Errorf("user is nil") + } + // Check updated user exists + ok, err := lgc.HasUser(prms.Username) + if err != nil { + return 500, err + } + if !ok { + return 404, fmt.Errorf(`the user "%s" is not found`, prms.Username) + } + + // client side validation + if err := validator.UpdateValidator.Struct(prms); err != nil { + return 400, err + } + + // check role exists + if prms.Roles != nil { + if sc, err := lgc.checkUserRoles(prms.Roles.ToList()); err != nil { + return sc, err + } + } + if prms.Password != nil { + prms.Password = ptr.PStr(encryptPassword(*prms.Password)) + } + + // update + if err := lgc.store.UpdateUser(prms); err != nil { + return 500, err + } + return 200, nil +} + +// DeleteUser removes a user from the Server. +func (lgc *Logic) DeleteUser(name string) (int, error) { + // Check deleted user exists + ok, err := lgc.HasUser(name) + if err != nil { + return 500, err + } + if !ok { + return 404, fmt.Errorf(`the user "%s" is not found`, name) + } + if name == "admin" { + // graylog spec + return 404, fmt.Errorf(`the user "%s" is not found`, name) + } + // Delete a user + if err := lgc.store.DeleteUser(name); err != nil { + return 500, err + } + return 204, nil +} + +// UserList returns a list of all users. +func (lgc *Logic) UserList() ([]graylog.User, error) { + return lgc.store.GetUsers() +} diff --git a/mockserver/logic/user_test.go b/mockserver/logic/user_test.go new file mode 100644 index 00000000..ec5839b1 --- /dev/null +++ b/mockserver/logic/user_test.go @@ -0,0 +1,154 @@ +package logic_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/testutil" + "github.com/suzuki-shunsuke/go-set" +) + +func TestAddUser(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + t.Run("basic", func(t *testing.T) { + user := testutil.User() + if user.Roles == nil { + user.Roles = set.NewStrSet("Admin") + } + if _, err := lgc.AddUser(user); err != nil { + t.Fatal(err) + } + }) + t.Run("username is required", func(t *testing.T) { + user := testutil.User() + user.Username = "" + if _, err := lgc.AddUser(user); err == nil { + t.Fatal("user.Username is required") + } + }) + t.Run("username duplicate", func(t *testing.T) { + user := testutil.User() + if _, err := lgc.AddUser(user); err == nil { + t.Fatal("user name is duplicate") + } + }) + t.Run("unexisting role name", func(t *testing.T) { + user := testutil.User() + if user.Roles == nil { + user.Roles = set.NewStrSet("aa") + } + user.Username += " changed" + if _, err := lgc.AddUser(user); err == nil { + t.Fatal("unexisting role name") + } + }) +} + +func TestGetUser(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + t.Run("basic", func(t *testing.T) { + if _, _, err := lgc.GetUser("admin"); err != nil { + t.Fatal(err) + } + }) + t.Run("username is required", func(t *testing.T) { + if _, _, err := lgc.GetUser(""); err == nil { + t.Fatal("username is required") + } + }) + t.Run("not found", func(t *testing.T) { + if _, _, err := lgc.GetUser("h"); err == nil { + t.Fatal("user not found") + } + }) +} + +func TestGetUsers(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + if _, _, err := lgc.GetUsers(); err != nil { + t.Fatal(err) + } +} + +func TestUpdateUser(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + t.Run("nil", func(t *testing.T) { + if _, err := lgc.UpdateUser(nil); err == nil { + t.Fatal("user is nil") + } + }) + user := testutil.User() + name := user.Username + if user.Roles == nil { + user.Roles = set.NewStrSet("Admin") + } + if _, err := lgc.AddUser(user); err != nil { + t.Fatal(err) + } + t.Run("basic", func(t *testing.T) { + if _, err := lgc.UpdateUser(user.NewUpdateParams()); err != nil { + t.Fatal(err) + } + }) + t.Run("username is required", func(t *testing.T) { + user.Username = "" + if _, err := lgc.UpdateUser(user.NewUpdateParams()); err == nil { + t.Fatal("user.Username is required") + } + }) + t.Run("validation error", func(t *testing.T) { + // TODO + }) + t.Run("check role", func(t *testing.T) { + user.Username = name + user.Roles = set.NewStrSet("aa") + if _, err := lgc.UpdateUser(user.NewUpdateParams()); err == nil { + t.Fatal("unexisting role") + } + }) +} + +func TestDeleteUser(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + t.Run("username is required", func(t *testing.T) { + if _, err := lgc.DeleteUser(""); err == nil { + t.Fatal("username is required") + } + }) + user := testutil.User() + if _, err := lgc.AddUser(user); err != nil { + t.Fatal(err) + } + t.Run("basic", func(t *testing.T) { + if _, err := lgc.DeleteUser(user.Username); err != nil { + t.Fatal(err) + } + }) +} + +func TestUserList(t *testing.T) { + lgc, err := logic.NewLogic(nil) + if err != nil { + t.Fatal(err) + } + t.Run("basic", func(t *testing.T) { + if _, err := lgc.UserList(); err != nil { + t.Fatal(err) + } + }) +} diff --git a/mockserver/logic/util.go b/mockserver/logic/util.go new file mode 100644 index 00000000..3633ca50 --- /dev/null +++ b/mockserver/logic/util.go @@ -0,0 +1,26 @@ +package logic + +import ( + "fmt" + + "gopkg.in/mgo.v2/bson" + + log "github.com/sirupsen/logrus" +) + +// ValidateObjectID validates ObjectID. +func ValidateObjectID(id string) error { + if !bson.IsObjectIdHex(id) { + return fmt.Errorf("id <%s> is invalid", id) + } + return nil +} + +// LogWE outputs outputs log at Warn or Error level. +func LogWE(sc int, entry *log.Entry, msg string) { + if sc >= 500 { + entry.Error(msg) + } else { + entry.Warn(msg) + } +} diff --git a/mockserver/seed/seed.go b/mockserver/seed/seed.go new file mode 100644 index 00000000..c2d31fc3 --- /dev/null +++ b/mockserver/seed/seed.go @@ -0,0 +1,102 @@ +package seed + +import ( + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-set" +) + +// Role returns a Role. +func Role() *graylog.Role { + return &graylog.Role{ + Name: "Admin", + Description: "Grants all permissions for Graylog administrators (built-in)", + Permissions: set.NewStrSet("*"), + ReadOnly: true} +} + +// User returns a user. +func User() *graylog.User { + return &graylog.User{ + Username: "admin", + Email: "hoge@example.com", + FullName: "Administrator", + Password: "admin", + Permissions: set.NewStrSet("*"), + } +} + +// Nobody returns a user who has no permission. +// This user is used to test the authorization. +func Nobody() *graylog.User { + return &graylog.User{ + Username: "nobody", + Email: "nobody@example.com", + FullName: "No Body", + Password: "password", + Permissions: set.NewStrSet(), + } +} + +// Input returns an input. +func Input() *graylog.Input { + return &graylog.Input{ + Title: "test", + Node: "2ad6b340-3e5f-4a96-ae81-040cfb8b6024", + Attrs: &graylog.InputBeatsAttrs{ + BindAddress: "0.0.0.0", + Port: 514, + RecvBufferSize: 262144, + }} +} + +// IndexSet returns an index set. +func IndexSet() *graylog.IndexSet { + return &graylog.IndexSet{ + Title: "Default index set", + Description: "The Graylog default index set", + IndexPrefix: "graylog", + Shards: 4, + Replicas: 0, + RotationStrategyClass: "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategy", + RotationStrategy: &graylog.RotationStrategy{ + Type: "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategyConfig", + MaxDocsPerIndex: 20000000}, + RetentionStrategyClass: "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy", + RetentionStrategy: &graylog.RetentionStrategy{ + Type: "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig", + MaxNumberOfIndices: 20}, + CreationDate: "2018-02-20T11:37:19.305Z", + IndexAnalyzer: "standard", + IndexOptimizationMaxNumSegments: 1, + IndexOptimizationDisabled: false, + Writable: true, + Default: true} +} + +// IndexSetStats returns an index set statistics. +func IndexSetStats() *graylog.IndexSetStats { + return &graylog.IndexSetStats{ + Indices: 2, + Documents: 0, + Size: 1412, + } +} + +// Stream returns a stream. +func Stream() *graylog.Stream { + return &graylog.Stream{ + MatchingType: "AND", + Description: "Stream containing all messages", + Rules: []graylog.StreamRule{}, + Title: "All messages", + } +} + +// StreamRule returns a stream rule. +func StreamRule() *graylog.StreamRule { + return &graylog.StreamRule{ + Type: 1, + Value: "test", + Field: "tag", + } +} diff --git a/mockserver/server.go b/mockserver/server.go new file mode 100644 index 00000000..9b4c5ab8 --- /dev/null +++ b/mockserver/server.go @@ -0,0 +1,75 @@ +package mockserver + +import ( + "fmt" + "net" + "net/http/httptest" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/handler" + "github.com/suzuki-shunsuke/go-graylog/mockserver/logic" + "github.com/suzuki-shunsuke/go-graylog/mockserver/store" + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" +) + +// Server represents a mock of the Graylog API. +// Server embeds the Logic, so please see the document about Logic also. +// https://godoc.org/github.com/suzuki-shunsuke/go-graylog/mockserver/logic +type Server struct { + *logic.Logic `json:"-"` + server *httptest.Server + endpoint string +} + +// NewServer returns new Server but doesn't start it. +// The argument `addr` is the port number which the server uses. +// +// server, err := mockserver.NewServer(":8000", nil) +// +// If addr is an empty string, the free port is assigned automatially. +// The argument `store` is the store which the server uses. +// If `store` is nil, the default plain store is used and data is not persisted. +// To start the server, call the Start method. +// +// server.Start() +// defer server.Close() +func NewServer(addr string, store store.Store) (*Server, error) { + if store == nil { + store = plain.NewStore("") + } + srv, err := logic.NewLogic(store) + if err != nil { + return nil, err + } + ms := &Server{ + Logic: srv, + server: httptest.NewUnstartedServer(handler.NewRouter(srv)), + } + if addr != "" { + ln, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + ms.server.Listener = ln + } + ms.endpoint = fmt.Sprintf("http://%s/api", ms.server.Listener.Addr().String()) + return ms, nil +} + +// Start starts a server from NewUnstartedServer. +func (ms *Server) Start() { + ms.server.Start() +} + +// Close shuts down the server and blocks until all outstanding requests on this server have completed. +func (ms *Server) Close() { + ms.Logger().Info("Close Server") + ms.server.Close() +} + +// Endpoint returns the endpoint url. +// +// server, err := mockserver.NewServer(":8000", nil) +// fmt.Println(server.Endpoint()) // http://localhost:8000/api +func (ms *Server) Endpoint() string { + return ms.endpoint +} diff --git a/mockserver/server_test.go b/mockserver/server_test.go new file mode 100644 index 00000000..442cb817 --- /dev/null +++ b/mockserver/server_test.go @@ -0,0 +1,22 @@ +package mockserver_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver" +) + +func TestNewServer(t *testing.T) { + server, err := mockserver.NewServer("", nil) + if err != nil { + t.Fatal(err) + } + if server == nil { + t.Fatal("server is nil") + } + server.Start() + defer server.Close() + if server.Endpoint() == "" { + t.Fatal("endpoint is empty") + } +} diff --git a/mockserver/store/doc.go b/mockserver/store/doc.go new file mode 100644 index 00000000..a2922f84 --- /dev/null +++ b/mockserver/store/doc.go @@ -0,0 +1,4 @@ +/* +Package store provides store interface for Graylog API mock server. +*/ +package store diff --git a/mockserver/store/plain/doc.go b/mockserver/store/plain/doc.go new file mode 100644 index 00000000..9df92130 --- /dev/null +++ b/mockserver/store/plain/doc.go @@ -0,0 +1,4 @@ +/* +Package plain provides the implementation of store.Store interface. +*/ +package plain diff --git a/mockserver/store/plain/index_set.go b/mockserver/store/plain/index_set.go new file mode 100644 index 00000000..4b7b0324 --- /dev/null +++ b/mockserver/store/plain/index_set.go @@ -0,0 +1,173 @@ +package plain + +import ( + "fmt" + "strings" + + "github.com/suzuki-shunsuke/go-graylog" + st "github.com/suzuki-shunsuke/go-graylog/mockserver/store" +) + +// HasIndexSet returns whether the index set exists. +func (store *Store) HasIndexSet(id string) (bool, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + for _, is := range store.indexSets { + if is.ID == id { + return true, nil + } + } + return false, nil +} + +// GetIndexSet returns an index set. +func (store *Store) GetIndexSet(id string) (*graylog.IndexSet, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + for _, is := range store.indexSets { + if is.ID == id { + is.Default = store.defaultIndexSetID == id + return &is, nil + } + } + return nil, nil +} + +// GetDefaultIndexSetID returns a default index set id. +func (store *Store) GetDefaultIndexSetID() (string, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + return store.defaultIndexSetID, nil +} + +// SetDefaultIndexSetID sets a default index set id. +func (store *Store) SetDefaultIndexSetID(id string) error { + is, err := store.GetIndexSet(id) + if err != nil { + return err + } + if is == nil { + return fmt.Errorf("no index set with id <%s> is not found", id) + } + if !is.Writable { + return fmt.Errorf("default index set must be writable") + } + store.imutex.Lock() + defer store.imutex.Unlock() + store.defaultIndexSetID = id + return nil +} + +// AddIndexSet adds an index set to the store. +func (store *Store) AddIndexSet(is *graylog.IndexSet) error { + if is == nil { + return fmt.Errorf("index set is nil") + } + if is.ID == "" { + is.ID = st.NewObjectID() + } + store.imutex.Lock() + defer store.imutex.Unlock() + store.indexSets = append(store.indexSets, *is) + return nil +} + +// UpdateIndexSet updates an index set at the Mock Server. +func (store *Store) UpdateIndexSet(prms *graylog.IndexSetUpdateParams) (*graylog.IndexSet, error) { + id := prms.ID + store.imutex.Lock() + defer store.imutex.Unlock() + for i, is := range store.indexSets { + if is.ID != id { + continue + } + is.Title = prms.Title + is.IndexPrefix = prms.IndexPrefix + is.RotationStrategyClass = prms.RotationStrategyClass + is.RotationStrategy = prms.RotationStrategy + is.RetentionStrategy = prms.RetentionStrategy + is.IndexAnalyzer = prms.IndexAnalyzer + is.Shards = prms.Shards + is.IndexOptimizationMaxNumSegments = prms.IndexOptimizationMaxNumSegments + if prms.Description != nil { + is.Description = *prms.Description + } + if prms.Replicas != nil { + is.Replicas = *prms.Replicas + } + if prms.IndexOptimizationDisabled != nil { + is.IndexOptimizationDisabled = *prms.IndexOptimizationDisabled + } + if prms.Writable != nil { + is.Writable = *prms.Writable + } + store.indexSets[i] = is + return &is, nil + } + return nil, fmt.Errorf("no index set with id <%s>", id) +} + +// DeleteIndexSet removes a index set from the Mock Server. +func (store *Store) DeleteIndexSet(id string) error { + store.imutex.Lock() + defer store.imutex.Unlock() + size := len(store.indexSets) + if size == 0 { + return nil + } + var arr []graylog.IndexSet + if size == 1 { + arr = []graylog.IndexSet{} + } else { + arr = make([]graylog.IndexSet, size-1) + } + i := 0 + for _, is := range store.indexSets { + if is.ID == id { + continue + } + arr[i] = is + i++ + } + store.indexSets = arr + return nil +} + +// GetIndexSets returns a list of all index sets. +func (store *Store) GetIndexSets(skip, limit int) ([]graylog.IndexSet, int, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + total := len(store.indexSets) + size := total + if skip < 0 { + skip = 0 + } else { + size -= skip + } + if limit > 0 && limit < size { + size = limit + } + arr := make([]graylog.IndexSet, size) + defID := store.defaultIndexSetID + for i := 0; i < size; i++ { + is := store.indexSets[i+skip] + is.Default = defID == is.ID + arr[i] = is + } + return arr, total, nil +} + +// IsConflictIndexPrefix returns true if indexPrefix would conflict with an existing index set. +func (store *Store) IsConflictIndexPrefix(id, prefix string) (bool, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + for _, is := range store.indexSets { + if id != is.ID && strings.HasPrefix(prefix, is.IndexPrefix) { + return true, nil + } + if id != is.ID && strings.HasPrefix(is.IndexPrefix, prefix) { + return true, nil + } + } + return false, nil +} diff --git a/mockserver/store/plain/index_set_stats.go b/mockserver/store/plain/index_set_stats.go new file mode 100644 index 00000000..f026b4da --- /dev/null +++ b/mockserver/store/plain/index_set_stats.go @@ -0,0 +1,37 @@ +package plain + +import ( + "github.com/suzuki-shunsuke/go-graylog" +) + +// GetIndexSetStats returns an index set stats. +func (store *Store) GetIndexSetStats(id string) (*graylog.IndexSetStats, error) { + ok, err := store.HasIndexSet(id) + if err != nil { + return nil, err + } + if ok { + // TODO returns correct index set stats + return &graylog.IndexSetStats{}, nil + } + return nil, nil +} + +// GetIndexSetStatsMap returns all of index set stats. +func (store *Store) GetIndexSetStatsMap() (map[string]graylog.IndexSetStats, error) { + m := map[string]graylog.IndexSetStats{} + store.imutex.RLock() + defer store.imutex.RUnlock() + for _, is := range store.indexSets { + // TODO returns correct index set stats + m[is.ID] = graylog.IndexSetStats{} + } + return m, nil +} + +// GetTotalIndexSetStats returns all index set's statistics. +func (store *Store) GetTotalIndexSetStats() (*graylog.IndexSetStats, error) { + // TODO returns correct index set stats + indexSetStats := &graylog.IndexSetStats{} + return indexSetStats, nil +} diff --git a/mockserver/store/plain/index_set_stats_test.go b/mockserver/store/plain/index_set_stats_test.go new file mode 100644 index 00000000..bf81f388 --- /dev/null +++ b/mockserver/store/plain/index_set_stats_test.go @@ -0,0 +1,23 @@ +package plain_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" +) + +func TestGetIndexSetStats(t *testing.T) { + store := plain.NewStore("") + _, err := store.GetIndexSetStats("foo") + if err != nil { + t.Fatal(err) + } +} + +func TestGetTotalIndexSetStats(t *testing.T) { + store := plain.NewStore("") + _, err := store.GetTotalIndexSetStats() + if err != nil { + t.Fatal(err) + } +} diff --git a/mockserver/store/plain/index_set_test.go b/mockserver/store/plain/index_set_test.go new file mode 100644 index 00000000..a7cd7069 --- /dev/null +++ b/mockserver/store/plain/index_set_test.go @@ -0,0 +1,170 @@ +package plain_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/store" + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestHasIndexSet(t *testing.T) { + store := plain.NewStore("") + ok, err := store.HasIndexSet("foo") + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("is foo should not exist") + } +} + +func TestGetIndexSet(t *testing.T) { + store := plain.NewStore("") + is, err := store.GetIndexSet("foo") + if err != nil { + t.Fatal(err) + } + if is != nil { + t.Fatal("is foo should not exist") + } +} + +func TestGetIndexSets(t *testing.T) { + store := plain.NewStore("") + iss, _, err := store.GetIndexSets(0, 0) + if err != nil { + t.Fatal(err) + } + if len(iss) != 0 { + t.Fatal("iss should be nil or empty array") + } +} + +func TestAddIndexSet(t *testing.T) { + st := plain.NewStore("") + is := testutil.IndexSet("hoge") + is.ID = store.NewObjectID() + if err := st.AddIndexSet(is); err != nil { + t.Fatal(err) + } + r, err := st.GetIndexSet(is.ID) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("is is nil") + } +} + +func TestUpdateIndexSet(t *testing.T) { + st := plain.NewStore("") + is := testutil.IndexSet("hoge") + is.ID = store.NewObjectID() + if err := st.AddIndexSet(is); err != nil { + t.Fatal(err) + } + r, err := st.GetIndexSet(is.ID) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("is is nil") + } + is.Title += " changed" + if _, err := st.UpdateIndexSet(is.NewUpdateParams()); err != nil { + t.Fatal(err) + } + r, err = st.GetIndexSet(is.ID) + if err != nil { + t.Fatal(err) + } + if is.Title != r.Title { + t.Fatalf(`is.Title = "%s", wanted "%s"`, r.Title, is.Title) + } +} + +func TestDeleteIndexSet(t *testing.T) { + st := plain.NewStore("") + if err := st.DeleteIndexSet("foo"); err != nil { + t.Fatal(err) + } + is := testutil.IndexSet("hoge") + is.ID = store.NewObjectID() + if err := st.AddIndexSet(is); err != nil { + t.Fatal(err) + } + if err := st.DeleteIndexSet(is.ID); err != nil { + t.Fatal(err) + } + ok, err := st.HasIndexSet(is.ID) + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("is should be deleted") + } +} + +func TestGetDefaultIndexSetID(t *testing.T) { + store := plain.NewStore("") + if _, err := store.GetDefaultIndexSetID(); err != nil { + t.Fatal(err) + } +} + +func TestSetDefaultIndexSetID(t *testing.T) { + st := plain.NewStore("") + is := testutil.IndexSet("hoge") + is.ID = store.NewObjectID() + if err := st.AddIndexSet(is); err != nil { + t.Fatal(err) + } + if err := st.SetDefaultIndexSetID(is.ID); err != nil { + t.Fatal(err) + } + id, err := st.GetDefaultIndexSetID() + if err != nil { + t.Fatal(err) + } + if id != is.ID { + t.Fatalf("default id is <%s>, wanted <%s>", id, is.ID) + } +} + +func TestIsConflictIndexPrefix(t *testing.T) { + st := plain.NewStore("") + is := testutil.IndexSet("hoge") + is.ID = store.NewObjectID() + if err := st.AddIndexSet(is); err != nil { + t.Fatal(err) + } + ok, err := st.IsConflictIndexPrefix(is.ID, is.IndexPrefix) + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("id should not conflict") + } + ok, err = st.IsConflictIndexPrefix("", is.IndexPrefix) + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("id should conflict") + } + ok, err = st.IsConflictIndexPrefix("", "ho") + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("id should conflict") + } + ok, err = st.IsConflictIndexPrefix("", "hogefuga") + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("id should conflict") + } +} diff --git a/mockserver/store/plain/input.go b/mockserver/store/plain/input.go new file mode 100644 index 00000000..7e7b8981 --- /dev/null +++ b/mockserver/store/plain/input.go @@ -0,0 +1,88 @@ +package plain + +import ( + "fmt" + "time" + + "github.com/suzuki-shunsuke/go-graylog" + st "github.com/suzuki-shunsuke/go-graylog/mockserver/store" +) + +// HasInput returns whether the input exists. +func (store *Store) HasInput(id string) (bool, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + _, ok := store.inputs[id] + return ok, nil +} + +// GetInput returns an input. +func (store *Store) GetInput(id string) (*graylog.Input, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + s, ok := store.inputs[id] + if ok { + return &s, nil + } + return nil, nil +} + +// AddInput adds an input to the store. +func (store *Store) AddInput(input *graylog.Input) error { + if input == nil { + return fmt.Errorf("input is nil") + } + if input.ID == "" { + input.ID = st.NewObjectID() + } + input.CreatedAt = time.Now().Format("2006-01-02T15:04:05.000Z") + + store.imutex.Lock() + defer store.imutex.Unlock() + store.inputs[input.ID] = *input + return nil +} + +// UpdateInput updates an input at the Store. +// Required: Title, Type, Attrs +// Allowed: Global, Node +func (store *Store) UpdateInput(prms *graylog.InputUpdateParams) (*graylog.Input, error) { + store.imutex.Lock() + defer store.imutex.Unlock() + input, ok := store.inputs[prms.ID] + if !ok { + return nil, fmt.Errorf("the input <%s> is not found", prms.ID) + } + input.Title = prms.Title + input.Attrs = prms.Attrs + if prms.Global == nil { + input.Global = *prms.Global + } + if prms.Node == "" { + input.Node = prms.Node + } + store.inputs[input.ID] = input + return &input, nil +} + +// DeleteInput deletes an input from the store. +func (store *Store) DeleteInput(id string) error { + store.imutex.Lock() + defer store.imutex.Unlock() + delete(store.inputs, id) + return nil +} + +// GetInputs returns inputs. +func (store *Store) GetInputs() ([]graylog.Input, int, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + size := len(store.inputs) + arr := make([]graylog.Input, size) + i := 0 + for _, input := range store.inputs { + arr[i] = input + i++ + } + return arr, size, nil +} diff --git a/mockserver/store/plain/input_test.go b/mockserver/store/plain/input_test.go new file mode 100644 index 00000000..04abdfe5 --- /dev/null +++ b/mockserver/store/plain/input_test.go @@ -0,0 +1,103 @@ +package plain_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestHasInput(t *testing.T) { + store := plain.NewStore("") + ok, err := store.HasInput("foo") + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("input foo should not exist") + } +} + +func TestGetInput(t *testing.T) { + store := plain.NewStore("") + input, err := store.GetInput("foo") + if err != nil { + t.Fatal(err) + } + if input != nil { + t.Fatal("input foo should not exist") + } +} + +func TestGetInputs(t *testing.T) { + store := plain.NewStore("") + inputs, _, err := store.GetInputs() + if err != nil { + t.Fatal(err) + } + if len(inputs) != 0 { + t.Fatal("inputs should be nil or empty array") + } +} + +func TestAddInput(t *testing.T) { + store := plain.NewStore("") + input := testutil.Input() + if err := store.AddInput(input); err != nil { + t.Fatal(err) + } + r, err := store.GetInput(input.ID) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("input is nil") + } +} + +func TestUpdateInput(t *testing.T) { + store := plain.NewStore("") + input := testutil.Input() + if err := store.AddInput(input); err != nil { + t.Fatal(err) + } + r, err := store.GetInput(input.ID) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("input is nil") + } + input.Title += " changed" + if _, err := store.UpdateInput(input.NewUpdateParams()); err != nil { + t.Fatal(err) + } + r, err = store.GetInput(input.ID) + if err != nil { + t.Fatal(err) + } + if input.Title != r.Title { + t.Fatalf(`input.Title = "%s", wanted "%s"`, r.Title, input.Title) + } +} + +func TestDeleteInput(t *testing.T) { + store := plain.NewStore("") + if err := store.DeleteInput("foo"); err != nil { + t.Fatal(err) + } + input := testutil.Input() + if err := store.AddInput(input); err != nil { + t.Fatal(err) + } + if err := store.DeleteInput(input.ID); err != nil { + t.Fatal(err) + } + ok, err := store.HasInput(input.ID) + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("input should be deleted") + } +} diff --git a/mockserver/store/plain/role.go b/mockserver/store/plain/role.go new file mode 100644 index 00000000..10301b1b --- /dev/null +++ b/mockserver/store/plain/role.go @@ -0,0 +1,75 @@ +package plain + +import ( + "fmt" + + "github.com/suzuki-shunsuke/go-graylog" +) + +// HasRole returns whether the role exists. +func (store *Store) HasRole(name string) (bool, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + _, ok := store.roles[name] + return ok, nil +} + +// GetRole returns a Role. +// If no role with given name is found, returns nil and not returns an error. +func (store *Store) GetRole(name string) (*graylog.Role, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + s, ok := store.roles[name] + if ok { + return &s, nil + } + return nil, nil +} + +// GetRoles returns Roles. +func (store *Store) GetRoles() ([]graylog.Role, int, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + size := len(store.roles) + arr := make([]graylog.Role, size) + i := 0 + for _, role := range store.roles { + arr[i] = role + i++ + } + return arr, size, nil +} + +// AddRole adds a new role to the store. +func (store *Store) AddRole(role *graylog.Role) error { + store.imutex.Lock() + defer store.imutex.Unlock() + store.roles[role.Name] = *role + return nil +} + +// UpdateRole updates a role at the store. +func (store *Store) UpdateRole(name string, prms *graylog.RoleUpdateParams) (*graylog.Role, error) { + store.imutex.Lock() + defer store.imutex.Unlock() + role, ok := store.roles[name] + if !ok { + return nil, fmt.Errorf(`no role with name "%s"`, name) + } + if prms.Description != nil { + role.Description = *prms.Description + } + role.Permissions = prms.Permissions + role.Name = prms.Name + delete(store.roles, name) + store.roles[role.Name] = role + return &role, nil +} + +// DeleteRole deletes a role from store. +func (store *Store) DeleteRole(name string) error { + store.imutex.Lock() + defer store.imutex.Unlock() + delete(store.roles, name) + return nil +} diff --git a/mockserver/store/plain/role_test.go b/mockserver/store/plain/role_test.go new file mode 100644 index 00000000..4d546cc9 --- /dev/null +++ b/mockserver/store/plain/role_test.go @@ -0,0 +1,103 @@ +package plain_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestHasRole(t *testing.T) { + store := plain.NewStore("") + ok, err := store.HasRole("foo") + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("role foo should not exist") + } +} + +func TestGetRole(t *testing.T) { + store := plain.NewStore("") + role, err := store.GetRole("foo") + if err != nil { + t.Fatal(err) + } + if role != nil { + t.Fatal("role foo should not exist") + } +} + +func TestGetRoles(t *testing.T) { + store := plain.NewStore("") + roles, _, err := store.GetRoles() + if err != nil { + t.Fatal(err) + } + if len(roles) != 0 { + t.Fatal("roles should be nil or empty array") + } +} + +func TestAddRole(t *testing.T) { + store := plain.NewStore("") + role := testutil.Role() + if err := store.AddRole(role); err != nil { + t.Fatal(err) + } + r, err := store.GetRole(role.Name) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("role is nil") + } +} + +func TestUpdateRole(t *testing.T) { + store := plain.NewStore("") + role := testutil.Role() + if err := store.AddRole(role); err != nil { + t.Fatal(err) + } + r, err := store.GetRole(role.Name) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("role is nil") + } + role.Description += " changed" + if _, err := store.UpdateRole(role.Name, role.NewUpdateParams()); err != nil { + t.Fatal(err) + } + r, err = store.GetRole(role.Name) + if err != nil { + t.Fatal(err) + } + if role.Description != r.Description { + t.Fatalf(`role.Description = "%s", wanted "%s"`, r.Description, role.Description) + } +} + +func TestDeleteRole(t *testing.T) { + store := plain.NewStore("") + if err := store.DeleteRole("foo"); err != nil { + t.Fatal(err) + } + role := testutil.Role() + if err := store.AddRole(role); err != nil { + t.Fatal(err) + } + if err := store.DeleteRole(role.Name); err != nil { + t.Fatal(err) + } + ok, err := store.HasRole(role.Name) + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("role should be deleted") + } +} diff --git a/mockserver/store/plain/store.go b/mockserver/store/plain/store.go new file mode 100644 index 00000000..6984bf6d --- /dev/null +++ b/mockserver/store/plain/store.go @@ -0,0 +1,152 @@ +package plain + +import ( + "encoding/json" + "io/ioutil" + "os" + "strings" + "sync" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/store" +) + +// Store is the implementation of the Store interface with pure golang. +type Store struct { + users map[string]graylog.User + roles map[string]graylog.Role + inputs map[string]graylog.Input + indexSets []graylog.IndexSet + defaultIndexSetID string + streams map[string]graylog.Stream + streamRules map[string]map[string]graylog.StreamRule + dataPath string + tokens map[string]string + imutex sync.RWMutex +} + +type plainStore struct { + Users map[string]graylog.User `json:"users"` + Roles map[string]graylog.Role `json:"roles"` + Inputs map[string]graylog.Input `json:"inputs"` + IndexSets []graylog.IndexSet `json:"index_sets"` + DefaultIndexSetID string `json:"default_index_set_id"` + Streams map[string]graylog.Stream `json:"streams"` + StreamRules map[string]map[string]graylog.StreamRule `json:"stream_rules"` + Tokens map[string]string `json:"tokens"` +} + +// MarshalJSON is the implementation of the json.Marshaler interface. +func (store *Store) MarshalJSON() ([]byte, error) { + data := map[string]interface{}{ + "users": store.users, + "roles": store.roles, + "inputs": store.inputs, + "index_sets": store.indexSets, + "default_index_set_id": store.defaultIndexSetID, + "streams": store.streams, + "stream_rules": store.streamRules, + "tokens": store.tokens, + } + return json.Marshal(data) +} + +// UnmarshalJSON is the implementation of the json.Unmarshaler interface. +func (store *Store) UnmarshalJSON(b []byte) error { + s := &plainStore{} + if err := json.Unmarshal(b, s); err != nil { + return err + } + store.users = s.Users + store.roles = s.Roles + store.inputs = s.Inputs + store.indexSets = s.IndexSets + store.defaultIndexSetID = s.DefaultIndexSetID + store.streams = s.Streams + store.streamRules = s.StreamRules + store.tokens = s.Tokens + return nil +} + +// NewStore returns a new Store. +// the argument `dataPath` is the file path where write the data. +// If `dataPath` is empty, the data aren't written to the file. +func NewStore(dataPath string) store.Store { + return &Store{ + roles: map[string]graylog.Role{}, + users: map[string]graylog.User{}, + inputs: map[string]graylog.Input{}, + indexSets: []graylog.IndexSet{}, + streams: map[string]graylog.Stream{}, + streamRules: map[string]map[string]graylog.StreamRule{}, + tokens: map[string]string{}, + dataPath: dataPath, + } +} + +// Save writes Mock Server's data in a file for persistence. +func (store *Store) Save() error { + store.imutex.RLock() + defer store.imutex.RUnlock() + if store.dataPath == "" { + return nil + } + b, err := json.Marshal(store) + if err != nil { + return err + } + return ioutil.WriteFile(store.dataPath, b, 0600) +} + +// Load reads Mock Server's data from a file. +func (store *Store) Load() error { + store.imutex.Lock() + defer store.imutex.Unlock() + if store.dataPath == "" { + return nil + } + if _, err := os.Stat(store.dataPath); err != nil { + return nil + } + b, err := ioutil.ReadFile(store.dataPath) + if err != nil { + return err + } + return json.Unmarshal(b, store) +} + +// Authorize authorizes the user. +func (store *Store) Authorize(user *graylog.User, scope string, args ...string) (bool, error) { + if user == nil { + return true, nil + } + perm := scope + if len(args) != 0 { + perm += ":" + strings.Join(args, ":") + } + // check user permissions + if user.Permissions != nil { + if user.Permissions.HasAny("*", scope, perm) { + return true, nil + } + } + // check user roles + if user.Roles == nil { + return false, nil + } + for k := range user.Roles.ToMap(false) { + // get role + role, err := store.GetRole(k) + if err != nil { + return false, err + } + // check role permissions + if role.Permissions == nil { + continue + } + if role.Permissions.HasAny("*", scope, perm) { + return true, nil + } + } + return false, nil +} diff --git a/mockserver/store/plain/store_test.go b/mockserver/store/plain/store_test.go new file mode 100644 index 00000000..4a530203 --- /dev/null +++ b/mockserver/store/plain/store_test.go @@ -0,0 +1,121 @@ +package plain_test + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" + "github.com/suzuki-shunsuke/go-graylog/testutil" + "github.com/suzuki-shunsuke/go-set" +) + +func TestNewStore(t *testing.T) { + store := plain.NewStore("") + if store == nil { + t.Fatal("store is nil") + } +} + +func TestSave(t *testing.T) { + store := plain.NewStore("") + if err := store.Save(); err != nil { + t.Fatal(err) + } + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) + store = plain.NewStore(tmpfile.Name()) + if err := store.Save(); err != nil { + t.Fatal(err) + } +} + +func TestLoad(t *testing.T) { + store := plain.NewStore("") + if err := store.Load(); err != nil { + t.Fatal(err) + } + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) + store = plain.NewStore(tmpfile.Name()) + if err := store.Save(); err != nil { + t.Fatal(err) + } + if err := store.Load(); err != nil { + t.Fatal(err) + } +} + +func TestAuthorize(t *testing.T) { + store := plain.NewStore("") + ok, err := store.Authorize(nil, "users:read") + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("not allowed") + } + + admin := testutil.User() + admin.Permissions.Add("*") + ok, err = store.Authorize(admin, "users:read") + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("not allowed") + } + + admin.Permissions = nil + ok, err = store.Authorize(admin, "users:read") + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("not allowed") + } + + adminRole := testutil.Role() + if admin.Permissions == nil { + admin.Permissions = set.NewStrSet() + } + adminRole.Permissions.Add("*") + if admin.Roles == nil { + admin.Roles = set.NewStrSet() + } + admin.Roles.Add(adminRole.Name) + if err := store.AddRole(adminRole); err != nil { + t.Fatal(err) + } + ok, err = store.Authorize(admin, "users:read") + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("not allowed") + } + ok, err = store.Authorize(admin, "users:read", "foo") + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("not allowed") + } + adminRole.Permissions = nil + if _, err := store.UpdateRole(adminRole.Name, adminRole.NewUpdateParams()); err != nil { + t.Fatal(err) + } + ok, err = store.Authorize(admin, "users:read", "foo") + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("not allowed") + } +} diff --git a/mockserver/store/plain/stream.go b/mockserver/store/plain/stream.go new file mode 100644 index 00000000..34924754 --- /dev/null +++ b/mockserver/store/plain/stream.go @@ -0,0 +1,117 @@ +package plain + +import ( + "fmt" + + "github.com/suzuki-shunsuke/go-graylog" + st "github.com/suzuki-shunsuke/go-graylog/mockserver/store" +) + +// HasStream returns whether the stream exists. +func (store *Store) HasStream(id string) (bool, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + _, ok := store.streams[id] + return ok, nil +} + +// GetStream returns a stream. +func (store *Store) GetStream(id string) (*graylog.Stream, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + s, ok := store.streams[id] + if ok { + return &s, nil + } + return nil, nil +} + +// AddStream adds a stream to the store. +func (store *Store) AddStream(stream *graylog.Stream) error { + if stream == nil { + return fmt.Errorf("stream is nil") + } + if stream.ID == "" { + stream.ID = st.NewObjectID() + } + + store.imutex.Lock() + defer store.imutex.Unlock() + store.streams[stream.ID] = *stream + return nil +} + +// UpdateStream updates a stream at the store. +func (store *Store) UpdateStream(prms *graylog.StreamUpdateParams) (*graylog.Stream, error) { + store.imutex.Lock() + defer store.imutex.Unlock() + stream, ok := store.streams[prms.ID] + if !ok { + return nil, fmt.Errorf("the stream <%s> is not found", prms.ID) + } + if prms.Title != "" { + stream.Title = prms.Title + } + if prms.IndexSetID != "" { + stream.IndexSetID = prms.IndexSetID + } + if prms.Description != "" { + stream.Description = prms.Description + } + if prms.Outputs != nil { + stream.Outputs = prms.Outputs + } + if prms.MatchingType != "" { + stream.MatchingType = prms.MatchingType + } + if prms.Rules != nil { + stream.Rules = prms.Rules + } + if prms.AlertConditions != nil { + stream.AlertConditions = prms.AlertConditions + } + if prms.AlertReceivers != nil { + stream.AlertReceivers = prms.AlertReceivers + } + if prms.RemoveMatchesFromDefaultStream != nil { + stream.RemoveMatchesFromDefaultStream = *prms.RemoveMatchesFromDefaultStream + } + store.streams[stream.ID] = stream + return &stream, nil +} + +// DeleteStream removes a stream from the store. +func (store *Store) DeleteStream(id string) error { + store.imutex.Lock() + defer store.imutex.Unlock() + delete(store.streams, id) + return nil +} + +// GetStreams returns a list of all streams. +func (store *Store) GetStreams() ([]graylog.Stream, int, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + total := len(store.streams) + arr := make([]graylog.Stream, total) + i := 0 + for _, index := range store.streams { + arr[i] = index + i++ + } + return arr, total, nil +} + +// GetEnabledStreams returns all enabled streams. +func (store *Store) GetEnabledStreams() ([]graylog.Stream, int, error) { + arr := []graylog.Stream{} + store.imutex.RLock() + defer store.imutex.RUnlock() + for _, index := range store.streams { + if index.Disabled { + continue + } + arr = append(arr, index) + } + return arr, len(arr), nil +} diff --git a/mockserver/store/plain/stream_rule.go b/mockserver/store/plain/stream_rule.go new file mode 100644 index 00000000..ac2eeb1c --- /dev/null +++ b/mockserver/store/plain/stream_rule.go @@ -0,0 +1,120 @@ +package plain + +import ( + "fmt" + + "github.com/suzuki-shunsuke/go-graylog" + st "github.com/suzuki-shunsuke/go-graylog/mockserver/store" +) + +// HasStreamRule returns whether the stream rule exists. +func (store *Store) HasStreamRule(streamID, streamRuleID string) (bool, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + rules, ok := store.streamRules[streamID] + if !ok { + return false, nil + } + _, ok = rules[streamRuleID] + return ok, nil +} + +// GetStreamRule returns a stream rule. +func (store *Store) GetStreamRule(streamID, streamRuleID string) (*graylog.StreamRule, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + rules, ok := store.streamRules[streamID] + if !ok { + return nil, nil + } + rule, ok := rules[streamRuleID] + if ok { + return &rule, nil + } + return nil, nil +} + +// GetStreamRules returns stream rules of the given stream. +func (store *Store) GetStreamRules(id string) ([]graylog.StreamRule, int, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + rules, ok := store.streamRules[id] + if !ok { + return nil, 0, nil + } + size := len(rules) + arr := make([]graylog.StreamRule, size) + i := 0 + for _, rule := range rules { + arr[i] = rule + i++ + } + return arr, size, nil +} + +// AddStreamRule adds a stream rule. +func (store *Store) AddStreamRule(rule *graylog.StreamRule) error { + if rule == nil { + return fmt.Errorf("rule is nil") + } + store.imutex.Lock() + defer store.imutex.Unlock() + rules, ok := store.streamRules[rule.StreamID] + if !ok { + rules = map[string]graylog.StreamRule{} + } + if rule.ID == "" { + rule.ID = st.NewObjectID() + } + rules[rule.ID] = *rule + store.streamRules[rule.StreamID] = rules + return nil +} + +// UpdateStreamRule updates a stream rule. +func (store *Store) UpdateStreamRule(prms *graylog.StreamRuleUpdateParams) error { + if prms == nil { + return fmt.Errorf("rule is nil") + } + store.imutex.Lock() + defer store.imutex.Unlock() + rules, ok := store.streamRules[prms.StreamID] + if !ok { + return fmt.Errorf("no stream with id <%s> is found", prms.StreamID) + } + rule, ok := rules[prms.ID] + if !ok { + return fmt.Errorf("no stream rule with id <%s> is found", prms.ID) + } + if prms.Field != "" { + rule.Field = prms.Field + } + if prms.Description != "" { + rule.Description = prms.Description + } + if prms.Value != "" { + rule.Value = prms.Value + } + if prms.Type != nil { + rule.Type = *prms.Type + } + if prms.Inverted != nil { + rule.Inverted = *prms.Inverted + } + rules[rule.ID] = rule + store.streamRules[rule.StreamID] = rules + return nil +} + +// DeleteStreamRule deletes a stream rule. +func (store *Store) DeleteStreamRule(streamID, streamRuleID string) error { + store.imutex.Lock() + defer store.imutex.Unlock() + rules, ok := store.streamRules[streamID] + if !ok { + return nil + } + delete(rules, streamRuleID) + store.streamRules[streamID] = rules + return nil +} diff --git a/mockserver/store/plain/stream_rule_test.go b/mockserver/store/plain/stream_rule_test.go new file mode 100644 index 00000000..86728210 --- /dev/null +++ b/mockserver/store/plain/stream_rule_test.go @@ -0,0 +1,128 @@ +package plain_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestHasStreamRule(t *testing.T) { + store := plain.NewStore("") + _, err := store.HasStreamRule("", "") + if err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + rule := testutil.StreamRule() + if err := store.AddStream(stream); err != nil { + t.Fatal(err) + } + rule.StreamID = stream.ID + if err := store.AddStreamRule(rule); err != nil { + t.Fatal(err) + } + _, err = store.HasStreamRule(stream.ID, rule.ID) + if err != nil { + t.Fatal(err) + } +} + +func TestGetStreamRule(t *testing.T) { + store := plain.NewStore("") + _, err := store.GetStreamRule("", "") + if err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + rule := testutil.StreamRule() + if err := store.AddStream(stream); err != nil { + t.Fatal(err) + } + rule.StreamID = stream.ID + if err := store.AddStreamRule(rule); err != nil { + t.Fatal(err) + } + _, err = store.GetStreamRule(stream.ID, rule.ID) + if err != nil { + t.Fatal(err) + } + _, err = store.GetStreamRule(stream.ID, "") + if err != nil { + t.Fatal(err) + } +} + +func TestGetStreamRules(t *testing.T) { + store := plain.NewStore("") + _, _, err := store.GetStreamRules("") + if err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + rule := testutil.StreamRule() + if err := store.AddStream(stream); err != nil { + t.Fatal(err) + } + rule.StreamID = stream.ID + if err := store.AddStreamRule(rule); err != nil { + t.Fatal(err) + } + _, _, err = store.GetStreamRules(stream.ID) + if err != nil { + t.Fatal(err) + } +} + +func TestAddStreamRule(t *testing.T) { + store := plain.NewStore("") + if err := store.AddStreamRule(nil); err == nil { + t.Fatal("rule is nil") + } + if err := store.AddStreamRule(&graylog.StreamRule{}); err != nil { + t.Fatal(err) + } +} + +func TestUpdateStreamRule(t *testing.T) { + store := plain.NewStore("") + stream := testutil.Stream() + if err := store.AddStream(stream); err != nil { + t.Fatal(err) + } + rule := testutil.StreamRule() + rule.StreamID = stream.ID + if err := store.AddStreamRule(rule); err != nil { + t.Fatal(err) + } + if err := store.UpdateStreamRule(rule.NewUpdateParams()); err != nil { + t.Fatal(err) + } + rule.StreamID = "" + if err := store.UpdateStreamRule(rule.NewUpdateParams()); err == nil { + t.Fatal("stream id is empty") + } + if err := store.UpdateStreamRule(nil); err == nil { + t.Fatal("rule is nil") + } +} + +func TestDeleteStreamRule(t *testing.T) { + store := plain.NewStore("") + if err := store.DeleteStreamRule("", ""); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + rule := testutil.StreamRule() + if err := store.AddStream(stream); err != nil { + t.Fatal(err) + } + rule.StreamID = stream.ID + if err := store.AddStreamRule(rule); err != nil { + t.Fatal(err) + } + if err := store.DeleteStreamRule(stream.ID, rule.ID); err != nil { + t.Fatal(err) + } +} diff --git a/mockserver/store/plain/stream_test.go b/mockserver/store/plain/stream_test.go new file mode 100644 index 00000000..3dc6ee87 --- /dev/null +++ b/mockserver/store/plain/stream_test.go @@ -0,0 +1,129 @@ +package plain_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/store" + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestHasStream(t *testing.T) { + store := plain.NewStore("") + ok, err := store.HasStream("foo") + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("stream foo should not exist") + } +} + +func TestGetStream(t *testing.T) { + store := plain.NewStore("") + is, err := store.GetStream("01") + if err != nil { + t.Fatal(err) + } + if is != nil { + t.Fatal("stream foo should not exist") + } +} + +func TestGetStreams(t *testing.T) { + store := plain.NewStore("") + streams, _, err := store.GetStreams() + if err != nil { + t.Fatal(err) + } + if len(streams) != 0 { + t.Fatal("streams should be nil or empty array") + } +} + +func TestAddStream(t *testing.T) { + st := plain.NewStore("") + is := testutil.IndexSet("hoge") + is.ID = store.NewObjectID() + if err := st.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.ID = store.NewObjectID() + stream.IndexSetID = is.ID + if err := st.AddStream(stream); err != nil { + t.Fatal(err) + } + r, err := st.GetStream(stream.ID) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("is is nil") + } +} + +func TestUpdateStream(t *testing.T) { + st := plain.NewStore("") + is := testutil.IndexSet("hoge") + is.ID = store.NewObjectID() + if err := st.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream := testutil.Stream() + stream.ID = store.NewObjectID() + stream.IndexSetID = is.ID + if err := st.AddStream(stream); err != nil { + t.Fatal(err) + } + stream.Title += " changed" + if _, err := st.UpdateStream(stream.NewUpdateParams()); err != nil { + t.Fatal(err) + } + r, err := st.GetStream(stream.ID) + if err != nil { + t.Fatal(err) + } + if stream.Title != r.Title { + t.Fatalf(`stream.Title = "%s", wanted "%s"`, r.Title, stream.Title) + } +} + +func TestDeleteStream(t *testing.T) { + st := plain.NewStore("") + stream := testutil.Stream() + stream.ID = store.NewObjectID() + if err := st.DeleteStream(stream.ID); err != nil { + t.Fatal(err) + } + is := testutil.IndexSet("hoge") + is.ID = store.NewObjectID() + if err := st.AddIndexSet(is); err != nil { + t.Fatal(err) + } + stream.IndexSetID = is.ID + if err := st.AddStream(stream); err != nil { + t.Fatal(err) + } + if err := st.DeleteStream(stream.ID); err != nil { + t.Fatal(err) + } + ok, err := st.HasStream(stream.ID) + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("stream should be deleted") + } +} + +func TestGetEnabledStreams(t *testing.T) { + store := plain.NewStore("") + streams, _, err := store.GetEnabledStreams() + if err != nil { + t.Fatal(err) + } + if len(streams) != 0 { + t.Fatal("streams should be nil or empty array") + } +} diff --git a/mockserver/store/plain/user.go b/mockserver/store/plain/user.go new file mode 100644 index 00000000..7f7e725e --- /dev/null +++ b/mockserver/store/plain/user.go @@ -0,0 +1,120 @@ +package plain + +import ( + "fmt" + + "github.com/suzuki-shunsuke/go-graylog" + st "github.com/suzuki-shunsuke/go-graylog/mockserver/store" +) + +// HasUser returns whether the user exists. +func (store *Store) HasUser(username string) (bool, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + _, ok := store.users[username] + return ok, nil +} + +// GetUser returns a user. +// If the user is not found, this method returns nil and doesn't raise an error. +func (store *Store) GetUser(username string) (*graylog.User, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + s, ok := store.users[username] + if ok { + return &s, nil + } + return nil, nil +} + +// GetUsers returns users +func (store *Store) GetUsers() ([]graylog.User, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + arr := make([]graylog.User, len(store.users)) + i := 0 + for _, user := range store.users { + arr[i] = user + i++ + } + return arr, nil +} + +// AddUser adds a user to the Store. +func (store *Store) AddUser(user *graylog.User) error { + if user == nil { + return fmt.Errorf("user is nil") + } + if user.ID == "" { + user.ID = st.NewObjectID() + } + store.imutex.Lock() + defer store.imutex.Unlock() + store.users[user.Username] = *user + return nil +} + +// UpdateUser updates a user of the Store. +// "email", "permissions", "full_name", "password" +func (store *Store) UpdateUser(prms *graylog.UserUpdateParams) error { + if prms == nil { + return fmt.Errorf("user is nil") + } + store.imutex.Lock() + defer store.imutex.Unlock() + user, ok := store.users[prms.Username] + if !ok { + return fmt.Errorf(`the user "%s" is not found`, prms.Username) + } + + if prms.Email != nil { + user.Email = *prms.Email + } + if prms.FullName != nil { + user.FullName = *prms.FullName + } + if prms.Password != nil { + user.Password = *prms.Password + } + if prms.Timezone != nil { + user.Timezone = *prms.Timezone + } + if prms.SessionTimeoutMs != nil { + user.SessionTimeoutMs = *prms.SessionTimeoutMs + } + if prms.Permissions != nil { + user.Permissions = prms.Permissions + } + if prms.Startpage != nil { + user.Startpage = prms.Startpage + } + if prms.Roles != nil { + user.Roles = prms.Roles + } + store.users[user.Username] = user + return nil +} + +// DeleteUser removes a user from the Store. +func (store *Store) DeleteUser(name string) error { + store.imutex.Lock() + defer store.imutex.Unlock() + delete(store.users, name) + return nil +} + +// GetUserByAccessToken returns a user name. +// If the user is not found, this method returns nil and doesn't raise an error. +func (store *Store) GetUserByAccessToken(token string) (*graylog.User, error) { + store.imutex.RLock() + defer store.imutex.RUnlock() + username, ok := store.tokens[token] + if !ok { + return nil, nil + } + s, ok := store.users[username] + if ok { + return &s, nil + } + return nil, nil +} diff --git a/mockserver/store/plain/user_test.go b/mockserver/store/plain/user_test.go new file mode 100644 index 00000000..cd6049be --- /dev/null +++ b/mockserver/store/plain/user_test.go @@ -0,0 +1,103 @@ +package plain_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/store/plain" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestHasUser(t *testing.T) { + store := plain.NewStore("") + ok, err := store.HasUser("foo") + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("user foo should not exist") + } +} + +func TestGetUser(t *testing.T) { + store := plain.NewStore("") + user, err := store.GetUser("foo") + if err != nil { + t.Fatal(err) + } + if user != nil { + t.Fatal("user foo should not exist") + } +} + +func TestGetUsers(t *testing.T) { + store := plain.NewStore("") + users, err := store.GetUsers() + if err != nil { + t.Fatal(err) + } + if len(users) != 0 { + t.Fatal("users should be nil or empty array") + } +} + +func TestAddUser(t *testing.T) { + store := plain.NewStore("") + user := testutil.User() + if err := store.AddUser(user); err != nil { + t.Fatal(err) + } + r, err := store.GetUser(user.Username) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("user is nil") + } +} + +func TestUpdateUser(t *testing.T) { + store := plain.NewStore("") + user := testutil.User() + if err := store.AddUser(user); err != nil { + t.Fatal(err) + } + r, err := store.GetUser(user.Username) + if err != nil { + t.Fatal(err) + } + if r == nil { + t.Fatal("user is nil") + } + user.FullName += " changed" + if err := store.UpdateUser(user.NewUpdateParams()); err != nil { + t.Fatal(err) + } + r, err = store.GetUser(user.Username) + if err != nil { + t.Fatal(err) + } + if user.FullName != r.FullName { + t.Fatalf(`user.FullName = "%s", wanted "%s"`, r.FullName, user.FullName) + } +} + +func TestDeleteUser(t *testing.T) { + store := plain.NewStore("") + if err := store.DeleteUser("foo"); err != nil { + t.Fatal(err) + } + user := testutil.User() + if err := store.AddUser(user); err != nil { + t.Fatal(err) + } + if err := store.DeleteUser(user.Username); err != nil { + t.Fatal(err) + } + ok, err := store.HasUser(user.Username) + if err != nil { + t.Fatal(err) + } + if ok { + t.Fatal("user should be deleted") + } +} diff --git a/mockserver/store/store.go b/mockserver/store/store.go new file mode 100644 index 00000000..d8a28591 --- /dev/null +++ b/mockserver/store/store.go @@ -0,0 +1,67 @@ +package store + +import ( + "github.com/suzuki-shunsuke/go-graylog" +) + +// Store manage data. +// Basically Store doesn't have responsibility to validate a request from user. +type Store interface { + Save() error + Load() error + Authorize(user *graylog.User, scope string, args ...string) (bool, error) + + AddRole(*graylog.Role) error + // GetRole returns a role. + // If no role with given name is found, returns nil and not returns an error. + GetRole(name string) (*graylog.Role, error) + GetRoles() ([]graylog.Role, int, error) + UpdateRole(name string, role *graylog.RoleUpdateParams) (*graylog.Role, error) + DeleteRole(name string) error + HasRole(name string) (bool, error) + + AddUser(user *graylog.User) error + GetUser(username string) (*graylog.User, error) + GetUsers() ([]graylog.User, error) + UpdateUser(*graylog.UserUpdateParams) error + DeleteUser(name string) error + HasUser(username string) (bool, error) + GetUserByAccessToken(token string) (*graylog.User, error) + + AddInput(*graylog.Input) error + GetInput(id string) (*graylog.Input, error) + GetInputs() ([]graylog.Input, int, error) + UpdateInput(*graylog.InputUpdateParams) (*graylog.Input, error) + DeleteInput(id string) error + HasInput(id string) (bool, error) + + AddIndexSet(*graylog.IndexSet) error + GetIndexSet(id string) (*graylog.IndexSet, error) + GetIndexSets(skip, limit int) ([]graylog.IndexSet, int, error) + UpdateIndexSet(*graylog.IndexSetUpdateParams) (*graylog.IndexSet, error) + DeleteIndexSet(id string) error + HasIndexSet(id string) (bool, error) + IsConflictIndexPrefix(id, indexPrefix string) (bool, error) + SetDefaultIndexSetID(id string) error + GetDefaultIndexSetID() (string, error) + + // SetIndexSetStats(id string, stats *graylog.IndexSetStats) error + GetIndexSetStats(id string) (*graylog.IndexSetStats, error) + GetTotalIndexSetStats() (*graylog.IndexSetStats, error) + GetIndexSetStatsMap() (map[string]graylog.IndexSetStats, error) + + AddStream(*graylog.Stream) error + GetStream(id string) (*graylog.Stream, error) + GetStreams() ([]graylog.Stream, int, error) + GetEnabledStreams() ([]graylog.Stream, int, error) + UpdateStream(*graylog.StreamUpdateParams) (*graylog.Stream, error) + DeleteStream(id string) error + HasStream(id string) (bool, error) + + AddStreamRule(*graylog.StreamRule) error + GetStreamRules(id string) ([]graylog.StreamRule, int, error) + GetStreamRule(streamID, streamRuleID string) (*graylog.StreamRule, error) + UpdateStreamRule(*graylog.StreamRuleUpdateParams) error + DeleteStreamRule(streamID, streamRuleID string) error + HasStreamRule(streamID, streamRuleID string) (bool, error) +} diff --git a/mockserver/store/util.go b/mockserver/store/util.go new file mode 100644 index 00000000..d8678e1b --- /dev/null +++ b/mockserver/store/util.go @@ -0,0 +1,10 @@ +package store + +import ( + "gopkg.in/mgo.v2/bson" +) + +// NewObjectID returns a new ObjectId. +func NewObjectID() string { + return bson.NewObjectId().Hex() +} diff --git a/mockserver/store/util_test.go b/mockserver/store/util_test.go new file mode 100644 index 00000000..bd5ede53 --- /dev/null +++ b/mockserver/store/util_test.go @@ -0,0 +1,13 @@ +package store_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/mockserver/store" +) + +func TestNewObjectID(t *testing.T) { + if id := store.NewObjectID(); len(id) != 24 { + t.Fatalf(`len(id) = %d, wanted 24: %s`, len(id), id) + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..f4b46b8a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2305 @@ +{ + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@commitlint/cli": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-6.1.3.tgz", + "integrity": "sha1-RgbhOLBbQwNNyErxXaGOITkFhTc=", + "dev": true, + "requires": { + "@commitlint/format": "6.1.3", + "@commitlint/lint": "6.1.3", + "@commitlint/load": "6.1.3", + "@commitlint/read": "6.1.3", + "babel-polyfill": "6.26.0", + "chalk": "2.3.1", + "get-stdin": "5.0.1", + "lodash.merge": "4.6.1", + "lodash.pick": "4.4.0", + "meow": "3.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "get-stdin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", + "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "@commitlint/config-conventional": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-6.1.3.tgz", + "integrity": "sha1-bAburgTFrHicNhjfTVKu2on/uBA=", + "dev": true + }, + "@commitlint/ensure": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-6.1.3.tgz", + "integrity": "sha1-gTtYyf364VNRty/mRqFi69tx6io=", + "dev": true, + "requires": { + "lodash.camelcase": "4.3.0", + "lodash.kebabcase": "4.1.1", + "lodash.snakecase": "4.1.1", + "lodash.startcase": "4.4.0", + "lodash.upperfirst": "4.3.1" + } + }, + "@commitlint/execute-rule": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-6.1.3.tgz", + "integrity": "sha1-SJKOc27xXocQ0zKhXHyJlVXk4Qs=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "@commitlint/format": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-6.1.3.tgz", + "integrity": "sha1-QUuQSKmvVFh9qWIicXujMjR6veM=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "chalk": "2.4.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.0.tgz", + "integrity": "sha512-Wr/w0f4o9LuE7K53cD0qmbAMM+2XNLzR29vFn5hqko4sxGlUsyy363NvmyGIyk5tpe9cjTr9SJYbysEyPkRnFw==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "@commitlint/is-ignored": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-6.1.3.tgz", + "integrity": "sha1-icm5ZKTWIoh1pXnCv1UtADc0t+g=", + "dev": true, + "requires": { + "semver": "5.5.0" + } + }, + "@commitlint/lint": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-6.1.3.tgz", + "integrity": "sha1-aneI6uOBrahz90IeMVWecgPKrfM=", + "dev": true, + "requires": { + "@commitlint/is-ignored": "6.1.3", + "@commitlint/parse": "6.1.3", + "@commitlint/rules": "6.1.3", + "babel-runtime": "6.26.0", + "lodash.topairs": "4.3.0" + } + }, + "@commitlint/load": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-6.1.3.tgz", + "integrity": "sha1-G+QHETl5WPMWz0BXepyHmhbwClQ=", + "dev": true, + "requires": { + "@commitlint/execute-rule": "6.1.3", + "@commitlint/resolve-extends": "6.1.3", + "babel-runtime": "6.26.0", + "cosmiconfig": "4.0.0", + "lodash.merge": "4.6.1", + "lodash.mergewith": "4.6.1", + "lodash.pick": "4.4.0", + "lodash.topairs": "4.3.0", + "resolve-from": "4.0.0" + } + }, + "@commitlint/message": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-6.1.3.tgz", + "integrity": "sha1-XgRzMwyIcBYBDExWJwcjuAARRdI=", + "dev": true + }, + "@commitlint/parse": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-6.1.3.tgz", + "integrity": "sha1-/x5NksJ81naBK7a512zYhTwNlAc=", + "dev": true, + "requires": { + "conventional-changelog-angular": "1.6.5", + "conventional-commits-parser": "2.1.4" + } + }, + "@commitlint/read": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-6.1.3.tgz", + "integrity": "sha1-n52NtQ+/Z/MACSFlftbvrbjPnxo=", + "dev": true, + "requires": { + "@commitlint/top-level": "6.1.3", + "@marionebl/sander": "0.6.1", + "babel-runtime": "6.26.0", + "git-raw-commits": "1.3.3" + } + }, + "@commitlint/resolve-extends": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-6.1.3.tgz", + "integrity": "sha1-9F/P5Dhg4F4489lNVMrtfdqkHiU=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "lodash.merge": "4.6.1", + "lodash.omit": "4.5.0", + "require-uncached": "1.0.3", + "resolve-from": "4.0.0", + "resolve-global": "0.1.0" + } + }, + "@commitlint/rules": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-6.1.3.tgz", + "integrity": "sha1-NOvSYsA3DUgwnlFnmUJNMsM/mEs=", + "dev": true, + "requires": { + "@commitlint/ensure": "6.1.3", + "@commitlint/message": "6.1.3", + "@commitlint/to-lines": "6.1.3", + "babel-runtime": "6.26.0" + } + }, + "@commitlint/to-lines": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-6.1.3.tgz", + "integrity": "sha1-erFqAsrtjapH6Vkmm5YWRhCinQw=", + "dev": true + }, + "@commitlint/top-level": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-6.1.3.tgz", + "integrity": "sha1-Em3LbeFnY0LGnNQiYUg/RHhUcpk=", + "dev": true, + "requires": { + "find-up": "2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + } + } + }, + "@commitlint/travis-cli": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@commitlint/travis-cli/-/travis-cli-6.1.3.tgz", + "integrity": "sha1-m/F1bZsyo7xFgOpArjypLId3OAA=", + "dev": true, + "requires": { + "@commitlint/cli": "6.1.3", + "babel-runtime": "6.26.0", + "execa": "0.9.0" + }, + "dependencies": { + "execa": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.9.0.tgz", + "integrity": "sha512-BbUMBiX4hqiHZUA5+JujIjNb6TyAlp2D5KLheMjMluwOuzcnylDL4AxZYLLn1n2AGB49eSWwyKvvEQoRpnAtmA==", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + } + } + }, + "@marionebl/sander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@marionebl/sander/-/sander-0.6.1.tgz", + "integrity": "sha1-GViWWHTyS8Ub5Ih1/rUNZC/EH3s=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "JSONStream": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", + "integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=", + "dev": true, + "requires": { + "jsonparse": "1.3.1", + "through": "2.3.8" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "core-js": "2.5.5", + "regenerator-runtime": "0.10.5" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "2.5.5", + "regenerator-runtime": "0.11.1" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + } + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "ci-info": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.2.tgz", + "integrity": "sha512-uTGIPNx/nSpBdsF6xnseRXLLtfr9VLqkz8ZqHXr3Y7b6SftyRxBGjwMtJj1OhNbmlc1wZzLNAlAcvyIiE8a6ZA==", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + } + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "compare-func": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz", + "integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=", + "dev": true, + "requires": { + "array-ify": "1.0.0", + "dot-prop": "3.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.4", + "typedarray": "0.0.6" + } + }, + "conventional-changelog": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-1.1.16.tgz", + "integrity": "sha512-7Z7B39PJeCbviJw6ukVOjoyWDGmSvlZj77UkPxXPLO03zGgHHADCDSu1yU/YfVsKkQiel4Ot0o7jTqbP8zIvVQ==", + "dev": true, + "requires": { + "conventional-changelog-angular": "1.6.5", + "conventional-changelog-atom": "0.2.3", + "conventional-changelog-codemirror": "0.3.3", + "conventional-changelog-core": "2.0.4", + "conventional-changelog-ember": "0.3.5", + "conventional-changelog-eslint": "1.0.3", + "conventional-changelog-express": "0.3.3", + "conventional-changelog-jquery": "0.1.0", + "conventional-changelog-jscs": "0.1.0", + "conventional-changelog-jshint": "0.3.3", + "conventional-changelog-preset-loader": "1.1.5" + } + }, + "conventional-changelog-angular": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-1.6.5.tgz", + "integrity": "sha512-70zO0ThLMlAzPOiYqRAFcMTQbe1Qewy+4v/TJuKMZueCJwR1fLqJCVfvjRlnPrYDwgjI0kc74VymbFO7rJDIPg==", + "dev": true, + "requires": { + "compare-func": "1.3.2", + "q": "1.5.1" + } + }, + "conventional-changelog-atom": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-0.2.3.tgz", + "integrity": "sha512-ZhhcyBeMQQbQ/eqanVb9bFJfS7ScsPgvlbPWLrL5HgcfdO9yGSaEp/hLvXIZ2tYYDd8e5Y0AB+5C4tiSE2K4EQ==", + "dev": true, + "requires": { + "q": "1.5.1" + } + }, + "conventional-changelog-codemirror": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-0.3.3.tgz", + "integrity": "sha512-9vYteJT6F3Ao3CtgXYg1JCRdzLI4qqbH3GdHL0OSjEe/FQjsJRcGHqpbUQ1SQ0ga+vqAJlJdJNib9BwHIQF4mw==", + "dev": true, + "requires": { + "q": "1.5.1" + } + }, + "conventional-changelog-core": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-2.0.4.tgz", + "integrity": "sha512-XsmaKcbfewovP72N5w17TNV6fqZYTF0jnNKG0V/OhPsIZETFvFzJwPHCDM3FrRNvDXWRvqb2M0a83cvYhHbvzw==", + "dev": true, + "requires": { + "conventional-changelog-writer": "3.0.3", + "conventional-commits-parser": "2.1.4", + "dateformat": "1.0.12", + "get-pkg-repo": "1.4.0", + "git-raw-commits": "1.3.3", + "git-remote-origin-url": "2.0.0", + "git-semver-tags": "1.3.3", + "lodash": "4.17.5", + "normalize-package-data": "2.4.0", + "q": "1.5.1", + "read-pkg": "1.1.0", + "read-pkg-up": "1.0.1", + "through2": "2.0.3" + } + }, + "conventional-changelog-ember": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-0.3.5.tgz", + "integrity": "sha512-A55p1kg/ekyU2zTEScdRKwuHaCUfDocakIPoaxdCxsRQ1732C4Em4u6lbN0F1jQHoCsQqqA1aPAEOMKgDicnbA==", + "dev": true, + "requires": { + "q": "1.5.1" + } + }, + "conventional-changelog-eslint": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-1.0.3.tgz", + "integrity": "sha512-4Y1/9TX16RvWiOdMnJQ8flxZ+hdDGVm1E4yrWvyr1L1UBWM56CJobMRg7nf+LqqVnKj0kkuyhwf3WV9//luYHg==", + "dev": true, + "requires": { + "q": "1.5.1" + } + }, + "conventional-changelog-express": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-0.3.3.tgz", + "integrity": "sha512-ivIa/9a05xxcA4bH1jQFpzwCEJywDirHhcQ6OQwYvAy0/ekRfGXA9U2ULVqE1WRqcmpFuayfAuysE9BJjqthEQ==", + "dev": true, + "requires": { + "q": "1.5.1" + } + }, + "conventional-changelog-jquery": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-0.1.0.tgz", + "integrity": "sha1-Agg5cWLjhGmG5xJztsecW1+A9RA=", + "dev": true, + "requires": { + "q": "1.5.1" + } + }, + "conventional-changelog-jscs": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-jscs/-/conventional-changelog-jscs-0.1.0.tgz", + "integrity": "sha1-BHnrRDzH1yxYvwvPDvHURKkvDlw=", + "dev": true, + "requires": { + "q": "1.5.1" + } + }, + "conventional-changelog-jshint": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-0.3.3.tgz", + "integrity": "sha512-RuGFuOp101qhxQdV+h4Dhdeqs1ZsycdoqN14GZ98E3MiDGQ++jfH6wjgUjU8uv8jULyrkLPenIWC0ooxQYneCw==", + "dev": true, + "requires": { + "compare-func": "1.3.2", + "q": "1.5.1" + } + }, + "conventional-changelog-preset-loader": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-1.1.5.tgz", + "integrity": "sha512-ngaSOqeyH5hYYdvl+bcJNEiT9B2c8cSRpIZtnFxbRyH7MVQbLOEn5oPV9NpQM00hTyBtqDviNDCXmy/Al6Gq8w==", + "dev": true + }, + "conventional-changelog-writer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-3.0.3.tgz", + "integrity": "sha512-gzkAFnFxjEOBCwISTBUJ6DnthMwzqY1MRyElN6S25VYBcRV/6DOVbbZgbBL1KdsogO6Z/QuJlSnIH7OteVd5lQ==", + "dev": true, + "requires": { + "compare-func": "1.3.2", + "conventional-commits-filter": "1.1.4", + "dateformat": "1.0.12", + "handlebars": "4.0.11", + "json-stringify-safe": "5.0.1", + "lodash": "4.17.5", + "meow": "3.7.0", + "semver": "5.5.0", + "split": "1.0.1", + "through2": "2.0.3" + } + }, + "conventional-commits-filter": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-1.1.4.tgz", + "integrity": "sha512-VaLmHo+P1IYQenZwkgp21y9z5tyU8sH3GKI/AfthQYPK9IYhT4lNbATGW9Iona+BU3fmWw/9S3L5vMopMI+jkA==", + "dev": true, + "requires": { + "is-subset": "0.1.1", + "modify-values": "1.0.0" + } + }, + "conventional-commits-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-2.1.4.tgz", + "integrity": "sha512-dyTFuqQQzTynPN81zk9Pvm6d4mGiTuPz+Qi1ee8CmhupBji7gwO0DKXvAWo3MEFilE50pm8h8kRbETL5wsI65A==", + "dev": true, + "requires": { + "JSONStream": "1.3.2", + "is-text-path": "1.0.1", + "lodash": "4.17.5", + "meow": "3.7.0", + "split2": "2.2.0", + "through2": "2.0.3", + "trim-off-newlines": "1.0.1" + } + }, + "conventional-recommended-bump": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-1.2.1.tgz", + "integrity": "sha512-oJjG6DkRgtnr/t/VrPdzmf4XZv8c4xKVJrVT4zrSHd92KEL+EYxSbYoKq8lQ7U5yLMw7130wrcQTLRjM/T+d4w==", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "conventional-commits-filter": "1.1.4", + "conventional-commits-parser": "2.1.4", + "git-raw-commits": "1.3.3", + "git-semver-tags": "1.3.3", + "meow": "3.7.0", + "object-assign": "4.1.1" + } + }, + "core-js": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz", + "integrity": "sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz", + "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==", + "dev": true, + "requires": { + "is-directory": "0.3.1", + "js-yaml": "3.11.0", + "parse-json": "4.0.0", + "require-from-string": "2.0.2" + }, + "dependencies": { + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "1.3.1", + "json-parse-better-errors": "1.0.2" + } + } + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "dargs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", + "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "dot-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", + "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", + "dev": true, + "requires": { + "is-obj": "1.0.1" + } + }, + "dotgitignore": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dotgitignore/-/dotgitignore-1.0.3.tgz", + "integrity": "sha512-eu5XjSstm0WXQsARgo6kPjkINYZlOUW+z/KtAAIBjHa5mUpMPrxJytbPIndWz6GubBuuuH5ljtVcXKnVnH5q8w==", + "dev": true, + "requires": { + "find-up": "2.1.0", + "minimatch": "3.0.4" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + } + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5", + "object-assign": "4.1.1" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "fs-access": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", + "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", + "dev": true, + "requires": { + "null-check": "1.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "dev": true + }, + "get-pkg-repo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", + "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "meow": "3.7.0", + "normalize-package-data": "2.4.0", + "parse-github-repo-url": "1.4.1", + "through2": "2.0.3" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "git-raw-commits": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-1.3.3.tgz", + "integrity": "sha512-EKgNIRhpCYFgLTM+o4lbu5+JxTGhUBgaRN3JQv/X2dm7zuSf64tay/igxf2RMJfOZYpmUYX96JOk3Q7JajPLVA==", + "dev": true, + "requires": { + "dargs": "4.1.0", + "lodash.template": "4.4.0", + "meow": "3.7.0", + "split2": "2.2.0", + "through2": "2.0.3" + } + }, + "git-remote-origin-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", + "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", + "dev": true, + "requires": { + "gitconfiglocal": "1.0.0", + "pify": "2.3.0" + } + }, + "git-semver-tags": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-1.3.3.tgz", + "integrity": "sha512-FK/ZQeFwANfsoo+3FFhO1XRVVKgSZgcO0ABtydFrPQj7U2N5ELVr8MxBqlXa5TdpRKTp6/H5ki1cK2Anxr2kJw==", + "dev": true, + "requires": { + "meow": "3.7.0", + "semver": "5.5.0" + } + }, + "gitconfiglocal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", + "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", + "dev": true, + "requires": { + "ini": "1.3.5" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "1.3.5" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "dev": true + }, + "husky": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-0.14.3.tgz", + "integrity": "sha512-e21wivqHpstpoiWA/Yi8eFti8E+sQDSS53cpJsPptPs295QTOQR0ZwnHo2TXy1XOpZFD9rPOd3NpmqTK6uMLJA==", + "dev": true, + "requires": { + "is-ci": "1.1.0", + "normalize-path": "1.0.0", + "strip-indent": "2.0.0" + } + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-ci": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", + "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "dev": true, + "requires": { + "ci-info": "1.1.2" + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", + "dev": true + }, + "is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", + "dev": true, + "requires": { + "text-extensions": "1.7.0" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-yaml": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", + "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "dev": true, + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.0" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha1-hImxyw0p/4gZXM7KRI/21swpXDY=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", + "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==", + "dev": true + }, + "lodash.mergewith": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", + "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==", + "dev": true + }, + "lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=", + "dev": true + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=", + "dev": true + }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=", + "dev": true + }, + "lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha1-lDbjTtJgk+1/+uGTYUQ1CRXZrdg=", + "dev": true + }, + "lodash.template": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", + "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", + "dev": true, + "requires": { + "lodash._reinterpolate": "3.0.0", + "lodash.templatesettings": "4.1.0" + } + }, + "lodash.templatesettings": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", + "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", + "dev": true, + "requires": { + "lodash._reinterpolate": "3.0.0" + } + }, + "lodash.topairs": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", + "integrity": "sha1-O23qo31g+xFnE8RsXxfqGQ7EjWQ=", + "dev": true + }, + "lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "modify-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.0.tgz", + "integrity": "sha1-4rbN65zhn5kxelNyLz2/XfXqqrI=", + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.1" + } + }, + "normalize-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz", + "integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "null-check": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz", + "integrity": "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "0.0.10", + "wordwrap": "0.0.3" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + } + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "dev": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "dev": true, + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.2.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-github-repo-url": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", + "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz", + "integrity": "sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + }, + "dependencies": { + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + } + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "0.1.0", + "resolve-from": "1.0.1" + }, + "dependencies": { + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-global": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-0.1.0.tgz", + "integrity": "sha1-j7As/Vt9sgEY6IYxHxWvlb0V+9k=", + "dev": true, + "requires": { + "global-dirs": "0.1.1" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "dev": true, + "requires": { + "through2": "2.0.3" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "standard-version": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/standard-version/-/standard-version-4.3.0.tgz", + "integrity": "sha512-2UJ2BIUNa7+41PH4FvYicSQED2LCt2RXjmNFis+JZlxZtwzNnGn4uuL8WBUqHoC9b+bJ0AHIAX/bilzm+pGPeA==", + "dev": true, + "requires": { + "chalk": "1.1.3", + "conventional-changelog": "1.1.16", + "conventional-recommended-bump": "1.2.1", + "dotgitignore": "1.0.3", + "figures": "1.7.0", + "fs-access": "1.0.1", + "semver": "5.5.0", + "yargs": "8.0.2" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "text-extensions": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.7.0.tgz", + "integrity": "sha512-AKXZeDq230UaSzaO5s3qQUZOaC7iKbzq0jOFL614R7d9R593HLqAOL0cYoqLdkNrjBSOdmoQI06yigq1TSBXAg==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "requires": { + "readable-stream": "2.3.4", + "xtend": "4.0.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-off-newlines": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "dev": true, + "requires": { + "camelcase": "4.1.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "read-pkg-up": "2.0.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..478c0759 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "version": "0.1.0", + "scripts": { + "release": "standard-version", + "precommit": "bash precommit.sh", + "commitmsg": "commitlint -e $GIT_PARAMS", + "test": "bash test.sh", + "lint": "bash lint.sh", + "metalint": "gometalinter ./...", + "fmt": "find . -type d -name node_modules -prune -o -type d -name vendor -prune -o -type f -name \"*.go\" -print | xargs gofmt -l -s -w", + "cover": "make cover", + "cover-client": "make cover-client", + "cover-endpoint": "make cover-endpoint", + "cover-mockserver": "make cover-mockserver", + "cover-logic": "make cover-logic", + "cover-inmemory": "make cover-inmemory", + "build": "make build", + "upload": "make upload", + "commitlint-travis": "commitlint-travis" + }, + "devDependencies": { + "@commitlint/cli": "^6.1.3", + "@commitlint/config-conventional": "^6.1.3", + "@commitlint/travis-cli": "^6.1.3", + "husky": "^0.14.3", + "standard-version": "^4.3.0" + } +} diff --git a/precommit.sh b/precommit.sh new file mode 100644 index 00000000..7ced6b50 --- /dev/null +++ b/precommit.sh @@ -0,0 +1,12 @@ +set -e + +decho() { + echo "+ $@" + eval $@ +} + +# gofmt +echo "! git ls-files | grep \"\\.go$\" | xargs gofmt -s -d | grep '^'" +! git ls-files | grep "\.go$" | xargs gofmt -s -d | grep '^' +# go vet +decho go vet $(go list ./... | grep -v /vendor/) diff --git a/role.go b/role.go new file mode 100644 index 00000000..f7408695 --- /dev/null +++ b/role.go @@ -0,0 +1,39 @@ +package graylog + +import ( + "github.com/suzuki-shunsuke/go-ptr" + "github.com/suzuki-shunsuke/go-set" +) + +// Role represents a role. +type Role struct { + Name string `json:"name,omitempty" v-create:"required" v-update:"required"` + Description string `json:"description,omitempty"` + // ex. ["clusterconfigentry:read", "users:edit"] + Permissions *set.StrSet `json:"permissions,omitempty" v-create:"required" v-update:"required"` + ReadOnly bool `json:"read_only,omitempty"` +} + +// RoleUpdateParams represents Update Role API's parameters. +type RoleUpdateParams struct { + Name string `json:"name,omitempty" v-create:"required" v-update:"required"` + Description *string `json:"description,omitempty"` + // ex. ["clusterconfigentry:read", "users:edit"] + Permissions *set.StrSet `json:"permissions,omitempty" v-create:"required" v-update:"required"` +} + +// NewUpdateParams returns Update Role API's parameters. +func (role *Role) NewUpdateParams() *RoleUpdateParams { + return &RoleUpdateParams{ + Name: role.Name, + Description: ptr.PStr(role.Description), + Permissions: role.Permissions, + } +} + +// RolesBody represents Get Roles API's response body. +// Basically users don't use this struct, but this struct is public because some sub packages use this struct. +type RolesBody struct { + Roles []Role `json:"roles"` + Total int `json:"total"` +} diff --git a/role_test.go b/role_test.go new file mode 100644 index 00000000..13ef266d --- /dev/null +++ b/role_test.go @@ -0,0 +1,15 @@ +package graylog_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestRoleNewUpdateParams(t *testing.T) { + role := testutil.Role() + prms := role.NewUpdateParams() + if role.Name != prms.Name { + t.Fatalf(`prms.Name = "%s", wanted "%s"`, prms.Name, role.Name) + } +} diff --git a/stream.go b/stream.go new file mode 100644 index 00000000..a9131741 --- /dev/null +++ b/stream.go @@ -0,0 +1,81 @@ +package graylog + +import ( + "github.com/suzuki-shunsuke/go-ptr" +) + +// CloneStream +// POST /streams/{streamID}/clone Clone a stream +// TestMatchStream +// POST /streams/{streamID}/testMatch Test matching of a stream against a supplied message + +// Stream represents a steram. +type Stream struct { + ID string `json:"id,omitempty" v-create:"isdefault" v-update:"required,objectid"` + Title string `json:"title,omitempty" v-create:"required"` + IndexSetID string `json:"index_set_id,omitempty" v-create:"required"` + // ex. "2018-02-20T11:37:19.371Z" + CreatedAt string `json:"created_at,omitempty" v-create:"isdefault"` + // ex. local:admin + CreatorUserID string `json:"creator_user_id,omitempty" v-create:"isdefault"` + Description string `json:"description,omitempty"` + // ex. "AND" + MatchingType string `json:"matching_type,omitempty"` + Outputs []Output `json:"outputs,omitempty" v-create:"isdefault"` + Rules []StreamRule `json:"rules,omitempty"` + AlertConditions []AlertCondition `json:"alert_conditions,omitempty" v-create:"isdefault"` + AlertReceivers *AlertReceivers `json:"alert_receivers,omitempty" v-create:"isdefault"` + Disabled bool `json:"disabled,omitempty" v-create:"isdefault"` + RemoveMatchesFromDefaultStream bool `json:"remove_matches_from_default_stream,omitempty"` + IsDefault bool `json:"is_default,omitempty" v-create:"isdefault"` + // ContentPack `json:"content_pack,omitempty"` +} + +// StreamUpdateParams represents a steram update params. +type StreamUpdateParams struct { + ID string `json:"id,omitempty" v-update:"required,objectid"` + Title string `json:"title,omitempty"` + IndexSetID string `json:"index_set_id,omitempty"` + Description string `json:"description,omitempty"` + Outputs []Output `json:"outputs,omitempty"` + MatchingType string `json:"matching_type,omitempty"` + Rules []StreamRule `json:"rules,omitempty"` + AlertConditions []AlertCondition `json:"alert_conditions,omitempty"` + AlertReceivers *AlertReceivers `json:"alert_receivers,omitempty"` + RemoveMatchesFromDefaultStream *bool `json:"remove_matches_from_default_stream,omitempty"` +} + +// NewUpdateParams converts Stream to StreamUpdateParams. +func (stream *Stream) NewUpdateParams() *StreamUpdateParams { + return &StreamUpdateParams{ + ID: stream.ID, + Title: stream.Title, + IndexSetID: stream.IndexSetID, + Description: stream.Description, + Outputs: stream.Outputs, + MatchingType: stream.MatchingType, + Rules: stream.Rules, + AlertConditions: stream.AlertConditions, + AlertReceivers: stream.AlertReceivers, + RemoveMatchesFromDefaultStream: ptr.PBool(stream.RemoveMatchesFromDefaultStream), + } +} + +// Output represents an output. +type Output struct{} + +// AlertReceivers represents alert receivers. +type AlertReceivers struct { + Emails []string `json:"emails,omitempty"` + Users []string `json:"users,omitempty"` +} + +// AlertCondition represents an alert condition. +type AlertCondition struct{} + +// StreamsBody represents Get Streams API's response body. +// Basically users don't use this struct, but this struct is public because some sub packages use this struct. +type StreamsBody struct { + Total int `json:"total,omitempty"` + Streams []Stream `json:"streams,omitempty"` +} diff --git a/stream_rule.go b/stream_rule.go new file mode 100644 index 00000000..12af6e4a --- /dev/null +++ b/stream_rule.go @@ -0,0 +1,47 @@ +package graylog + +import ( + "github.com/suzuki-shunsuke/go-ptr" +) + +// StreamRule represents a stream rule. +type StreamRule struct { + ID string `json:"id,omitempty" v-create:"isdefault"` + StreamID string `json:"stream_id,omitempty" v-create:"required"` + Field string `json:"field,omitempty" v-create:"required"` + Value string `json:"value,omitempty" v-create:"required"` + Description string `json:"description,omitempty"` + Type int `json:"type,omitempty"` + Inverted bool `json:"inverted,omitempty"` +} + +// StreamRuleUpdateParams represents Update Stream API's paramteres. +type StreamRuleUpdateParams struct { + ID string `json:"id,omitempty" v-update:"required,objectid"` + StreamID string `json:"stream_id,omitempty" v-update:"required,objectid"` + Field string `json:"field,omitempty" v-update:"required"` + Value string `json:"value,omitempty" v-update:"required"` + Description string `json:"description,omitempty"` + Type *int `json:"type,omitempty"` + Inverted *bool `json:"inverted,omitempty"` +} + +// NewUpdateParams converts StreamRule to StreamRuleUpdateParams. +func (rule *StreamRule) NewUpdateParams() *StreamRuleUpdateParams { + return &StreamRuleUpdateParams{ + ID: rule.ID, + StreamID: rule.StreamID, + Field: rule.Field, + Description: rule.Description, + Type: ptr.PInt(rule.Type), + Inverted: ptr.PBool(rule.Inverted), + Value: rule.Value, + } +} + +// StreamRulesBody represents Get stream rules API's response body. +// Basically users don't use this struct, but this struct is public because some sub packages use this struct. +type StreamRulesBody struct { + Total int `json:"total"` + StreamRules []StreamRule `json:"stream_rules"` +} diff --git a/stream_rule_test.go b/stream_rule_test.go new file mode 100644 index 00000000..6adc9240 --- /dev/null +++ b/stream_rule_test.go @@ -0,0 +1,15 @@ +package graylog_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestStreamRuleNewUpdateParams(t *testing.T) { + rule := testutil.StreamRule() + prms := rule.NewUpdateParams() + if rule.ID != prms.ID { + t.Fatalf(`prms.ID = "%s", wanted "%s"`, prms.ID, rule.ID) + } +} diff --git a/stream_test.go b/stream_test.go new file mode 100644 index 00000000..4084acb1 --- /dev/null +++ b/stream_test.go @@ -0,0 +1,15 @@ +package graylog_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestStreamNewUpdateParams(t *testing.T) { + stream := testutil.Stream() + prms := stream.NewUpdateParams() + if stream.ID != prms.ID { + t.Fatalf(`prms.ID = "%s", wanted "%s"`, prms.ID, stream.ID) + } +} diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 00000000..d3528277 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,12 @@ +terraform-provider-graylog +vendor +.terraform +*.log +terraform.tfstate +terraform.tfstate.backup +terraform.tfvars +variables.tf +node_modules +coverage.txt +dist +main.tf diff --git a/terraform/Makefile b/terraform/Makefile new file mode 100644 index 00000000..0a758238 --- /dev/null +++ b/terraform/Makefile @@ -0,0 +1,27 @@ +TAG=edge +BIN=terraform-provider-graylog +$(BIN): *.go graylog/*.go + go build -o $(BIN) + terraform init +.terraform: + terraform init +plan: $(BIN) .terraform + terraform plan +apply: $(BIN) .terraform + terraform apply +test: + go test -count=1 -v -covermode=atomic ./... +cover: + go test -count=1 -v -coverprofile=coverage.txt -covermode=atomic ./graylog + go tool cover -html=coverage.txt +# https://github.com/mitchellh/gox +# brew install gox +# go get github.com/mitchellh/gox +build: + gox -output="dist/$(TAG)/$(BIN)_$(TAG)_{{.OS}}_{{.Arch}}" -osarch="darwin/amd64 linux/amd64 windows/amd64" . +# https://github.com/tcnksm/ghr +# brew tap tcnksm/ghr +# brew install ghr +# go get -u github.com/tcnksm/ghr +upload: + ghr $(TAG) dist/$(TAG) diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 00000000..442d65fc --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,53 @@ +# terraform-provider-graylog + +[![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/suzuki-shunsuke/go-graylog/terraform) +[![Build Status](https://travis-ci.org/suzuki-shunsuke/terraform-provider-graylog.svg?branch=master)](https://travis-ci.org/suzuki-shunsuke/terraform-provider-graylog) +[![codecov](https://codecov.io/gh/suzuki-shunsuke/go-graylog/branch/master/graph/badge.svg)](https://codecov.io/gh/suzuki-shunsuke/go-graylog) +[![Go Report Card](https://goreportcard.com/badge/github.com/suzuki-shunsuke/go-graylog)](https://goreportcard.com/report/github.com/suzuki-shunsuke/go-graylog) +[![GitHub last commit](https://img.shields.io/github/last-commit/suzuki-shunsuke/go-graylog.svg)](https://github.com/suzuki-shunsuke/go-graylog) +[![GitHub tag](https://img.shields.io/github/tag/suzuki-shunsuke/go-graylog.svg)](https://github.com/suzuki-shunsuke/go-graylog/releases) +[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/suzuki-shunsuke/go-graylog/master/LICENSE) + +terraform provider for [Graylog](https://www.graylog.org/). + +This is sub project of [go-graylog](https://github.com/suzuki-shunsuke/go-graylog). + +## Motivation + +http://docs.graylog.org/en/2.4/pages/users_and_roles/permission_system.html + +The Graylog permission system is extremely flexible but you can't utilize this flexibility from Web UI. +By using this provider, you can utilize this flexibility and manage the infrastructure as code. + +## Install + +Download binary and rename it `terraform-provider-graylog` (name is important) and install under $PATH. + +https://github.com/suzuki-shunsuke/go-graylog/releases + +## Example + +``` +// Role my-role-2 +resource "graylog_role" "my-role-2" { + name = "my-role-2" + permissions = ["users:edit"] + description = "Created by terraform" +} +``` + +## Variables + +name | Environment variable | description +--- | --- | --- +web_endpoint_uri | GRAYLOG_WEB_ENDPOINT_URI | +auth_name | GRAYLOG_AUTH_NAME | +auth_password | GRAYLOG_AUTH_PASSWORD | + +## Resources + +* [role](docs/role.md) +* [user](docs/user.md) +* [input](docs/input.md) +* [index_set](docs/index_set.md) +* [stream](docs/stream.md) diff --git a/terraform/docs/index_set.md b/terraform/docs/index_set.md new file mode 100644 index 00000000..eee60620 --- /dev/null +++ b/terraform/docs/index_set.md @@ -0,0 +1,60 @@ +# graylog_index_set + +https://github.com/suzuki-shunsuke/terraform-provider-graylog/blob/master/resource_index_set.go + +``` +resource "graylog_index_set" "test-index-set" { + title = "terraform test index set" + index_prefix = "terraform-test" + rotation_strategy_class = "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategy" + rotation_strategy = { + type = "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategyConfig" + } + retention_strategy_class = "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy" + retention_strategy = { + type = "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig" + } + index_analyzer = "standard" + shards = 4 + index_optimization_max_num_segments = 1 +} +``` + +## Argument Reference + +### Required Argument + +name | type | description +--- | --- | --- +title | string | +index_prefix | string | +rotation_strategy_class | string | +rotation_strategy | | +rotation_strategy.type | string | +rotation_strategy.max_docs_per_index | int | +rotation_strategy.max_size | int | +rotation_strategy.rotation_period | string | +retention_strategy_class | string | +retention_strategy | | +retention_strategy.type | string | +retention_strategy.max_number_of_indices | int | +index_analyzer | string | +shards | int | +index_optimization_max_num_segments | int | + +### Optional Argument + +name | default | type | description +--- | --- | --- | --- +description | "" | string | +replicas | 0 | int | +index_optimization_disabled | | bool | +writable | | bool | +default | | bool | +creation_date | computed | string | + +## Attrs Reference + +name | type | etc +--- | --- | --- +id | string | diff --git a/terraform/docs/input.md b/terraform/docs/input.md new file mode 100644 index 00000000..c2e7d213 --- /dev/null +++ b/terraform/docs/input.md @@ -0,0 +1,100 @@ +# graylog_input + +https://github.com/suzuki-shunsuke/terraform-provider-graylog/blob/master/resource_input.go + +``` +resource "graylog_input" "test" { + title = "terraform test" + type = "org.graylog2.inputs.syslog.udp.SyslogUDPInput" + configuration = { + bind_address = "0.0.0.0" + port = 514 + recv_buffer_size = 262144 + } +} +``` + +## Argument Reference + +### Required Argument + +name | type | description +--- | --- | --- +title | string | +type | string | +configuration | | + +### Optional Argument + +name | default | type | description +--- | --- | --- | --- +global | "" | string | +node | "" | string | +configuration.bind_address | string | +configuration.port | int | +configuration.recv_buffer_size | int | +configuration.heartbeat | int | +configuration.prefetch | int | +configuration.broker_port | int | +configuration.parallel_queues | int | +configuration.fetch_wait_max | int | +configuration.fetch_min_bytes | int | +configuration.threads | int | +configuration.max_message_size | int | +configuration.decompress_size_limit | int | +configuration.idle_writer_timeout | int | +configuration.max_chunk_size | int | +configuration.interval | int | +configuration.throttling_allowed | bool | +configuration.tls_enable | bool | +configuration.tcp_keepalive | bool | +configuration.exchange_bind | bool | +configuration.tls | bool | +configuration.requeue_invalid_messages | bool | +configuration.use_full_names | bool | +configuration.use_null_delimiter | bool | +configuration.enable_cors | bool | +configuration.force_rdns | bool | +configuration.store_full_message | bool | +configuration.expand_structured_data | bool | +configuration.allow_override_date | bool | +configuration.aws_region | string | +configuration.aws_assume_role_arn | string | +configuration.aws_access_key | string | +configuration.kinesis_stream_name | string | +configuration.aws_secret_key | string | +configuration.aws_sqs_region | string | +configuration.aws_s3_region | string | +configuration.aws_sqs_queue_name | string | +configuration.override_source | string | +configuration.tls_key_file | string | +configuration.tls_key_password | string | +configuration.tls_client_auth | string | +configuration.tls_client_auth_cert_file | string | +configuration.tls_cert_file | string | +configuration.timezone | string | +configuration.broker_vhost | string | +configuration.broker_username | string | +configuration.locale | string | +configuration.broker_password | string | +configuration.exchange | string | +configuration.routing_key | string | +configuration.broker_hostname | string | +configuration.queue | string | +configuration.topic_filter | string | +configuration.offset_reset | string | +configuration.zookeeper | string | +configuration.headers | string | +configuration.path | string | +configuration.target_url | string | +configuration.source | string | +configuration.timeunit | string | +configuration.netflow9_definitions_path | string | + +## Attrs Reference + +name | type | etc +--- | --- | --- +input_id | string | computed +created_at | string | computed +creator_user_id | string | computed diff --git a/terraform/docs/role.md b/terraform/docs/role.md new file mode 100644 index 00000000..f6b73bf9 --- /dev/null +++ b/terraform/docs/role.md @@ -0,0 +1,32 @@ +# graylog_role + +https://github.com/suzuki-shunsuke/terraform-provider-graylog/blob/master/resource_role.go + +``` +resource "graylog_role" "foo" { + name = "foo" + description = "user foo" + permissions = ["*"] +} +``` + +## Argument Reference + +### Required Argument + +name | type | description +--- | --- | --- +name | string | +permissions | []string | + +### Optional Argument + +name | default | type | description +--- | --- | --- | --- +description | "" | string | + +## Attrs Reference + +name | type | etc +--- | --- | --- +read_only | bool | computed diff --git a/terraform/docs/stream.md b/terraform/docs/stream.md new file mode 100644 index 00000000..a9225122 --- /dev/null +++ b/terraform/docs/stream.md @@ -0,0 +1,37 @@ +# graylog_stream + +https://github.com/suzuki-shunsuke/terraform-provider-graylog/blob/master/resource_stream.go + +``` +resource "graylog_stream" "test-terraform" { + title = "test-terraform" + index_set_id = "${graylog_index_set.test-terraform.id}" + disabled = true + matching_type = "AND" +} +``` + +## Argument Reference + +### Required Argument + +name | type | description +--- | --- | --- +title | string | +index_set_id | string | + +### Optional Argument + +name | default | type | description +--- | --- | --- | --- +disabled | | bool | +matching_type | | string | +remove_matches_from_default_stream | | bool | +is_default | | bool | + +## Attrs Reference + +name | type | etc +--- | --- | --- +creator_user_id | string | computed +created_at | string | computed diff --git a/terraform/docs/user.md b/terraform/docs/user.md new file mode 100644 index 00000000..4009ab93 --- /dev/null +++ b/terraform/docs/user.md @@ -0,0 +1,44 @@ +# graylog_user + +https://github.com/suzuki-shunsuke/terraform-provider-graylog/blob/master/resource_user.go + +``` +resource "graylog_user" "zoo" { + username = "zoo" + password = "password" + email = "zoo@example.com" + full_name = "zooull" + permissions = ["users:read:zoo"] +} +``` + +## Argument Reference + +### Required Argument + +name | type | etc +--- | --- | --- +username | string | +password | string | sensitive +email | string | +permissions | []string | +full_name | string | + +### Optional Argument + +name | default | type | etc +--- | --- | --- | --- +roles | [] | []string | +timezone | "" | string | computed +session_timeout_ms | | int | computed + +## Attrs Reference + +name | type | etc +--- | --- | --- +user_id | string | computed +client_address | | string | computed +external | bool | computed +read_only | bool | computed +session_active | bool | computed +last_activity | string | computed diff --git a/terraform/graylog/config.go b/terraform/graylog/config.go new file mode 100644 index 00000000..18bb30bf --- /dev/null +++ b/terraform/graylog/config.go @@ -0,0 +1,12 @@ +package graylog + +// Config represents terraform provider's configuration. +type Config struct { + Endpoint string + AuthName string + AuthPassword string +} + +func (c *Config) loadAndValidate() error { + return nil +} diff --git a/terraform/graylog/doc.go b/terraform/graylog/doc.go new file mode 100644 index 00000000..a1a0d381 --- /dev/null +++ b/terraform/graylog/doc.go @@ -0,0 +1,4 @@ +/* +Package graylog provides the terraform provider for Graylog. +*/ +package graylog diff --git a/terraform/graylog/provider.go b/terraform/graylog/provider.go new file mode 100644 index 00000000..029dde97 --- /dev/null +++ b/terraform/graylog/provider.go @@ -0,0 +1,55 @@ +package graylog + +import ( + "github.com/hashicorp/terraform/helper/schema" +) + +// Provider returns a terraform resource provider for graylog. +func Provider() *schema.Provider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "web_endpoint_uri": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"GRAYLOG_WEB_ENDPOINT_URI"}, nil), + }, + "auth_name": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "GRAYLOG_AUTH_NAME"}, nil), + }, + "auth_password": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "GRAYLOG_AUTH_PASSWORD"}, nil), + }, + }, + ResourcesMap: map[string]*schema.Resource{ + "graylog_role": resourceRole(), + "graylog_user": resourceUser(), + "graylog_input": resourceInput(), + "graylog_index_set": resourceIndexSet(), + "graylog_stream": resourceStream(), + }, + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + endpoint := d.Get("web_endpoint_uri").(string) + authName := d.Get("auth_name").(string) + authPassword := d.Get("auth_password").(string) + config := Config{ + Endpoint: endpoint, + AuthName: authName, + AuthPassword: authPassword, + } + + if err := config.loadAndValidate(); err != nil { + return nil, err + } + + return &config, nil +} diff --git a/terraform/graylog/resource_index_set.go b/terraform/graylog/resource_index_set.go new file mode 100644 index 00000000..9bf595db --- /dev/null +++ b/terraform/graylog/resource_index_set.go @@ -0,0 +1,262 @@ +package graylog + +import ( + "encoding/json" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/client" + "github.com/suzuki-shunsuke/go-graylog/util" +) + +func resourceIndexSet() *schema.Resource { + return &schema.Resource{ + Create: resourceIndexSetCreate, + Read: resourceIndexSetRead, + Update: resourceIndexSetUpdate, + Delete: resourceIndexSetDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + // Required + "title": { + Type: schema.TypeString, + Required: true, + }, + "index_prefix": { + Type: schema.TypeString, + Required: true, + }, + "rotation_strategy_class": { + Type: schema.TypeString, + Required: true, + }, + // type required + "rotation_strategy": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + }, + "max_docs_per_index": { + Type: schema.TypeInt, + Optional: true, + }, + "max_size": { + Type: schema.TypeInt, + Optional: true, + }, + "rotation_period": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "retention_strategy_class": { + Type: schema.TypeString, + Required: true, + }, + // type required + "retention_strategy": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + }, + "max_number_of_indices": { + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + "index_analyzer": { + Type: schema.TypeString, + Required: true, + }, + // >= 1 + "shards": { + Type: schema.TypeInt, + Required: true, + }, + // >= 1 + "index_optimization_max_num_segments": { + Type: schema.TypeInt, + Required: true, + }, + + // Optional + "creation_date": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "replicas": { + Type: schema.TypeInt, + Optional: true, + }, + "index_optimization_disabled": { + Type: schema.TypeBool, + Optional: true, + }, + "writable": { + Type: schema.TypeBool, + Optional: true, + }, + "default": { + Type: schema.TypeBool, + Optional: true, + }, + }, + } +} + +func newIndexSet(d *schema.ResourceData) (*graylog.IndexSet, error) { + rotationStrategy := &graylog.RotationStrategy{} + retentionStrategy := &graylog.RetentionStrategy{} + + ros := d.Get("rotation_strategy").([]interface{})[0].(map[string]interface{}) + res := d.Get("retention_strategy").([]interface{})[0].(map[string]interface{}) + if err := util.MSDecode(ros, rotationStrategy); err != nil { + return nil, err + } + if err := util.MSDecode(res, retentionStrategy); err != nil { + return nil, err + } + + return &graylog.IndexSet{ + ID: d.Id(), + Title: d.Get("title").(string), + IndexPrefix: d.Get("index_prefix").(string), + Description: d.Get("description").(string), + Shards: d.Get("shards").(int), + Replicas: d.Get("replicas").(int), + RotationStrategyClass: d.Get("rotation_strategy_class").(string), + RotationStrategy: rotationStrategy, + RetentionStrategyClass: d.Get("retention_strategy_class").(string), + RetentionStrategy: retentionStrategy, + IndexAnalyzer: d.Get("index_analyzer").(string), + IndexOptimizationMaxNumSegments: d.Get("index_optimization_max_num_segments").(int), + IndexOptimizationDisabled: d.Get("index_optimization_disabled").(bool), + Writable: d.Get("writable").(bool), + Default: d.Get("default").(bool), + CreationDate: d.Get("creation_date").(string), + }, nil +} + +func resourceIndexSetCreate(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + is, err := newIndexSet(d) + if err != nil { + return err + } + if _, err := cl.CreateIndexSet(is); err != nil { + return err + } + d.SetId(is.ID) + return nil +} + +func resourceIndexSetRead(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + is, _, err := cl.GetIndexSet(d.Id()) + if err != nil { + return err + } + if is.RotationStrategy != nil { + b, err := json.Marshal(is.RotationStrategy) + if err != nil { + return err + } + dest := map[string]interface{}{} + if err := json.Unmarshal(b, &dest); err != nil { + return err + } + d.Set("rotation_strategy", dest) + } + if is.RetentionStrategy != nil { + b, err := json.Marshal(is.RetentionStrategy) + if err != nil { + return err + } + dest := map[string]interface{}{} + if err := json.Unmarshal(b, &dest); err != nil { + return err + } + d.Set("retention_strategy", dest) + } + + d.Set("title", is.Title) + d.Set("description", is.Description) + d.Set("shards", is.Shards) + d.Set("replicas", is.Replicas) + d.Set("rotation_strategy_class", is.RotationStrategyClass) + d.Set("retention_strategy_class", is.RetentionStrategyClass) + d.Set("index_analyzer", is.IndexAnalyzer) + d.Set( + "index_optimization_max_num_segments", + is.IndexOptimizationMaxNumSegments) + d.Set("index_optimization_disabled", is.IndexOptimizationDisabled) + d.Set("writable", is.Writable) + d.Set("default", is.Default) + return nil +} + +func resourceIndexSetUpdate(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + is, err := newIndexSet(d) + if err != nil { + return err + } + + if _, _, err = cl.UpdateIndexSet(is.NewUpdateParams()); err != nil { + return err + } + return nil +} + +func resourceIndexSetDelete(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + if _, err := cl.DeleteIndexSet(d.Id()); err != nil { + return err + } + return nil +} diff --git a/terraform/graylog/resource_index_set_test.go b/terraform/graylog/resource_index_set_test.go new file mode 100644 index 00000000..3fd7e893 --- /dev/null +++ b/terraform/graylog/resource_index_set_test.go @@ -0,0 +1,128 @@ +package graylog + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/satori/go.uuid" + "github.com/suzuki-shunsuke/go-graylog/client" + "github.com/suzuki-shunsuke/go-graylog/mockserver" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func testDeleteIndexSet( + cl *client.Client, server *mockserver.Server, key string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + id, err := getIDFromTfState(tfState, key) + if err != nil { + return err + } + testutil.WaitAfterDeleteIndexSet(server) + if _, _, err := cl.GetIndexSet(id); err == nil { + return fmt.Errorf(`indexSet "%s" must be deleted`, id) + } + return nil + } +} + +func testCreateIndexSet( + cl *client.Client, server *mockserver.Server, key string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + id, err := getIDFromTfState(tfState, key) + if err != nil { + return err + } + testutil.WaitAfterCreateIndexSet(server) + + _, _, err = cl.GetIndexSet(id) + return err + } +} + +func testUpdateIndexSet( + cl *client.Client, key, title string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + id, err := getIDFromTfState(tfState, key) + if err != nil { + return err + } + indexSet, _, err := cl.GetIndexSet(id) + if err != nil { + return err + } + if indexSet.Title != title { + return fmt.Errorf("indexSet.Title == %s, wanted %s", indexSet.Title, title) + } + return nil + } +} + +func TestAccIndexSet(t *testing.T) { + cl, server, err := setEnv() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer os.Unsetenv("GRAYLOG_WEB_ENDPOINT_URI") + } + testAccProvider := Provider() + testAccProviders := map[string]terraform.ResourceProvider{ + "graylog": testAccProvider, + } + + u, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + prefix := u.String() + roleTf := ` +resource "graylog_index_set" "test" { + title = "%s" + description = "terraform test index set description" + index_prefix = "%s" + shards = 4 + replicas = 0 + rotation_strategy_class = "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategy" + rotation_strategy = { + type = "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategyConfig" + } + retention_strategy_class = "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy" + retention_strategy = { + type = "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig" + } + index_analyzer = "standard" + shards = 4 + index_optimization_max_num_segments = 1 +}` + + updateTitle := "terraform test index set title updated" + key := "graylog_index_set.test" + if server != nil { + server.Start() + defer server.Close() + } + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + CheckDestroy: testDeleteIndexSet(cl, server, key), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(roleTf, "terraform test index set title", prefix), + Check: resource.ComposeTestCheckFunc( + testCreateIndexSet(cl, server, key), + ), + }, + { + Config: fmt.Sprintf(roleTf, updateTitle, prefix), + Check: resource.ComposeTestCheckFunc( + testUpdateIndexSet(cl, key, updateTitle), + ), + }, + }, + }) +} diff --git a/terraform/graylog/resource_input.go b/terraform/graylog/resource_input.go new file mode 100644 index 00000000..a25fd1db --- /dev/null +++ b/terraform/graylog/resource_input.go @@ -0,0 +1,191 @@ +package graylog + +import ( + "encoding/json" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/client" +) + +func resourceInput() *schema.Resource { + cfgSchema := map[string]*schema.Schema{} + for s := range graylog.InputAttrsStrFieldSet.ToMap(false) { + cfgSchema[s] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + } + } + for s := range graylog.InputAttrsIntFieldSet.ToMap(false) { + cfgSchema[s] = &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + } + } + for s := range graylog.InputAttrsBoolFieldSet.ToMap(false) { + cfgSchema[s] = &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + } + } + return &schema.Resource{ + Create: resourceInputCreate, + Read: resourceInputRead, + Update: resourceInputUpdate, + Delete: resourceInputDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + // required + "title": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + }, + "attributes": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: cfgSchema, + }, + MaxItems: 1, + MinItems: 1, + }, + + "global": { + Type: schema.TypeBool, + Optional: true, + }, + "node": { + Type: schema.TypeString, + Optional: true, + }, + + "created_at": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "creator_user_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + // "context_pack": &schema.Schema{ + // Type: schema.TypeString, + // Optional: true, + // }, + // "static_fields": &schema.Schema{ + // Type: schema.TypeString, + // Optional: true, + // }, + }, + } +} + +func newInput(d *schema.ResourceData) (*graylog.Input, error) { + data := &graylog.InputData{ + Title: d.Get("title").(string), + Type: d.Get("type").(string), + Global: d.Get("global").(bool), + Node: d.Get("node").(string), + ID: d.Id(), + CreatorUserID: d.Get("creator_user_id").(string), + CreatedAt: d.Get("created_at").(string), + Attrs: d.Get("attributes").([]interface{})[0].(map[string]interface{}), + } + input := &graylog.Input{} + if err := data.ToInput(input); err != nil { + return nil, err + } + return input, nil +} + +func resourceInputCreate(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + input, err := newInput(d) + if err != nil { + return err + } + + if _, err := cl.CreateInput(input); err != nil { + return err + } + d.SetId(input.ID) + return nil +} + +func resourceInputRead(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + input, ei, err := cl.GetInput(d.Id()) + if err != nil { + if ei != nil && ei.Response != nil && ei.Response.StatusCode == 404 { + d.SetId("") + return nil + } + return err + } + if input.Attrs != nil { + b, err := json.Marshal(input.Attrs) + if err != nil { + return err + } + dest := map[string]interface{}{} + if err := json.Unmarshal(b, &dest); err != nil { + return err + } + d.Set("attributes", dest) + } + d.Set("title", input.Title) + d.Set("type", input.Type) + d.Set("node", input.Node) + d.Set("creator_user_id", input.CreatorUserID) + d.Set("created_at", input.CreatedAt) + return nil +} + +func resourceInputUpdate(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + input, err := newInput(d) + if err != nil { + return err + } + if _, _, err := cl.UpdateInput(input.NewUpdateParams()); err != nil { + return err + } + return nil +} + +func resourceInputDelete(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + if _, err := cl.DeleteInput(d.Id()); err != nil { + return err + } + return nil +} diff --git a/terraform/graylog/resource_input_test.go b/terraform/graylog/resource_input_test.go new file mode 100644 index 00000000..5fede694 --- /dev/null +++ b/terraform/graylog/resource_input_test.go @@ -0,0 +1,102 @@ +package graylog + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/suzuki-shunsuke/go-graylog/client" +) + +var ( + terraformTestInputID string +) + +func testDeleteInput(cl *client.Client, key string) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + if _, _, err := cl.GetInput(terraformTestInputID); err == nil { + return fmt.Errorf(`input "%s" must be deleted`, terraformTestInputID) + } + return nil + } +} + +func testCreateInput(cl *client.Client, key string) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + id, err := getIDFromTfState(tfState, key) + if err != nil { + return err + } + terraformTestInputID = id + + _, _, err = cl.GetInput(id) + return err + } +} + +func testUpdateInput(cl *client.Client, key, title string) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + id, err := getIDFromTfState(tfState, key) + if err != nil { + return err + } + input, _, err := cl.GetInput(id) + if err != nil { + return err + } + if input.Title != title { + return fmt.Errorf("input.Title == %s, wanted %s", input.Title, title) + } + return nil + } +} + +func TestAccInput(t *testing.T) { + cl, server, err := setEnv() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer os.Unsetenv("GRAYLOG_WEB_ENDPOINT_URI") + } + + testAccProvider := Provider() + testAccProviders := map[string]terraform.ResourceProvider{ + "graylog": testAccProvider, + } + + roleTf := ` +resource "graylog_input" "test" { + title = "%s" + type = "org.graylog2.inputs.syslog.udp.SyslogUDPInput" + attributes = { + bind_address = "0.0.0.0" + port = 514 + recv_buffer_size = 262144 + } +}` + createTitle := "terraform test input title" + updateTitle := "terraform test input title updated" + + key := "graylog_input.test" + if server != nil { + server.Start() + defer server.Close() + } + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + CheckDestroy: testDeleteInput(cl, key), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(roleTf, createTitle), + Check: testCreateInput(cl, key), + }, + { + Config: fmt.Sprintf(roleTf, updateTitle), + Check: testUpdateInput(cl, key, updateTitle), + }, + }, + }) +} diff --git a/terraform/graylog/resource_role.go b/terraform/graylog/resource_role.go new file mode 100644 index 00000000..2d22702a --- /dev/null +++ b/terraform/graylog/resource_role.go @@ -0,0 +1,113 @@ +package graylog + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/client" + "github.com/suzuki-shunsuke/go-set" +) + +func resourceRole() *schema.Resource { + return &schema.Resource{ + Create: resourceRoleCreate, + Read: resourceRoleRead, + Update: resourceRoleUpdate, + Delete: resourceRoleDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "permissions": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "read_only": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + }, + } +} + +func newRole(d *schema.ResourceData) *graylog.Role { + return &graylog.Role{ + Name: d.Get("name").(string), + Permissions: set.NewStrSet(getStringArray(d.Get("permissions").([]interface{}))...), + Description: d.Get("description").(string), + ReadOnly: d.Get("read_only").(bool), + } +} + +func resourceRoleCreate(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + role := newRole(d) + if _, err := cl.CreateRole(role); err != nil { + return err + } + d.SetId(role.Name) + return nil +} + +func resourceRoleRead(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + role, _, err := cl.GetRole(d.Get("name").(string)) + if err != nil { + return err + } + d.Set("name", role.Name) + d.Set("permissions", role.Permissions) + d.Set("description", role.Description) + d.Set("read_only", role.ReadOnly) + return nil +} + +func resourceRoleUpdate(d *schema.ResourceData, m interface{}) error { + o, n := d.GetChange("name") + oldName := o.(string) + newName := n.(string) + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + role := newRole(d) + role.Name = newName + _, _, err = cl.UpdateRole(oldName, role.NewUpdateParams()) + return err +} + +func resourceRoleDelete(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + if _, err := cl.DeleteRole(d.Get("name").(string)); err != nil { + return err + } + return nil +} diff --git a/terraform/graylog/resource_role_test.go b/terraform/graylog/resource_role_test.go new file mode 100644 index 00000000..66692328 --- /dev/null +++ b/terraform/graylog/resource_role_test.go @@ -0,0 +1,98 @@ +package graylog + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/suzuki-shunsuke/go-graylog/client" +) + +func testDeleteRole( + cl *client.Client, name string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + if _, _, err := cl.GetRole(name); err == nil { + return fmt.Errorf(`role "%s" must be deleted`, name) + } + return nil + } +} + +func testCreateRole( + cl *client.Client, name string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + _, _, err := cl.GetRole(name) + return err + } +} + +func testUpdateRole( + cl *client.Client, name, description string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + role, _, err := cl.GetRole(name) + if err != nil { + return err + } + if role.Description != description { + return fmt.Errorf("role.Description is not updated") + } + return nil + } +} + +func TestAccRole(t *testing.T) { + cl, server, err := setEnv() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer os.Unsetenv("GRAYLOG_WEB_ENDPOINT_URI") + } + + testAccProvider := Provider() + testAccProviders := map[string]terraform.ResourceProvider{ + "graylog": testAccProvider, + } + + roleTf := ` +resource "graylog_role" "test-terraform" { + name = "test terraform name" + description = "test terraform description" + permissions = ["*"] +}` + description := "test terraform description updated" + updateTf := fmt.Sprintf(` +resource "graylog_role" "test-terraform" { + name = "test terraform name" + description = "%s" + permissions = ["*"] +}`, description) + name := "test terraform name" + if server != nil { + server.Start() + defer server.Close() + } + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + CheckDestroy: testDeleteRole(cl, name), + Steps: []resource.TestStep{ + { + Config: roleTf, + Check: resource.ComposeTestCheckFunc( + testCreateRole(cl, name), + ), + }, + { + Config: updateTf, + Check: resource.ComposeTestCheckFunc( + testUpdateRole(cl, name, description), + ), + }, + }, + }) +} diff --git a/terraform/graylog/resource_stream.go b/terraform/graylog/resource_stream.go new file mode 100644 index 00000000..5e3d6024 --- /dev/null +++ b/terraform/graylog/resource_stream.go @@ -0,0 +1,168 @@ +package graylog + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/client" +) + +func resourceStream() *schema.Resource { + return &schema.Resource{ + Create: resourceStreamCreate, + Read: resourceStreamRead, + Update: resourceStreamUpdate, + Delete: resourceStreamDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + // Required + "title": { + Type: schema.TypeString, + Required: true, + }, + "index_set_id": { + Type: schema.TypeString, + Required: true, + }, + + // Optional + // rules + "description": { + Type: schema.TypeString, + Optional: true, + }, + // content_pack + "matching_type": { + Type: schema.TypeString, + Optional: true, + }, + "remove_matches_from_default_stream": { + Type: schema.TypeBool, + Optional: true, + }, + + // attributes + "creator_user_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + // outputs + "created_at": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "disabled": { + Type: schema.TypeBool, + Optional: true, + }, + "is_default": { + Type: schema.TypeBool, + Optional: true, + }, + // alert_conditions + // alert_receivers + }, + } +} + +func newStream(d *schema.ResourceData) (*graylog.Stream, error) { + return &graylog.Stream{ + IndexSetID: d.Get("index_set_id").(string), + Title: d.Get("title").(string), + Description: d.Get("description").(string), + MatchingType: d.Get("matching_type").(string), + RemoveMatchesFromDefaultStream: d.Get( + "remove_matches_from_default_stream").(bool), + ID: d.Id(), + }, nil +} + +func resourceStreamCreate(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + stream, err := newStream(d) + if err != nil { + return err + } + + if _, err := cl.CreateStream(stream); err != nil { + return err + } + d.SetId(stream.ID) + // resume if needed + disabled := d.Get("disabled").(bool) + if !disabled { + if _, err := cl.ResumeStream(stream.ID); err != nil { + return err + } + } + return nil +} + +func resourceStreamRead(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + stream, _, err := cl.GetStream(d.Id()) + if err != nil { + return err + } + d.Set("index_set_id", stream.IndexSetID) + d.Set("title", stream.Title) + d.Set("description", stream.Description) + d.Set("matching_type", stream.MatchingType) + d.Set( + "remove_matches_from_default_stream", + stream.RemoveMatchesFromDefaultStream) + // rules + // content_pack + d.Set("creator_user_id", stream.CreatorUserID) + d.Set("created_at", stream.CreatedAt) + d.Set("disabled", stream.Disabled) + d.Set("is_default", stream.IsDefault) + // alert_receivers + // alert_conditions + return nil +} + +func resourceStreamUpdate(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + stream, err := newStream(d) + if err != nil { + return err + } + if _, err := cl.UpdateStream(stream); err != nil { + return err + } + return nil +} + +func resourceStreamDelete(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + if _, err := cl.DeleteStream(d.Id()); err != nil { + return err + } + return nil +} diff --git a/terraform/graylog/resource_stream_test.go b/terraform/graylog/resource_stream_test.go new file mode 100644 index 00000000..29b3e8a2 --- /dev/null +++ b/terraform/graylog/resource_stream_test.go @@ -0,0 +1,136 @@ +package graylog + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/satori/go.uuid" + "github.com/suzuki-shunsuke/go-graylog/client" + "github.com/suzuki-shunsuke/go-graylog/mockserver" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func testDeleteStream( + cl *client.Client, key string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + id, err := getIDFromTfState(tfState, key) + if err != nil { + return err + } + if _, _, err := cl.GetStream(id); err == nil { + return fmt.Errorf(`stream "%s" must be deleted`, id) + } + return nil + } +} + +func testCreateStream( + cl *client.Client, server *mockserver.Server, key string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + id, err := getIDFromTfState(tfState, key) + if err != nil { + return err + } + testutil.WaitAfterCreateIndexSet(server) + + _, _, err = cl.GetStream(id) + return err + } +} + +func testUpdateStream( + cl *client.Client, key, title string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + id, err := getIDFromTfState(tfState, key) + if err != nil { + return err + } + stream, _, err := cl.GetStream(id) + if err != nil { + return err + } + if stream.Title != title { + return fmt.Errorf("stream.Title == %s, wanted %s", stream.Title, title) + } + return nil + } +} + +func TestAccStream(t *testing.T) { + cl, server, err := setEnv() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer os.Unsetenv("GRAYLOG_WEB_ENDPOINT_URI") + } + + testAccProvider := Provider() + testAccProviders := map[string]terraform.ResourceProvider{ + "graylog": testAccProvider, + } + + u, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + prefix := u.String() + roleTf := ` +resource "graylog_index_set" "test" { + title = "terraform test index set" + description = "terraform test index set description" + index_prefix = "%s" + shards = 4 + replicas = 0 + rotation_strategy_class = "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategy" + rotation_strategy = { + type = "org.graylog2.indexer.rotation.strategies.MessageCountRotationStrategyConfig" + } + retention_strategy_class = "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy" + retention_strategy = { + type = "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig" + } + index_analyzer = "standard" + shards = 4 + writable = true + index_optimization_max_num_segments = 1 +} + +resource "graylog_stream" "test" { + title = "%s" + index_set_id = "${graylog_index_set.test.id}" + matching_type = "AND" +}` + createTitle := "terraform stream test" + updateTitle := "terraform stream test updated" + + key := "graylog_stream.test" + if server != nil { + server.Start() + defer server.Close() + } + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + CheckDestroy: testDeleteStream(cl, key), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(roleTf, prefix, createTitle), + Check: resource.ComposeTestCheckFunc( + testCreateStream(cl, server, key), + ), + }, + { + Config: fmt.Sprintf(roleTf, prefix, updateTitle), + Check: resource.ComposeTestCheckFunc( + testUpdateStream(cl, key, updateTitle), + ), + }, + }, + }) +} diff --git a/terraform/graylog/resource_user.go b/terraform/graylog/resource_user.go new file mode 100644 index 00000000..eca72700 --- /dev/null +++ b/terraform/graylog/resource_user.go @@ -0,0 +1,175 @@ +package graylog + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/client" + "github.com/suzuki-shunsuke/go-set" +) + +func resourceUser() *schema.Resource { + return &schema.Resource{ + Create: resourceUserCreate, + Read: resourceUserRead, + Update: resourceUserUpdate, + Delete: resourceUserDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + // Required + "username": { + Type: schema.TypeString, + Required: true, + }, + "password": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, + "email": { + Type: schema.TypeString, + Required: true, + }, + "permissions": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "full_name": { + Type: schema.TypeString, + Required: true, + }, + + // Optional + "roles": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "timezone": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "session_timeout_ms": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + + "user_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "external": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "client_address": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "session_active": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "last_activity": { + Type: schema.TypeString, + Required: false, + Computed: true, + }, + }, + } +} + +func newUser(d *schema.ResourceData) *graylog.User { + permissions := set.NewStrSet(getStringArray(d.Get("permissions").([]interface{}))...) + roles := set.NewStrSet(getStringArray(d.Get("roles").([]interface{}))...) + return &graylog.User{ + Username: d.Get("username").(string), + Roles: roles, + Permissions: permissions, + Email: d.Get("email").(string), + FullName: d.Get("full_name").(string), + Timezone: d.Get("timezone").(string), + SessionTimeoutMs: d.Get("session_timeout_ms").(int), + External: d.Get("external").(bool), + ClientAddress: d.Get("client_address").(string), + Password: d.Get("password").(string), + ReadOnly: d.Get("read_only").(bool), + // SessionActive: d.Get("session_active").(bool), + } +} + +func resourceUserCreate(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + user := newUser(d) + if _, err = cl.CreateUser(user); err != nil { + return err + } + d.SetId(user.Username) + return nil +} + +func resourceUserRead(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + user, _, err := cl.GetUser(d.Get("username").(string)) + if err != nil { + return err + } + d.Set("username", user.Username) + d.Set("email", user.Email) + d.Set("permissions", user.Permissions) + d.Set("timezone", user.Timezone) + d.Set("session_timeout_ms", user.SessionTimeoutMs) + d.Set("external", user.External) + d.Set("read_only", user.ReadOnly) + return nil +} + +func resourceUserUpdate(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + user := newUser(d) + _, err = cl.UpdateUser(user.NewUpdateParams()) + return err +} + +func resourceUserDelete(d *schema.ResourceData, m interface{}) error { + config := m.(*Config) + cl, err := client.NewClient( + config.Endpoint, config.AuthName, config.AuthPassword) + if err != nil { + return err + } + if _, err := cl.DeleteUser(d.Get("username").(string)); err != nil { + return err + } + return nil +} diff --git a/terraform/graylog/resource_user_test.go b/terraform/graylog/resource_user_test.go new file mode 100644 index 00000000..eae329c4 --- /dev/null +++ b/terraform/graylog/resource_user_test.go @@ -0,0 +1,103 @@ +package graylog + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/suzuki-shunsuke/go-graylog/client" +) + +func testDeleteUser( + cl *client.Client, name string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + if _, _, err := cl.GetUser(name); err == nil { + return fmt.Errorf(`user "%s" must be deleted`, name) + } + return nil + } +} + +func testCreateUser( + cl *client.Client, name string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + _, _, err := cl.GetUser(name) + return err + } +} + +func testUpdateUser( + cl *client.Client, name, fullName string, +) resource.TestCheckFunc { + return func(tfState *terraform.State) error { + user, _, err := cl.GetUser(name) + if err != nil { + return err + } + if user.FullName != fullName { + return fmt.Errorf("user.FullName is not updated") + } + return nil + } +} + +func TestAccUser(t *testing.T) { + cl, server, err := setEnv() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer os.Unsetenv("GRAYLOG_WEB_ENDPOINT_URI") + } + + testAccProvider := Provider() + testAccProviders := map[string]terraform.ResourceProvider{ + "graylog": testAccProvider, + } + + name := "test terraform name" + + userTf := fmt.Sprintf(` +resource "graylog_user" "zoo" { + username = "%s" + password = "password" + email = "zoo@example.com" + full_name = "zooull" + permissions = ["users:read:zoo"] +}`, name) + fullName := "new full name" + updateTf := fmt.Sprintf(` +resource "graylog_user" "zoo" { + username = "%s" + password = "password" + email = "zoo@example.com" + full_name = "%s" + permissions = ["users:read:zoo"] +}`, name, fullName) + if server != nil { + server.Start() + defer server.Close() + } + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + CheckDestroy: testDeleteUser(cl, name), + Steps: []resource.TestStep{ + { + Config: userTf, + Check: resource.ComposeTestCheckFunc( + testCreateUser(cl, name), + ), + }, + { + Config: updateTf, + Check: resource.ComposeTestCheckFunc( + testUpdateUser(cl, name, fullName), + ), + }, + }, + }) +} diff --git a/terraform/graylog/util.go b/terraform/graylog/util.go new file mode 100644 index 00000000..789327fe --- /dev/null +++ b/terraform/graylog/util.go @@ -0,0 +1,74 @@ +package graylog + +import ( + "fmt" + "os" + + "github.com/hashicorp/terraform/terraform" + "github.com/suzuki-shunsuke/go-graylog/client" + "github.com/suzuki-shunsuke/go-graylog/mockserver" +) + +func getStringArray(src []interface{}) []string { + dest := make([]string, len(src)) + for i, p := range src { + dest[i] = p.(string) + } + return dest +} + +func setEnv() (*client.Client, *mockserver.Server, error) { + _, ok := os.LookupEnv("TF_ACC") + if !ok { + if err := os.Setenv("TF_ACC", "true"); err != nil { + return nil, nil, err + } + } + authName, ok := os.LookupEnv("GRAYLOG_AUTH_NAME") + if !ok { + authName = "admin" + if err := os.Setenv("GRAYLOG_AUTH_NAME", authName); err != nil { + return nil, nil, err + } + } + authPass, ok := os.LookupEnv("GRAYLOG_AUTH_PASSWORD") + if !ok { + authPass = "admin" + if err := os.Setenv("GRAYLOG_AUTH_PASSWORD", "admin"); err != nil { + return nil, nil, err + } + } + var ( + server *mockserver.Server + err error + ) + endpoint := os.Getenv("GRAYLOG_WEB_ENDPOINT_URI") + if endpoint == "" { + server, err = mockserver.NewServer("", nil) + if err != nil { + return nil, nil, err + } + server.SetAuth(true) + endpoint = server.Endpoint() + if err := os.Setenv("GRAYLOG_WEB_ENDPOINT_URI", endpoint); err != nil { + return nil, nil, err + } + } + cl, err := client.NewClient(endpoint, authName, authPass) + if err != nil { + return nil, nil, err + } + return cl, server, nil +} + +func getIDFromTfState(tfState *terraform.State, key string) (string, error) { + rs, ok := tfState.RootModule().Resources[key] + if !ok { + return "", fmt.Errorf("Not found: %s", key) + } + id := rs.Primary.ID + if id == "" { + return "", fmt.Errorf("No ID is set") + } + return id, nil +} diff --git a/terraform/main.go b/terraform/main.go new file mode 100644 index 00000000..a7ae7e40 --- /dev/null +++ b/terraform/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/hashicorp/terraform/plugin" + "github.com/hashicorp/terraform/terraform" + "github.com/suzuki-shunsuke/go-graylog/terraform/graylog" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: func() terraform.ResourceProvider { + return graylog.Provider() + }, + }) +} diff --git a/test.sh b/test.sh new file mode 100644 index 00000000..701cbf38 --- /dev/null +++ b/test.sh @@ -0,0 +1,19 @@ +decho() { + echo "+ $@" + eval $@ +} + +# gofmt +npm run fmt || exit 1 + +# golint +decho golint client/... terraform/... validator mockserver mockserver/store mockserver/handler mockserver/logic mockserver/seed mockserver/exec mockserver/store/plain || exit 1 + +decho go test ./mockserver/... -covermode=atomic || exit 1 + +if [ -f env.sh ]; then + decho source env.sh +fi + +decho go test ./util/... ./validator/... ./client/... . -covermode=atomic || exit 1 +decho go test -v ./terraform/... -covermode=atomic || exit 1 diff --git a/test/doc.go b/test/doc.go new file mode 100644 index 00000000..7ae3d88b --- /dev/null +++ b/test/doc.go @@ -0,0 +1,5 @@ +/* +Package test provides shared test functions. +This package may be abolished, so don't add new test functions to this package. +*/ +package test diff --git a/test/role_member.go b/test/role_member.go new file mode 100644 index 00000000..2ecb94f8 --- /dev/null +++ b/test/role_member.go @@ -0,0 +1,104 @@ +package test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestGetRoleMembers(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + users, _, err := client.GetRoleMembers("Admin") + if err != nil { + t.Fatal("Failed to GetRoleMembers", err) + } + if len(users) != 0 { + t.Fatalf("the number of Admin users is %d, wanted 0", len(users)) + } + if _, _, err := client.GetRoleMembers(""); err == nil { + t.Fatal("name is required") + } + if _, _, err := client.GetRoleMembers("h"); err == nil { + t.Fatal(`no role whose name is "h"`) + } +} + +func TestAddUserToRole(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + user, err := testutil.GetNonAdminUser(client) + if err != nil { + t.Fatal(err) + } + if user == nil { + user = testutil.User() + user.Username = "foo" + if _, err := client.CreateUser(user); err != nil { + t.Fatal(err) + } + } + if _, err = client.AddUserToRole(user.Username, "Admin"); err != nil { + // Cannot modify local root user, this is a bug. + t.Fatal(err) + } + if _, err = client.AddUserToRole("", "Admin"); err == nil { + t.Fatal("user name is required") + } + if _, err = client.AddUserToRole("admin", ""); err == nil { + t.Fatal("role name is required") + } + if _, err = client.AddUserToRole("h", "Admin"); err == nil { + t.Fatal(`no user whose name is "h"`) + } + if _, err = client.AddUserToRole("admin", "h"); err == nil { + t.Fatal(`no role whose name is "h"`) + } +} + +func TestRemoveUserFromRole(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + user, err := testutil.GetNonAdminUser(client) + if err != nil { + t.Fatal(err) + } + if user == nil { + user = testutil.User() + user.Username = "foo" + if _, err := client.CreateUser(user); err != nil { + t.Fatal(err) + } + } + if _, err = client.RemoveUserFromRole(user.Username, "Admin"); err != nil { + // Cannot modify local root user, this is a bug. + t.Fatal(err) + } + if _, err = client.RemoveUserFromRole("", "Admin"); err == nil { + t.Fatal("user name is required") + } + if _, err = client.RemoveUserFromRole(user.Username, ""); err == nil { + t.Fatal("role name is required") + } + if _, err = client.RemoveUserFromRole("h", "Admin"); err == nil { + t.Fatal(`no user whose name is "h"`) + } + if _, err = client.RemoveUserFromRole(user.Username, "h"); err == nil { + t.Fatal(`no role whose name is "h"`) + } +} diff --git a/test/stream.go b/test/stream.go new file mode 100644 index 00000000..60276211 --- /dev/null +++ b/test/stream.go @@ -0,0 +1,111 @@ +package test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestGetEnabledStreams(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + _, total, _, err := client.GetEnabledStreams() + if err != nil { + t.Fatal("Failed to GetStreams", err) + } + if total != 1 { + t.Fatalf("total == %d, wanted %d", total, 1) + } +} + +func TestUpdateStream(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + + stream, f, err := testutil.GetStream(client, server, 2) + if err != nil { + t.Fatal(err) + } + if f != nil { + defer f(stream.ID) + } + + stream.Description = "changed!" + if _, err := client.UpdateStream(stream); err != nil { + t.Fatal(err) + } + stream.ID = "" + if _, err := client.UpdateStream(stream); err == nil { + t.Fatal("id is required") + } + stream.ID = "h" + if _, err := client.UpdateStream(stream); err == nil { + t.Fatal(`no stream whose id is "h"`) + } + if _, err := client.UpdateStream(nil); err == nil { + t.Fatal("stream is nil") + } +} + +func TestPauseStream(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + streams, _, _, err := client.GetStreams() + if err != nil { + t.Fatal(err) + } + stream := streams[0] + if _, err = client.PauseStream(stream.ID); err != nil { + t.Fatal("Failed to PauseStream", err) + } + if _, err := client.PauseStream(""); err == nil { + t.Fatal("id is required") + } + if _, err := client.PauseStream("h"); err == nil { + t.Fatal(`no stream whose id is "h"`) + } + // TODO test pause +} + +func TestResumeStream(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + streams, _, _, err := client.GetStreams() + if err != nil { + t.Fatal(err) + } + stream := streams[0] + + if _, err = client.ResumeStream(stream.ID); err != nil { + t.Fatal("Failed to ResumeStream", err) + } + + if _, err = client.ResumeStream(""); err == nil { + t.Fatal("id is required") + } + + if _, err = client.ResumeStream("h"); err == nil { + t.Fatal(`no stream whose id is "h"`) + } + // TODO test resume +} diff --git a/test/stream_rule.go b/test/stream_rule.go new file mode 100644 index 00000000..9971305e --- /dev/null +++ b/test/stream_rule.go @@ -0,0 +1,214 @@ +package test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestGetStreamRules(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + stream, f, err := testutil.GetStream(client, server, 2) + if err != nil { + t.Fatal(err) + } + if f != nil { + defer f(stream.ID) + } + + if _, _, _, err := client.GetStreamRules(stream.ID); err != nil { + t.Fatal("Failed to GetStreamRules", err) + } + if _, _, _, err := client.GetStreamRules("h"); err == nil { + t.Fatal(`no stream with id "h" is found`) + } +} + +func TestCreateStreamRule(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + stream, f, err := testutil.GetStream(client, server, 2) + if err != nil { + t.Fatal(err) + } + if f != nil { + defer f(stream.ID) + } + rule := testutil.DummyNewStreamRule() + rule.StreamID = stream.ID + if _, err := client.CreateStreamRule(rule); err != nil { + t.Fatal(err) + } + if _, err := client.CreateStreamRule(rule); err == nil { + t.Fatal("stream rule id should be empty") + } + rule.ID = "" + rule.StreamID = "" + if _, err := client.CreateStreamRule(rule); err == nil { + t.Fatal("stream id is required") + } + rule.StreamID = "h" + if _, err := client.CreateStreamRule(rule); err == nil { + t.Fatal(`no stream with id "h" is not found`) + } + + if _, err := client.CreateStreamRule(nil); err == nil { + t.Fatal("stream rule is nil") + } +} + +func TestUpdateStreamRule(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + stream, f, err := testutil.GetStream(client, server, 2) + if err != nil { + t.Fatal(err) + } + if f != nil { + defer f(stream.ID) + } + rules, _, _, err := client.GetStreamRules(stream.ID) + if err != nil { + t.Fatal(err) + } + rule := testutil.StreamRule() + if len(rules) == 0 { + rule.StreamID = stream.ID + if _, err := client.CreateStreamRule(rule); err != nil { + t.Fatal(err) + } + } else { + rule = &(rules[0]) + } + + rule.Description += " changed!" + if _, err := client.UpdateStreamRule(rule); err != nil { + t.Fatal(err) + } + streamID := rule.StreamID + rule.StreamID = "" + if _, err := client.UpdateStreamRule(rule); err == nil { + t.Fatal("stream id is required") + } + rule.StreamID = streamID + // ruleID = rule.ID + rule.ID = "" + if _, err := client.UpdateStreamRule(rule); err == nil { + t.Fatal("stream rule id is required") + } + rule.ID = "h" + if _, err := client.UpdateStreamRule(rule); err == nil { + t.Fatal(`no stream rule with id "h" is not found`) + } + + if _, err := client.UpdateStreamRule(nil); err == nil { + t.Fatal("stream rule is nil") + } +} + +func TestDeleteStreamRule(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + stream, f, err := testutil.GetStream(client, server, 2) + if err != nil { + t.Fatal(err) + } + if f != nil { + defer f(stream.ID) + } + rules, _, _, err := client.GetStreamRules(stream.ID) + if err != nil { + t.Fatal(err) + } + rule := testutil.StreamRule() + if len(rules) == 0 { + rule.StreamID = stream.ID + if _, err := client.CreateStreamRule(rule); err != nil { + t.Fatal(err) + } + } else { + rule = &(rules[0]) + } + + if _, err := client.DeleteStreamRule("", rule.ID); err == nil { + t.Fatal("stream id is required") + } + if _, err := client.DeleteStreamRule(rule.StreamID, ""); err == nil { + t.Fatal("stream rule id is required") + } + if _, err := client.DeleteStreamRule(rule.StreamID, rule.ID); err != nil { + t.Fatal(err) + } + if _, _, err := client.GetStreamRule(rule.StreamID, rule.ID); err == nil { + t.Fatal("stream rule should be deleted") + } +} + +func TestGetStreamRule(t *testing.T) { + server, client, err := testutil.GetServerAndClient() + if err != nil { + t.Fatal(err) + } + if server != nil { + defer server.Close() + } + streams, _, _, err := client.GetStreams() + if err != nil { + t.Fatal(err) + } + stream := streams[0] + rules, _, _, err := client.GetStreamRules(stream.ID) + if err != nil { + t.Fatal(err) + } + rule := testutil.StreamRule() + if len(rules) == 0 { + rule.StreamID = stream.ID + if _, err := client.CreateStreamRule(rule); err != nil { + t.Fatal(err) + } + } else { + rule = &(rules[0]) + } + + if _, _, err := client.GetStreamRule("", rule.ID); err == nil { + t.Fatal("stream id is required") + } + if _, _, err := client.GetStreamRule(rule.StreamID, ""); err == nil { + t.Fatal("stream rule id is required") + } + if _, _, err := client.GetStreamRule("h", rule.ID); err == nil { + t.Fatal(`no stream with id "h" is found`) + } + if _, _, err := client.GetStreamRule(rule.StreamID, "h"); err == nil { + t.Fatal(`no stream rule with id "h" is found`) + } + r, _, err := client.GetStreamRule(rule.StreamID, rule.ID) + if err != nil { + t.Fatal(err) + } + if r.ID != rule.ID { + t.Fatalf("rule.ID = %s, wanted %s", r.ID, rule.ID) + } +} diff --git a/testutil/doc.go b/testutil/doc.go new file mode 100644 index 00000000..1e78b4e7 --- /dev/null +++ b/testutil/doc.go @@ -0,0 +1,4 @@ +/* +Package testutil provides utilities for test. +*/ +package testutil diff --git a/testutil/test_data.go b/testutil/test_data.go new file mode 100644 index 00000000..d436351c --- /dev/null +++ b/testutil/test_data.go @@ -0,0 +1,138 @@ +package testutil + +import ( + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-set" +) + +func Role() *graylog.Role { + return &graylog.Role{ + Name: "Writer", + Description: "writer", + Permissions: set.NewStrSet("*"), + ReadOnly: true} +} + +func User() *graylog.User { + return &graylog.User{ + Username: "foo", + Email: "foo@example.com", + FullName: "foo bar", + Password: "password", + Permissions: set.NewStrSet("*"), + } +} + +func DummyAdmin() *graylog.User { + return &graylog.User{ + ID: "local:admin", + Username: "admin", + Email: "hoge@example.com", + FullName: "Administrator", + Password: "password", + Permissions: set.NewStrSet("*"), + Preferences: &graylog.Preferences{ + UpdateUnfocussed: false, + EnableSmartSearch: true, + }, + Timezone: "UTC", + SessionTimeoutMs: 28800000, + External: false, + Startpage: nil, + Roles: set.NewStrSet("Admin"), + ReadOnly: true, + SessionActive: true, + LastActivity: "2018-02-21T07:35:45.926+0000", + ClientAddress: "172.18.0.1", + } +} + +func Input() *graylog.Input { + return &graylog.Input{ + Title: "test", + Node: "2ad6b340-3e5f-4a96-ae81-040cfb8b6024", + Attrs: &graylog.InputBeatsAttrs{ + BindAddress: "0.0.0.0", + Port: 514, + RecvBufferSize: 262144, + }} +} + +func IndexSet(prefix string) *graylog.IndexSet { + return &graylog.IndexSet{ + Title: "Default index set", + Description: "The Graylog default index set", + IndexPrefix: prefix, + Replicas: 0, + RotationStrategyClass: graylog.MessageCountRotationStrategy, + RotationStrategy: graylog.NewMessageCountRotationStrategy(0), + RetentionStrategyClass: graylog.DeletionRetentionStrategy, + RetentionStrategy: graylog.NewDeletionRetentionStrategy(0), + IndexOptimizationMaxNumSegments: 1, + IndexOptimizationDisabled: false, + Writable: true, + Default: true} +} + +func DummyIndexSetStats() *graylog.IndexSetStats { + return &graylog.IndexSetStats{ + Indices: 2, + Documents: 0, + Size: 1412, + } +} + +func Stream() *graylog.Stream { + return &graylog.Stream{ + MatchingType: "AND", + Description: "Stream containing all messages", + Rules: []graylog.StreamRule{}, + Title: "All messages", + } +} + +func DummyStream() *graylog.Stream { + return &graylog.Stream{ + ID: "000000000000000000000001", + CreatorUserID: "local:admin", + Outputs: []graylog.Output{}, + MatchingType: "AND", + Description: "Stream containing all messages", + CreatedAt: "2018-02-20T11:37:19.371Z", + Rules: []graylog.StreamRule{}, + AlertConditions: []graylog.AlertCondition{}, + AlertReceivers: &graylog.AlertReceivers{ + Emails: []string{}, + Users: []string{}, + }, + Title: "All messages", + IndexSetID: "5a8c086fc006c600013ca6f5", + // "content_pack": null, + } +} + +func StreamRule() *graylog.StreamRule { + return &graylog.StreamRule{ + Type: 1, + Value: "test", + Field: "tag", + } +} + +func DummyNewStreamRule() *graylog.StreamRule { + return &graylog.StreamRule{ + Type: 1, + Value: "test", + Field: "tag", + } +} + +func DummyStreamRule() *graylog.StreamRule { + return &graylog.StreamRule{ + ID: "5a9b53c7c006c6000127f965", + Type: 1, + Value: "test", + StreamID: "5a94abdac006c60001f04fc1", + Field: "tag", + } +} diff --git a/testutil/util.go b/testutil/util.go new file mode 100644 index 00000000..e7407406 --- /dev/null +++ b/testutil/util.go @@ -0,0 +1,161 @@ +package testutil + +import ( + "fmt" + "os" + "time" + + "github.com/pkg/errors" + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/client" + "github.com/suzuki-shunsuke/go-graylog/mockserver" +) + +const ( + adminName string = "admin" +) + +func GetNonAdminUser(cl *client.Client) (*graylog.User, error) { + users, _, err := cl.GetUsers() + if err != nil { + return nil, err + } + for _, user := range users { + if user.Username != adminName { + return &user, nil + } + } + return nil, nil +} + +func GetRoleOrCreate(cl *client.Client, name string) (*graylog.Role, error) { + role, ei, err := cl.GetRole(name) + if err == nil { + return role, nil + } + if ei == nil || ei.Response == nil || ei.Response.StatusCode != 404 { + return nil, err + } + role = Role() + role.Name = name + if _, err := cl.CreateRole(role); err != nil { + return nil, err + } + return role, nil +} + +func GetIndexSet(cl *client.Client, server *mockserver.Server, prefix string) (*graylog.IndexSet, func(string), error) { + iss, _, _, _, err := cl.GetIndexSets(0, 0, false) + if err != nil { + return nil, nil, err + } + if len(iss) != 0 { + return &(iss[0]), nil, nil + } + is := IndexSet(prefix) + if _, err := cl.CreateIndexSet(is); err != nil { + return nil, nil, err + } + WaitAfterCreateIndexSet(server) + return is, func(id string) { + if _, err := cl.DeleteIndexSet(id); err == nil { + WaitAfterDeleteIndexSet(server) + } + }, nil +} + +func GetStream(cl *client.Client, server *mockserver.Server, mode int) (*graylog.Stream, func(string), error) { + streams, _, _, err := cl.GetStreams() + if err != nil { + return nil, nil, err + } + if len(streams) != 0 { + if mode == 1 { + for _, stream := range streams { + if stream.IsDefault { + return &stream, nil, nil + } + } + return nil, nil, fmt.Errorf("default stream is not found") + } + if mode == 2 { + for _, stream := range streams { + if !stream.IsDefault { + return &stream, nil, nil + } + } + return nil, nil, fmt.Errorf("not default stream is not found") + } + return &(streams[0]), nil, nil + } + is, f, err := GetIndexSet(cl, server, "hoge") + if err != nil { + return nil, nil, err + } + stream := Stream() + stream.IndexSetID = is.ID + if _, err := cl.CreateStream(stream); err != nil { + if f != nil { + f(is.ID) + } + return nil, nil, err + } + return stream, func(id string) { + cl.DeleteStream(id) + if f != nil { + f(is.ID) + } + }, nil +} + +func WaitAfterCreateIndexSet(server *mockserver.Server) { + // At real graylog API we need to sleep + // 404 Index set not found. + if server == nil { + time.Sleep(1 * time.Second) + } +} + +func WaitAfterDeleteIndexSet(server *mockserver.Server) { + // At real graylog API we need to sleep + // 404 Index set not found. + if server == nil { + time.Sleep(1 * time.Second) + } +} + +// GetServerAndClient returns server and client. +func GetServerAndClient() (*mockserver.Server, *client.Client, error) { + var ( + server *mockserver.Server + err error + ) + authName := os.Getenv("GRAYLOG_AUTH_NAME") + authPass := os.Getenv("GRAYLOG_AUTH_PASSWORD") + if authName == "" { + authName = adminName + } + if authPass == "" { + authPass = "admin" + } + endpoint := os.Getenv("GRAYLOG_WEB_ENDPOINT_URI") + if endpoint == "" { + server, err = mockserver.NewServer("", nil) + if err != nil { + return nil, nil, errors.Wrap(err, "Failed to get Mock Server") + } + server.SetAuth(true) + endpoint = server.Endpoint() + } + client, err := client.NewClient(endpoint, authName, authPass) + if err != nil { + if server != nil { + server.Close() + } + return nil, nil, errors.Wrap(err, "NewClient is failure") + } + if server != nil { + server.Start() + } + return server, client, nil +} diff --git a/upload.sh b/upload.sh new file mode 100644 index 00000000..f01964e8 --- /dev/null +++ b/upload.sh @@ -0,0 +1,3 @@ +if [ -n "$TRAVIS_TAG" ]; then + make upload-dep build upload TAG=$TRAVIS_TAG +fi diff --git a/user.go b/user.go new file mode 100644 index 00000000..554c551c --- /dev/null +++ b/user.go @@ -0,0 +1,97 @@ +package graylog + +import ( + "github.com/suzuki-shunsuke/go-ptr" + "github.com/suzuki-shunsuke/go-set" +) + +// User represents a user. +type User struct { + // a unique user name used to log in with. + // ex. "local:admin" + Username string `json:"username,omitempty" v-create:"required" v-update:"required"` + // the contact email address + Email string `json:"email,omitempty" v-create:"required"` + // a descriptive name for this account, e.g. the full name. + FullName string `json:"full_name,omitempty" v-create:"required"` + Password string `json:"password,omitempty" v-create:"required"` + + ID string `json:"id,omitempty"` + // the timezone to use to display times, or leave it as it is to use the system's default. + // ex. "UTC" + Timezone string `json:"timezone,omitempty"` + // ex. "2018-03-02T06:32:01.841+0000" + LastActivity string `json:"last_activity,omitempty"` + // ex. "192.168.192.1" + ClientAddress string `json:"client_address,omitempty"` + // Session automatically end after this amount of time, unless they are actively used. + // ex. 28800000 + SessionTimeoutMs int `json:"session_timeout_ms,omitempty"` + External bool `json:"external,omitempty"` + ReadOnly bool `json:"read_only,omitempty"` + SessionActive bool `json:"session_active,omitempty"` + Preferences *Preferences `json:"preferences,omitempty"` + Startpage *Startpage `json:"startpage,omitempty"` + // Assign the relevant roles to this user to grant them access to the relevant streams and dashboards. + // The Reader role grants basic access to the system and will be enabled. + // The Admin role grants access to everything in Graylog. + // ex. ["Admin"] + Roles *set.StrSet `json:"roles,omitempty"` + Permissions *set.StrSet `json:"permissions,omitempty" v-create:"required"` +} + +// UserUpdateParams represents a user update API's parameter. +type UserUpdateParams struct { + Username string `json:"username,omitempty" v-update:"required"` + Email *string `json:"email,omitempty"` + FullName *string `json:"full_name,omitempty"` + Password *string `json:"password,omitempty"` + Timezone *string `json:"timezone,omitempty"` + SessionTimeoutMs *int `json:"session_timeout_ms,omitempty"` + Permissions *set.StrSet `json:"permissions,omitempty"` + Startpage *Startpage `json:"startpage,omitempty"` + Roles *set.StrSet `json:"roles,omitempty"` +} + +// NewUpdateParams returns Update User API's parameters. +func (user *User) NewUpdateParams() *UserUpdateParams { + return &UserUpdateParams{ + Username: user.Username, + Email: ptr.PStr(user.Email), + FullName: ptr.PStr(user.FullName), + Password: nil, + Timezone: ptr.PStr(user.Timezone), + SessionTimeoutMs: ptr.PInt(user.SessionTimeoutMs), + Permissions: user.Permissions, + Startpage: user.Startpage, + Roles: user.Roles, + } +} + +// Preferences represents user's preferences. +type Preferences struct { + UpdateUnfocussed bool `json:"updateUnfocussed,omitempty"` + EnableSmartSearch bool `json:"enableSmartSearch,omitempty"` +} + +// Startpage represents a user's startpage. +type Startpage struct { + Type string `json:"type,omitempty"` + ID string `json:"id,omitempty"` +} + +// UsersBody represents Get Users API's response body. +// Basically users don't use this struct, but this struct is public because some sub packages use this struct. +type UsersBody struct { + Users []User `json:"users"` +} + +// SetDefaultValues sets default values. +func (user *User) SetDefaultValues() { + if user.SessionTimeoutMs == 0 { + user.SessionTimeoutMs = 28800000 + } + if user.Timezone == "" { + user.Timezone = "UTC" + } +} diff --git a/user_test.go b/user_test.go new file mode 100644 index 00000000..dc73c217 --- /dev/null +++ b/user_test.go @@ -0,0 +1,27 @@ +package graylog_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/testutil" +) + +func TestUserNewUpdateParams(t *testing.T) { + user := testutil.User() + prms := user.NewUpdateParams() + if user.Username != prms.Username { + t.Fatalf(`prms.Username = "%s", wanted "%s"`, prms.Username, user.Username) + } +} + +func TestUserSetDefaultValues(t *testing.T) { + user := &graylog.User{} + user.SetDefaultValues() + if user.SessionTimeoutMs == 0 { + t.Fatal("user.SessionTimeoutMs must be set") + } + if user.Timezone == "" { + t.Fatal("user.Timezone must be set") + } +} diff --git a/util/decode.go b/util/decode.go new file mode 100644 index 00000000..424ca53b --- /dev/null +++ b/util/decode.go @@ -0,0 +1,19 @@ +package util + +import ( + "github.com/mitchellh/mapstructure" + "github.com/suzuki-shunsuke/go-set" +) + +// MSDecode +func MSDecode(input, output interface{}) error { + config := &mapstructure.DecoderConfig{ + Metadata: nil, Result: output, TagName: "json", + DecodeHook: set.MapstructureDecodeHookFromListToStrSet, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + return decoder.Decode(input) +} diff --git a/util/decode_test.go b/util/decode_test.go new file mode 100644 index 00000000..75d85852 --- /dev/null +++ b/util/decode_test.go @@ -0,0 +1,22 @@ +package util_test + +import ( + "testing" + + "github.com/suzuki-shunsuke/go-graylog" + "github.com/suzuki-shunsuke/go-graylog/util" +) + +func TestMSDecode(t *testing.T) { + data := map[string]interface{}{ + "bind_address": "0.0.0.0", + } + var attrs graylog.InputAttrs = &graylog.InputBeatsAttrs{} + if err := util.MSDecode(data, attrs); err != nil { + t.Fatal(err) + } + a, _ := attrs.(*graylog.InputBeatsAttrs) + if a.BindAddress != "0.0.0.0" { + t.Fatalf(`bind_address = "%s", wanted "%s"`, a.BindAddress, "0.0.0.0") + } +} diff --git a/validator/doc.go b/validator/doc.go new file mode 100644 index 00000000..ea4897d7 --- /dev/null +++ b/validator/doc.go @@ -0,0 +1,12 @@ +/* +Package validator provides validators for graylog's create and update APIs. +validator uses gopkg.in/go-playground/validator.v9 . + +https://godoc.org/gopkg.in/go-playground/validator.v9 + + role := &graylog.Role{} + if err := validator.CreateValidator.Struct(role); err != nil { + return "", nil, err + } +*/ +package validator diff --git a/validator/validator.go b/validator/validator.go new file mode 100644 index 00000000..b1134c72 --- /dev/null +++ b/validator/validator.go @@ -0,0 +1,49 @@ +package validator + +import ( + "regexp" + + "gopkg.in/go-playground/validator.v9" + "gopkg.in/mgo.v2/bson" + + log "github.com/sirupsen/logrus" +) + +var ( + // CreateValidator validates parameters of Create APIs. + CreateValidator *validator.Validate + // UpdateValidator validates parameters of Update APIs. + UpdateValidator *validator.Validate + indexPrefixRegexp *regexp.Regexp +) + +func init() { + indexPrefixRegexp = regexp.MustCompile(`^[a-z0-9][a-z0-9_+-]*$`) + CreateValidator = validator.New() + CreateValidator.SetTagName("v-create") + UpdateValidator = validator.New() + UpdateValidator.SetTagName("v-update") + validators := map[string]validator.Func{ + "indexprefixregexp": ValidateIndexPrefixRegexp, + "objectid": ValidateObjectID, + } + for k, v := range validators { + if err := CreateValidator.RegisterValidation(k, v); err != nil { + log.Fatal(err) + } + if err := UpdateValidator.RegisterValidation(k, v); err != nil { + log.Fatal(err) + } + } +} + +// ValidateIndexPrefixRegexp validates index prefix's pattern. +func ValidateIndexPrefixRegexp(lf validator.FieldLevel) bool { + return indexPrefixRegexp.MatchString(lf.Field().String()) +} + +// ValidateObjectID validates objectID +// https://docs.mongodb.com/manual/reference/bson-types/#objectid +func ValidateObjectID(lf validator.FieldLevel) bool { + return bson.IsObjectIdHex(lf.Field().String()) +}