├── .excludelint ├── .excludemetalint ├── .gitignore ├── .gitmodules ├── .metalinter.json ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── auth ├── auth.go ├── noop.go ├── simple.go └── simple_test.go ├── config └── base.yaml ├── generated ├── mocks │ └── generate.go └── ui │ └── statik │ └── statik.go ├── glide.lock ├── glide.yaml ├── public ├── index.html └── r2 │ └── v1 │ └── swagger │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── index.html │ ├── oauth2-redirect.html │ ├── swagger-ui-bundle.js │ ├── swagger-ui-bundle.js.map │ ├── swagger-ui-standalone-preset.js │ ├── swagger-ui-standalone-preset.js.map │ ├── swagger-ui.css │ ├── swagger-ui.css.map │ ├── swagger-ui.js │ ├── swagger-ui.js.map │ └── swagger.json ├── server ├── http │ ├── options.go │ └── server.go └── server.go ├── service ├── health │ ├── service.go │ └── service_test.go ├── r2 │ ├── errors.go │ ├── handler.go │ ├── io.go │ ├── routes.go │ ├── routes_test.go │ ├── service.go │ ├── service_test.go │ └── store │ │ ├── kv │ │ ├── options.go │ │ ├── store.go │ │ └── store_test.go │ │ ├── store.go │ │ ├── store_mock.go │ │ ├── stub │ │ └── store.go │ │ └── update_options.go └── service.go ├── services └── r2ctl │ ├── config │ ├── r2ctl.go │ └── server.go │ └── main │ └── main.go ├── tools.json └── ui ├── .env ├── .eslintrc ├── .gitignore ├── .nvmrc ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── components │ ├── HelpTooltip.js │ ├── MappingRuleEditor.js │ ├── MappingRulesTable.js │ ├── PolicyEditor.js │ ├── RollupRuleEditor.js │ ├── RollupRulesTable.js │ └── TableActions.js ├── hocs │ ├── api.js │ ├── filter.js │ ├── index.js │ └── promiseState.js ├── index.css ├── index.js ├── pages │ ├── Namespace.js │ └── Namespaces.js └── utils │ ├── helpText.js │ ├── index.js │ └── index.test.js └── yarn.lock /.excludelint: -------------------------------------------------------------------------------- 1 | (vendor/) 2 | (generated/) 3 | (_mock.go) 4 | (mock.go) 5 | -------------------------------------------------------------------------------- /.excludemetalint: -------------------------------------------------------------------------------- 1 | vendor/ 2 | generated/ 3 | _mock.go 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | *.test 3 | *.xml 4 | *.swp 5 | .idea/ 6 | .vscode/ 7 | *.iml 8 | *.ipr 9 | *.iws 10 | *.cov 11 | test.log 12 | 13 | # glide manages this 14 | vendor/ 15 | 16 | # Build binaries 17 | bin/ 18 | 19 | # Debug binaries 20 | debug 21 | debug.test 22 | 23 | # retool tools 24 | _tools/ 25 | 26 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".ci/uber-licence"] 2 | path = .ci/uber-licence 3 | url = https://github.com/uber/uber-licence 4 | [submodule ".ci"] 5 | path = .ci 6 | url = https://github.com/m3db/ci-scripts.git 7 | -------------------------------------------------------------------------------- /.metalinter.json: -------------------------------------------------------------------------------- 1 | { 2 | "Enable": 3 | [ "deadcode" 4 | , "varcheck" 5 | , "structcheck" 6 | , "goconst" 7 | , "ineffassign" 8 | , "unconvert" 9 | , "misspell" 10 | , "unparam" 11 | , "megacheck" ], 12 | "Deadline": "3m", 13 | "EnableGC": true 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.9.x" 4 | - "1.10.x" 5 | install: make install-ci 6 | env: 7 | # Set higher timeouts and package name for travis 8 | - TEST_TIMEOUT_SCALE=20 PACKAGE=github.com/m3db/m3ctl NPROC=2 9 | sudo: required 10 | dist: trusty 11 | script: 12 | - make all 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | We'd love your help making m3ctl great! 5 | 6 | ## Getting Started 7 | 8 | m3ctl uses Go vendoring to manage dependencies. 9 | To get started: 10 | 11 | ```bash 12 | git submodule update --init --recursive 13 | make test 14 | ``` 15 | 16 | ## Making A Change 17 | 18 | *Before making any significant changes, please [open an 19 | issue](https://github.com/m3db/m3ctl/issues).* Discussing your proposed 20 | changes ahead of time will make the contribution process smooth for everyone. 21 | 22 | Once we've discussed your changes and you've got your code ready, make sure 23 | that tests are passing (`make test` or `make cover`) and open your PR! Your 24 | pull request is most likely to be accepted if it: 25 | 26 | * Includes tests for new functionality. 27 | * Follows the guidelines in [Effective 28 | Go](https://golang.org/doc/effective_go.html) and the [Go team's common code 29 | review comments](https://github.com/golang/go/wiki/CodeReviewComments). 30 | * Has a [good commit 31 | message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) 2018 Uber Technologies, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST))) 2 | include $(SELF_DIR)/.ci/common.mk 3 | 4 | SHELL=/bin/bash -o pipefail 5 | 6 | html_report := coverage.html 7 | test := .ci/test-cover.sh 8 | test_ci_integration := .ci/test-integration.sh 9 | convert-test-data := .ci/convert-test-data.sh 10 | coverfile := cover.out 11 | coverage_xml := coverage.xml 12 | junit_xml := junit.xml 13 | test_log := test.log 14 | lint_check := .ci/lint.sh 15 | metalint_check := .ci/metalint.sh 16 | metalint_config := .metalinter.json 17 | metalint_exclude := .excludemetalint 18 | m3ctl_package := github.com/m3db/m3ctl 19 | gopath_prefix := $(GOPATH)/src 20 | vendor_prefix := vendor 21 | mocks_output_dir := generated/mocks/mocks 22 | package_root := github.com/m3db/m3ctl 23 | mocks_rules_dir := generated/mocks 24 | ui_codegen_dir := generated/ui 25 | auto_gen := .ci/auto-gen.sh 26 | license_node_modules := $(license_dir)/node_modules 27 | license_dir := .ci/uber-licence 28 | retool_bin_path := _tools/bin 29 | retool_package := github.com/twitchtv/retool 30 | node_version := v6 31 | 32 | BUILD := $(abspath ./bin) 33 | LINUX_AMD64_ENV := GOOS=linux GOARCH=amd64 CGO_ENABLED=0 34 | 35 | SERVICES := \ 36 | r2ctl 37 | 38 | .PHONY: setup 39 | setup: 40 | mkdir -p $(BUILD) 41 | 42 | define SERVICE_RULES 43 | 44 | $(SERVICE): setup 45 | ifeq ($(SERVICE),r2ctl) 46 | @echo "Building $(SERVICE) dependencies" 47 | make build-ui-statik-packages 48 | endif 49 | @echo Building $(SERVICE) 50 | $(VENDOR_ENV) go build -o $(BUILD)/$(SERVICE) ./services/$(SERVICE)/main/. 51 | 52 | $(SERVICE)-linux-amd64: 53 | $(LINUX_AMD64_ENV) make $(SERVICE) 54 | 55 | endef 56 | 57 | services: $(SERVICES) 58 | services-linux-amd64: 59 | $(LINUX_AMD64_ENV) make services 60 | 61 | cmd-using-node-version: 62 | ifneq ($(shell brew --prefix nvm 2>/dev/null),) 63 | @echo "Using nvm from brew to select node version $(node_version)" 64 | source $(shell brew --prefix nvm)/nvm.sh && nvm use $(node_version) && bash -c "$(node_cmd)" 65 | else ifneq ($(shell type nvm 2>/dev/null),) 66 | @echo "Using nvm to select node version $(node_version)" 67 | nvm use $(node_version) && bash -c "$(node_cmd)" 68 | else 69 | @echo "Not using nvm, using node version $(shell node --version)" 70 | bash -c "$(node_cmd)" 71 | endif 72 | 73 | build-ui: 74 | ifeq ($(shell ls ./ui/build 2>/dev/null),) 75 | # Need to use subshell output of set-node-version as cannot 76 | # set side-effects of nvm to later commands 77 | @echo "Building UI components, if npm install or build fails try: npm cache clean" 78 | make cmd-using-node-version node_cmd="cd ui && npm install && npm run build" 79 | else 80 | @echo "Skip building UI components, already built, to rebuild first make clean" 81 | endif 82 | # Move public assets into public subdirectory so that it can 83 | # be included in the single statik package built from ./ui/build 84 | rm -rf ./ui/build/public 85 | cp -r ./public ./ui/build/public 86 | 87 | build-ui-statik-packages: build-ui install-tools 88 | mkdir -p $(ui_codegen_dir) 89 | $(retool_bin_path)/statik -src ./ui/build -dest $(ui_codegen_dir) -p statik 90 | 91 | $(foreach SERVICE,$(SERVICES),$(eval $(SERVICE_RULES))) 92 | 93 | .PHONY: lint 94 | lint: 95 | @which golint > /dev/null || go get -u github.com/golang/lint/golint 96 | $(lint_check) 97 | 98 | .PHONY: metalint 99 | metalint: install-metalinter 100 | @($(metalint_check) $(metalint_config) $(metalint_exclude) && echo "metalinted successfully!") || (echo "metalinter failed" && exit 1) 101 | 102 | .PHONY: test-internal 103 | test-internal: 104 | @which go-junit-report > /dev/null || go get -u github.com/sectioneight/go-junit-report 105 | $(test) $(coverfile) | tee $(test_log) 106 | 107 | .PHONY: test-integration 108 | test-integration: 109 | go test -v -tags=integration ./integration 110 | 111 | .PHONY: test-xml 112 | test-xml: test-internal 113 | go-junit-report < $(test_log) > $(junit_xml) 114 | gocov convert $(coverfile) | gocov-xml > $(coverage_xml) 115 | @$(convert-test-data) $(coverage_xml) 116 | @rm $(coverfile) &> /dev/null 117 | 118 | .PHONY: test 119 | test: test-internal 120 | gocov convert $(coverfile) | gocov report 121 | 122 | .PHONY: testhtml 123 | testhtml: test-internal 124 | gocov convert $(coverfile) | gocov-html > $(html_report) && open $(html_report) 125 | @rm -f $(test_log) &> /dev/null 126 | 127 | .PHONY: test-ci-unit 128 | test-ci-unit: test-internal 129 | @which goveralls > /dev/null || go get -u -f github.com/mattn/goveralls 130 | goveralls -coverprofile=$(coverfile) -service=travis-ci || echo -e "\x1b[31mCoveralls failed\x1b[m" 131 | 132 | .PHONY: test-ci-integration 133 | test-ci-integration: 134 | $(test_ci_integration) 135 | 136 | .PHONY: clean 137 | clean: 138 | @rm -f *.html *.xml *.out *.test $(BUILD)/* 139 | @rm -rf ./ui/build 140 | 141 | .PHONY: all 142 | all: lint metalint test-ci-unit services 143 | @echo Made all successfully 144 | 145 | .PHONY: install-licence-bin 146 | install-license-bin: install-vendor 147 | @echo Installing node modules 148 | git submodule update --init --recursive 149 | [ -d $(license_node_modules) ] || (cd $(license_dir) && npm install) 150 | 151 | .PHONY: install-mockgen 152 | install-mockgen: install-vendor 153 | @echo Installing mockgen 154 | glide install 155 | 156 | .PHONY: install-retool 157 | install-retool: 158 | @which retool >/dev/null || go get $(retool_package) 159 | 160 | .PHONY: install-tools 161 | install-tools: install-retool 162 | @echo "Installing tools" 163 | @retool sync >/dev/null 2>/dev/null 164 | @retool build >/dev/null 2>/dev/null 165 | 166 | .PHONY: mock-gen 167 | mock-gen: install-mockgen install-license-bin install-util-mockclean 168 | @echo Generating mocks 169 | PACKAGE=$(package_root) $(auto_gen) $(mocks_output_dir) $(mocks_rules_dir) 170 | 171 | .PHONY: mock-gen-no-deps 172 | mock-gen-no-deps: 173 | @echo Generating mocks 174 | PACKAGE=$(package_root) $(auto_gen) $(mocks_output_dir) $(mocks_rules_dir) 175 | 176 | .DEFAULT_GOAL := all 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Migration Warning 2 | ================= 3 | This repository has been migrated to github.com/m3db/m3. It's contents can be found at github.com/m3db/m3/src/ctl. Follow along there for updates. This repository is marked archived, and will no longer receive any updates. 4 | 5 | # m3ctl [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] 6 | 7 | Configuration controller for the M3DB ecosystem. Provides an http API to perform CRUD operatations on 8 | the various configs for M3DB compontents. 9 | 10 | ### Run the R2 App 11 | 12 | ```bash 13 | git clone --recursive https://github.com/m3db/m3ctl.git 14 | cd m3ctl 15 | glide install -v 16 | make && ./bin/r2ctl -f config/base.yaml 17 | open http://localhost:9000 18 | ``` 19 | 20 | **UI** 21 | `/` 22 | 23 | **API Server** 24 | `/r2/v1` 25 | 26 | **API Docs (via Swagger)** 27 | `public/r2/v1/swagger` 28 | 29 | 30 |
31 | 32 | This project is released under the [Apache License, Version 2.0](LICENSE). 33 | 34 | [doc-img]: https://godoc.org/github.com/m3db/m3ggregator?status.svg 35 | [doc]: https://godoc.org/github.com/m3db/m3ctl 36 | [ci-img]: https://travis-ci.org/m3db/m3ctl.svg?branch=master 37 | [ci]: https://travis-ci.org/m3db/m3ctl 38 | [cov-img]: https://coveralls.io/repos/m3db/m3ctl/badge.svg?branch=master&service=github 39 | [cov]: https://coveralls.io/github/m3db/m3ctl?branch=master 40 | -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package auth 22 | 23 | import ( 24 | "context" 25 | "net/http" 26 | ) 27 | 28 | type keyType int 29 | 30 | // AuthorizationType designates a type of authorization. 31 | type AuthorizationType int 32 | 33 | type errorResponseHandler func(w http.ResponseWriter, code int, msg string) error 34 | 35 | const ( 36 | // UserIDField is a key 37 | UserIDField keyType = iota 38 | ) 39 | 40 | const ( 41 | // UnknownAuthorization is the unknown authorizationType case. 42 | UnknownAuthorization AuthorizationType = iota 43 | // NoAuthorization is the no authorizationType case. 44 | NoAuthorization 45 | // ReadOnlyAuthorization is the read only authorizationType case. 46 | ReadOnlyAuthorization 47 | // WriteOnlyAuthorization is the write only authorizationType case. 48 | WriteOnlyAuthorization 49 | // ReadWriteAuthorization is the read and write authorizationType case. 50 | ReadWriteAuthorization 51 | ) 52 | 53 | // HTTPAuthService defines how to handle requests for various http authentication and authorization methods. 54 | type HTTPAuthService interface { 55 | // NewAuthHandler should return a handler that performs some check on the request coming into the given handler 56 | // and then runs the handler if it is. If the request passes authentication/authorization successfully, it should call SetUser 57 | // to make the callers id available to the service in a global context. errHandler should be passed in to properly format the 58 | // the error and respond the the request in the event of bad auth. 59 | NewAuthHandler(authType AuthorizationType, next http.Handler, errHandler errorResponseHandler) http.Handler 60 | 61 | // SetUser sets a userID that identifies the api caller in the global context. 62 | SetUser(parent context.Context, userID string) context.Context 63 | 64 | // GetUser fetches the ID of an api caller from the global context. 65 | GetUser(ctx context.Context) (string, error) 66 | } 67 | -------------------------------------------------------------------------------- /auth/noop.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package auth 22 | 23 | import ( 24 | "context" 25 | "net/http" 26 | ) 27 | 28 | const ( 29 | noopUser = "noopUser" 30 | ) 31 | 32 | // noopAuth lets everything through 33 | type noopAuth struct{} 34 | 35 | // NewNoopAuth creates a new noop auth instance. 36 | func NewNoopAuth() HTTPAuthService { 37 | return noopAuth{} 38 | } 39 | 40 | func (a noopAuth) NewAuthHandler(_ AuthorizationType, next http.Handler, errHandler errorResponseHandler) http.Handler { 41 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 42 | next.ServeHTTP(w, r) 43 | }) 44 | } 45 | 46 | // nolint:unparam 47 | func (a noopAuth) SetUser(parent context.Context, userID string) context.Context { 48 | return parent 49 | } 50 | 51 | func (a noopAuth) GetUser(ctx context.Context) (string, error) { 52 | return noopUser, nil 53 | } 54 | -------------------------------------------------------------------------------- /auth/simple.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package auth 22 | 23 | import ( 24 | "context" 25 | "fmt" 26 | "net/http" 27 | ) 28 | 29 | // SimpleAuthConfig holds this configuration necessary for a simple auth implementation. 30 | type SimpleAuthConfig struct { 31 | Authentication authenticationConfig `yaml:"authentication"` 32 | Authorization authorizationConfig `yaml:"authorization"` 33 | } 34 | 35 | // authenticationConfig holds this configuration necessary for a simple authentication implementation. 36 | type authenticationConfig struct { 37 | // This is an HTTP header that identifies the user performing the operation. 38 | UserIDHeader string `yaml:"userIDHeader" validate:"nonzero"` 39 | // This is an HTTP header that identifies the user originating the operation. 40 | OriginatorIDHeader string `yaml:"originatorIDHeader"` 41 | } 42 | 43 | // authorizationConfig holds this configuration necessary for a simple authorization implementation. 44 | type authorizationConfig struct { 45 | // This indicates whether reads should use a read whitelist. 46 | ReadWhitelistEnabled bool `yaml:"readWhitelistEnabled,omitempty"` 47 | // This is a list of users that are allowed to perform read operations. 48 | ReadWhitelistedUserIDs []string `yaml:"readWhitelistedUserIDs,omitempty"` 49 | // This indicates whether writes should use a write whitelist. 50 | WriteWhitelistEnabled bool `yaml:"writeWhitelistEnabled,omitempty"` 51 | // This is a list of users that are allowed to perform write operations. 52 | WriteWhitelistedUserIDs []string `yaml:"writeWhitelistedUserIDs,omitempty"` 53 | } 54 | 55 | // NewSimpleAuth creates a new simple auth instance given using the provided config. 56 | func (ac SimpleAuthConfig) NewSimpleAuth() HTTPAuthService { 57 | return simpleAuth{ 58 | authentication: simpleAuthentication{ 59 | userIDHeader: ac.Authentication.UserIDHeader, 60 | originatorIDHeader: ac.Authentication.OriginatorIDHeader, 61 | }, 62 | authorization: simpleAuthorization{ 63 | readWhitelistEnabled: ac.Authorization.ReadWhitelistEnabled, 64 | readWhitelistedUserIDs: ac.Authorization.ReadWhitelistedUserIDs, 65 | writeWhitelistEnabled: ac.Authorization.WriteWhitelistEnabled, 66 | writeWhitelistedUserIDs: ac.Authorization.WriteWhitelistedUserIDs, 67 | }, 68 | } 69 | } 70 | 71 | type simpleAuth struct { 72 | authentication simpleAuthentication 73 | authorization simpleAuthorization 74 | } 75 | 76 | type simpleAuthentication struct { 77 | userIDHeader string 78 | originatorIDHeader string 79 | } 80 | 81 | func (a simpleAuthentication) authenticate(userID string) error { 82 | if userID == "" { 83 | return fmt.Errorf("must provide header: [%s]", a.userIDHeader) 84 | } 85 | return nil 86 | } 87 | 88 | type simpleAuthorization struct { 89 | readWhitelistEnabled bool 90 | readWhitelistedUserIDs []string 91 | writeWhitelistEnabled bool 92 | writeWhitelistedUserIDs []string 93 | } 94 | 95 | func (a simpleAuthorization) authorize(authType AuthorizationType, userID string) error { 96 | switch authType { 97 | case NoAuthorization: 98 | return nil 99 | case ReadOnlyAuthorization: 100 | return a.authorizeUserForRead(userID) 101 | case WriteOnlyAuthorization: 102 | return a.authorizeUserForWrite(userID) 103 | case ReadWriteAuthorization: 104 | if err := a.authorizeUserForRead(userID); err != nil { 105 | return err 106 | } 107 | return a.authorizeUserForWrite(userID) 108 | default: 109 | return fmt.Errorf("unsupported authorization type %v passed to handler", authType) 110 | } 111 | } 112 | 113 | func authorizeUserForAccess(userID string, whitelistedUserIDs []string, enabled bool) error { 114 | if !enabled { 115 | return nil 116 | } 117 | 118 | for _, u := range whitelistedUserIDs { 119 | if u == userID { 120 | return nil 121 | } 122 | } 123 | return fmt.Errorf("supplied userID: [%s] is not authorized", userID) 124 | } 125 | 126 | func (a simpleAuthorization) authorizeUserForRead(userID string) error { 127 | return authorizeUserForAccess(userID, a.readWhitelistedUserIDs, a.readWhitelistEnabled) 128 | } 129 | 130 | func (a simpleAuthorization) authorizeUserForWrite(userID string) error { 131 | return authorizeUserForAccess(userID, a.writeWhitelistedUserIDs, a.writeWhitelistEnabled) 132 | } 133 | 134 | // Authenticate looks for a header defining a user name. If it finds it, runs the actual http handler passed as a parameter. 135 | // Otherwise, it returns an Unauthorized http response. 136 | func (a simpleAuth) NewAuthHandler(authType AuthorizationType, next http.Handler, errHandler errorResponseHandler) http.Handler { 137 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 138 | var ( 139 | userID = r.Header.Get(a.authentication.userIDHeader) 140 | originatorID = r.Header.Get(a.authentication.originatorIDHeader) 141 | ) 142 | if originatorID == "" { 143 | originatorID = userID 144 | } 145 | err := a.authentication.authenticate(originatorID) 146 | if err != nil { 147 | errHandler(w, http.StatusUnauthorized, err.Error()) 148 | return 149 | } 150 | 151 | err = a.authorization.authorize(authType, userID) 152 | if err != nil { 153 | errHandler(w, http.StatusForbidden, err.Error()) 154 | return 155 | } 156 | 157 | ctx := a.SetUser(r.Context(), originatorID) 158 | next.ServeHTTP(w, r.WithContext(ctx)) 159 | }) 160 | } 161 | 162 | // SetUser sets the user making the changes to the api. 163 | func (a simpleAuth) SetUser(parent context.Context, userID string) context.Context { 164 | return context.WithValue(parent, UserIDField, userID) 165 | } 166 | 167 | // GetUser fetches the ID of an api caller from the global context. 168 | func (a simpleAuth) GetUser(ctx context.Context) (string, error) { 169 | id := ctx.Value(UserIDField) 170 | if id == nil { 171 | return "", fmt.Errorf("couldn't identify user") 172 | } 173 | return id.(string), nil 174 | } 175 | -------------------------------------------------------------------------------- /auth/simple_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package auth 22 | 23 | import ( 24 | "bytes" 25 | "context" 26 | "fmt" 27 | "net/http" 28 | "net/http/httptest" 29 | "testing" 30 | 31 | "github.com/stretchr/testify/require" 32 | yaml "gopkg.in/yaml.v2" 33 | ) 34 | 35 | var ( 36 | testUser = "testUser" 37 | testOriginator = "testOriginator" 38 | testUserIDHeader = "testUserIDHeader" 39 | testOriginatorIDHeader = "testOriginatorIDHeader" 40 | testConfig = SimpleAuthConfig{ 41 | Authentication: authenticationConfig{ 42 | UserIDHeader: "testHeader", 43 | }, 44 | Authorization: authorizationConfig{ 45 | ReadWhitelistEnabled: true, 46 | WriteWhitelistEnabled: false, 47 | ReadWhitelistedUserIDs: []string{testUser}, 48 | WriteWhitelistedUserIDs: []string{}, 49 | }, 50 | } 51 | testConfigWithOriginatorID = SimpleAuthConfig{ 52 | Authentication: authenticationConfig{ 53 | UserIDHeader: testUserIDHeader, 54 | OriginatorIDHeader: testOriginatorIDHeader, 55 | }, 56 | Authorization: authorizationConfig{ 57 | ReadWhitelistEnabled: true, 58 | WriteWhitelistEnabled: true, 59 | ReadWhitelistedUserIDs: []string{}, 60 | WriteWhitelistedUserIDs: []string{testUser}, 61 | }, 62 | } 63 | ) 64 | 65 | func TestSimpleAuthConfigUnmarshal(t *testing.T) { 66 | configStr := ` 67 | authentication: 68 | userIDHeader: user-id 69 | authorization: 70 | readWhitelistEnabled: true 71 | readWhitelistedUserIDs: 72 | - foo 73 | - bar 74 | writeWhitelistEnabled: true 75 | writeWhitelistedUserIDs: 76 | - bar 77 | - baz 78 | ` 79 | var cfg SimpleAuthConfig 80 | require.NoError(t, yaml.Unmarshal([]byte(configStr), &cfg)) 81 | require.Equal(t, "user-id", cfg.Authentication.UserIDHeader) 82 | require.True(t, cfg.Authorization.ReadWhitelistEnabled) 83 | require.Equal(t, []string{"foo", "bar"}, cfg.Authorization.ReadWhitelistedUserIDs) 84 | require.True(t, cfg.Authorization.WriteWhitelistEnabled) 85 | require.Equal(t, []string{"bar", "baz"}, cfg.Authorization.WriteWhitelistedUserIDs) 86 | } 87 | 88 | func TestNewSimpleAuth(t *testing.T) { 89 | an := testConfig.NewSimpleAuth().(simpleAuth).authentication 90 | az := testConfig.NewSimpleAuth().(simpleAuth).authorization 91 | require.Equal(t, an.userIDHeader, "testHeader") 92 | require.Equal(t, az.readWhitelistEnabled, true) 93 | require.Equal(t, az.writeWhitelistEnabled, false) 94 | require.Equal(t, az.readWhitelistedUserIDs, []string{"testUser"}) 95 | require.Equal(t, az.writeWhitelistedUserIDs, []string{}) 96 | } 97 | 98 | func TestSetUser(t *testing.T) { 99 | a := testConfig.NewSimpleAuth() 100 | ctx := context.Background() 101 | require.Nil(t, ctx.Value(UserIDField)) 102 | ctx = a.SetUser(ctx, "foo") 103 | require.Equal(t, "foo", ctx.Value(UserIDField).(string)) 104 | } 105 | 106 | func TestGetUser(t *testing.T) { 107 | a := testConfig.NewSimpleAuth() 108 | ctx := context.Background() 109 | 110 | id, err := a.GetUser(ctx) 111 | require.Empty(t, id) 112 | require.Error(t, err) 113 | 114 | ctx = a.SetUser(ctx, "foo") 115 | id, err = a.GetUser(ctx) 116 | require.Equal(t, "foo", id) 117 | require.NoError(t, err) 118 | } 119 | 120 | func TestSimpleAuthenticationAuthenticate(t *testing.T) { 121 | authentication := simpleAuthentication{ 122 | userIDHeader: "foo", 123 | } 124 | 125 | require.Nil(t, authentication.authenticate("bar")) 126 | require.EqualError(t, authentication.authenticate(""), "must provide header: [foo]") 127 | } 128 | 129 | func TestSimpleAuthorizationAuthorize(t *testing.T) { 130 | authorization := simpleAuthorization{ 131 | readWhitelistEnabled: true, 132 | writeWhitelistEnabled: false, 133 | readWhitelistedUserIDs: []string{"foo", "bar"}, 134 | writeWhitelistedUserIDs: []string{"foo", "bar", "baz"}, 135 | } 136 | 137 | require.Nil(t, authorization.authorize(ReadOnlyAuthorization, "foo")) 138 | require.Nil(t, authorization.authorize(WriteOnlyAuthorization, "foo")) 139 | require.Nil(t, authorization.authorize(NoAuthorization, "foo")) 140 | require.Nil(t, authorization.authorize(WriteOnlyAuthorization, "baz")) 141 | require.EqualError(t, authorization.authorize(ReadOnlyAuthorization, "baz"), "supplied userID: [baz] is not authorized") 142 | require.EqualError(t, authorization.authorize(ReadWriteAuthorization, "baz"), "supplied userID: [baz] is not authorized") 143 | require.EqualError(t, authorization.authorize(AuthorizationType(100), "baz"), "unsupported authorization type 100 passed to handler") 144 | } 145 | 146 | func TestAuthorizeUserForAccess(t *testing.T) { 147 | userID := "user2" 148 | whitelistedUserIDs := []string{"user1", "user2", "user3"} 149 | require.NoError(t, authorizeUserForAccess(userID, whitelistedUserIDs, false)) 150 | require.NoError(t, authorizeUserForAccess(userID, whitelistedUserIDs, true)) 151 | } 152 | 153 | func TestAuthorizeUserForAccessUserNotWhitelisted(t *testing.T) { 154 | userID := "user4" 155 | whitelistedUserIDs := []string{"user1", "user2", "user3"} 156 | require.NoError(t, authorizeUserForAccess(userID, whitelistedUserIDs, false)) 157 | require.EqualError( 158 | t, 159 | authorizeUserForAccess(userID, whitelistedUserIDs, true), 160 | fmt.Sprintf("supplied userID: [%s] is not authorized", userID), 161 | ) 162 | } 163 | 164 | func TestHealthCheck(t *testing.T) { 165 | a := testConfig.NewSimpleAuth() 166 | f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 167 | v, err := a.GetUser(r.Context()) 168 | require.NoError(t, err) 169 | require.Equal(t, "testHeader", v) 170 | }) 171 | 172 | wrappedCall := a.NewAuthHandler(NoAuthorization, f, writeAPIResponse) 173 | wrappedCall.ServeHTTP(httptest.NewRecorder(), &http.Request{}) 174 | } 175 | 176 | func TestAuthenticateFailure(t *testing.T) { 177 | a := testConfig.NewSimpleAuth() 178 | f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 179 | v, err := a.GetUser(r.Context()) 180 | require.NoError(t, err) 181 | require.Equal(t, "testHeader", v) 182 | }) 183 | recorder := httptest.NewRecorder() 184 | 185 | wrappedCall := a.NewAuthHandler(NoAuthorization, f, writeAPIResponse) 186 | wrappedCall.ServeHTTP(recorder, &http.Request{}) 187 | require.Equal(t, http.StatusUnauthorized, recorder.Code) 188 | require.Equal(t, "application/json", recorder.HeaderMap["Content-Type"][0]) 189 | } 190 | 191 | func TestAuthenticateWithOriginatorID(t *testing.T) { 192 | req, err := http.NewRequest(http.MethodPost, "/update", nil) 193 | require.NoError(t, err) 194 | req.Header.Add(testUserIDHeader, testUser) 195 | req.Header.Add(testOriginatorIDHeader, testOriginator) 196 | 197 | a := testConfigWithOriginatorID.NewSimpleAuth() 198 | f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 199 | v, err := a.GetUser(r.Context()) 200 | require.NoError(t, err) 201 | require.Equal(t, testOriginator, v) 202 | writeAPIResponse(w, http.StatusOK, "success!") 203 | }) 204 | recorder := httptest.NewRecorder() 205 | wrappedCall := a.NewAuthHandler(NoAuthorization, f, writeAPIResponse) 206 | wrappedCall.ServeHTTP(recorder, req) 207 | require.Equal(t, http.StatusOK, recorder.Code) 208 | require.Equal(t, "application/json", recorder.HeaderMap["Content-Type"][0]) 209 | } 210 | 211 | func TestAuthorizeFailure(t *testing.T) { 212 | a := testConfig.NewSimpleAuth() 213 | f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 214 | v, err := a.GetUser(r.Context()) 215 | require.NoError(t, err) 216 | require.Equal(t, "testHeader", v) 217 | }) 218 | recorder := httptest.NewRecorder() 219 | req, err := http.NewRequest("Get", "/create", bytes.NewBuffer(nil)) 220 | require.NoError(t, err) 221 | req.Header.Add("testHeader", "validUserID") 222 | 223 | wrappedCall := a.NewAuthHandler(ReadOnlyAuthorization, f, writeAPIResponse) 224 | wrappedCall.ServeHTTP(recorder, req) 225 | require.Equal(t, http.StatusForbidden, recorder.Code) 226 | require.Equal(t, "application/json", recorder.HeaderMap["Content-Type"][0]) 227 | } 228 | 229 | func writeAPIResponse(w http.ResponseWriter, code int, msg string) error { 230 | w.Header().Set("Content-Type", "application/json") 231 | w.WriteHeader(code) 232 | _, err := w.Write([]byte(msg)) 233 | 234 | return err 235 | } 236 | -------------------------------------------------------------------------------- /config/base.yaml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: info 3 | stdout: true 4 | 5 | http: 6 | host: 0.0.0.0 7 | port: 9000 8 | readTimeout: 10s 9 | writeTimeout: 10s 10 | 11 | metrics: 12 | m3: 13 | hostPort: 127.0.0.1:5000 # local collector host port for m3 metrics 14 | service: r2ctl 15 | env: test 16 | includeHost: true 17 | samplingRate: 0.01 18 | 19 | store: 20 | stub: true 21 | 22 | -------------------------------------------------------------------------------- /generated/mocks/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // mockgen rules for generating mocks using file mode. 22 | //go:generate sh -c "mockgen -package=store $PACKAGE/service/r2/store Store | mockclean -pkg $PACKAGE/store -out $GOPATH/src/$PACKAGE/service/r2/store/store_mock.go" 23 | 24 | package mocks 25 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: a3bc31056f5227f10e9890dcd5c1fed4ee7dd460a2a32778715917686ac6879c 2 | updated: 2018-10-29T13:34:52.22873-04:00 3 | imports: 4 | - name: github.com/apache/thrift 5 | version: 9549b25c77587b29be4e0b5c258221a4ed85d37a 6 | subpackages: 7 | - lib/go/thrift 8 | - name: github.com/beorn7/perks 9 | version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9 10 | subpackages: 11 | - quantile 12 | - name: github.com/coreos/etcd 13 | version: 694728c496e22dfa5719c78ff23cc982e15bcb2f 14 | subpackages: 15 | - auth/authpb 16 | - clientv3 17 | - clientv3/concurrency 18 | - etcdserver/api/v3rpc/rpctypes 19 | - etcdserver/etcdserverpb 20 | - mvcc/mvccpb 21 | - name: github.com/facebookgo/clock 22 | version: 600d898af40aa09a7a93ecb9265d87b0504b6f03 23 | - name: github.com/ghodss/yaml 24 | version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee 25 | - name: github.com/go-ole/go-ole 26 | version: de8695c8edbf8236f30d6e1376e20b198a028d42 27 | - name: github.com/go-playground/locales 28 | version: 1e5f1161c6416a5ff48840eb8724a394e48cc534 29 | subpackages: 30 | - currency 31 | - name: github.com/go-playground/universal-translator 32 | version: 71201497bace774495daed26a3874fd339e0b538 33 | - name: github.com/gogo/protobuf 34 | version: 636bf0302bc95575d69441b25a2603156ffdddf1 35 | subpackages: 36 | - gogoproto 37 | - proto 38 | - protoc-gen-gogo/descriptor 39 | - name: github.com/golang/mock 40 | version: c34cdb4725f4c3844d095133c6e40e448b86589b 41 | subpackages: 42 | - gomock 43 | - name: github.com/golang/protobuf 44 | version: 5a0f697c9ed9d68fef0116532c6e05cfeae00e55 45 | subpackages: 46 | - proto 47 | - protoc-gen-go/descriptor 48 | - ptypes 49 | - ptypes/any 50 | - ptypes/duration 51 | - ptypes/timestamp 52 | - name: github.com/google/uuid 53 | version: 9b3b1e0f5f99ae461456d768e7d301a7acdaa2d8 54 | - name: github.com/gorilla/context 55 | version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 56 | - name: github.com/gorilla/mux 57 | version: 53c1911da2b537f792e7cafcb446b05ffe33b996 58 | - name: github.com/grpc-ecosystem/go-grpc-prometheus 59 | version: 6b7015e65d366bf3f19b2b2a000a831940f0f7e0 60 | - name: github.com/grpc-ecosystem/grpc-gateway 61 | version: 84398b94e188ee336f307779b57b3aa91af7063c 62 | - name: github.com/m3db/m3 63 | version: 16e2dfb2206a8ead03f733a7032b1bc2b83d4b93 64 | subpackages: 65 | - src/cluster/client 66 | - src/cluster/client/etcd 67 | - src/cluster/etcd/watchmanager 68 | - src/cluster/generated/proto/commonpb 69 | - src/cluster/generated/proto/metadatapb 70 | - src/cluster/generated/proto/placementpb 71 | - src/cluster/kv 72 | - src/cluster/kv/etcd 73 | - src/cluster/kv/util 74 | - src/cluster/kv/util/runtime 75 | - src/cluster/placement 76 | - src/cluster/placement/algo 77 | - src/cluster/placement/selector 78 | - src/cluster/placement/service 79 | - src/cluster/placement/storage 80 | - src/cluster/services 81 | - src/cluster/services/heartbeat/etcd 82 | - src/cluster/services/leader 83 | - src/cluster/services/leader/campaign 84 | - src/cluster/services/leader/election 85 | - src/cluster/shard 86 | - src/metrics/aggregation 87 | - src/metrics/errors 88 | - src/metrics/filters 89 | - src/metrics/generated/proto/aggregationpb 90 | - src/metrics/generated/proto/metricpb 91 | - src/metrics/generated/proto/pipelinepb 92 | - src/metrics/generated/proto/policypb 93 | - src/metrics/generated/proto/rulepb 94 | - src/metrics/generated/proto/transformationpb 95 | - src/metrics/metadata 96 | - src/metrics/metric 97 | - src/metrics/metric/id 98 | - src/metrics/pipeline 99 | - src/metrics/pipeline/applied 100 | - src/metrics/policy 101 | - src/metrics/rules 102 | - src/metrics/rules/store/kv 103 | - src/metrics/rules/validator 104 | - src/metrics/rules/validator/namespace 105 | - src/metrics/rules/validator/namespace/kv 106 | - src/metrics/rules/validator/namespace/static 107 | - src/metrics/rules/view 108 | - src/metrics/rules/view/changes 109 | - src/metrics/transformation 110 | - src/metrics/x/bytes 111 | - name: github.com/m3db/m3x 112 | version: 943173a151c8a6b2da1f93f814c27c4e4a1e2050 113 | subpackages: 114 | - checked 115 | - clock 116 | - close 117 | - config 118 | - errors 119 | - instrument 120 | - log 121 | - pool 122 | - process 123 | - resource 124 | - retry 125 | - sync 126 | - time 127 | - watch 128 | - name: github.com/m3db/prometheus_client_golang 129 | version: 8ae269d24972b8695572fa6b2e3718b5ea82d6b4 130 | subpackages: 131 | - prometheus 132 | - prometheus/promhttp 133 | - name: github.com/m3db/prometheus_client_model 134 | version: 8b2299a4bf7d7fc10835527021716d4b4a6e8700 135 | subpackages: 136 | - go 137 | - name: github.com/m3db/prometheus_common 138 | version: 25aaa3dff79bb48116615ebe1dea6a494b74ce77 139 | subpackages: 140 | - expfmt 141 | - internal/bitbucket.org/ww/goautoneg 142 | - model 143 | - name: github.com/m3db/prometheus_procfs 144 | version: 1878d9fbb537119d24b21ca07effd591627cd160 145 | - name: github.com/matttproud/golang_protobuf_extensions 146 | version: c12348ce28de40eed0136aa2b644d0ee0650e56c 147 | subpackages: 148 | - pbutil 149 | - name: github.com/MichaelTJones/pcg 150 | version: df440c6ed7ed8897ac98a408365e5e89c7becf1a 151 | - name: github.com/pborman/uuid 152 | version: adf5a7427709b9deb95d29d3fa8a2bf9cfd388f1 153 | - name: github.com/prometheus/client_golang 154 | version: c5b7fccd204277076155f10851dad72b76a49317 155 | - name: github.com/prometheus/client_model 156 | version: fa8ad6fec33561be4280a8f0514318c79d7f6cb6 157 | - name: github.com/prometheus/common 158 | version: 195bde7883f7c39ea62b0d92ab7359b5327065cb 159 | - name: github.com/prometheus/procfs 160 | version: 1878d9fbb537119d24b21ca07effd591627cd160 161 | - name: github.com/rakyll/statik 162 | version: 19b88da8fc15428620782ba18f68423130e7ac7d 163 | subpackages: 164 | - fs 165 | - name: github.com/shirou/gopsutil 166 | version: b62e301a8b9958eebb7299683eb57fab229a9501 167 | - name: github.com/shirou/w32 168 | version: bb4de0191aa41b5507caa14b0650cdbddcd9280b 169 | - name: github.com/StackExchange/wmi 170 | version: e542ed97d15e640bdc14b5c12162d59e8fc67324 171 | - name: github.com/uber-go/atomic 172 | version: e682c1008ac17bf26d2e4b5ad6cdd08520ed0b22 173 | - name: github.com/uber-go/tally 174 | version: ff17f3c43c065c3c2991f571e740eee43ea3a14a 175 | subpackages: 176 | - m3 177 | - m3/customtransports 178 | - m3/thrift 179 | - m3/thriftudp 180 | - multi 181 | - prometheus 182 | - name: github.com/willf/bitset 183 | version: e553b05586428962bf7058d1044519d87ca72d74 184 | - name: go.uber.org/atomic 185 | version: 1ea20fb1cbb1cc08cbd0d913a96dead89aa18289 186 | - name: go.uber.org/multierr 187 | version: 3c4937480c32f4c13a875a1829af76c98ca3d40a 188 | - name: go.uber.org/zap 189 | version: f85c78b1dd998214c5f2138155b320a4a43fbe36 190 | subpackages: 191 | - buffer 192 | - internal/bufferpool 193 | - internal/color 194 | - internal/exit 195 | - zapcore 196 | - name: golang.org/x/net 197 | version: ab5485076ff3407ad2d02db054635913f017b0ed 198 | subpackages: 199 | - context 200 | - http2 201 | - http2/hpack 202 | - idna 203 | - internal/timeseries 204 | - lex/httplex 205 | - trace 206 | - name: golang.org/x/text 207 | version: 4ee4af566555f5fbe026368b75596286a312663a 208 | subpackages: 209 | - secure/bidirule 210 | - transform 211 | - unicode/bidi 212 | - unicode/norm 213 | - name: google.golang.org/genproto 214 | version: 09f6ed296fc66555a25fe4ce95173148778dfa85 215 | subpackages: 216 | - googleapis/api/annotations 217 | - googleapis/rpc/status 218 | - name: google.golang.org/grpc 219 | version: 401e0e00e4bb830a10496d64cd95e068c5bf50de 220 | subpackages: 221 | - balancer 222 | - codes 223 | - connectivity 224 | - credentials 225 | - grpclb/grpc_lb_v1/messages 226 | - grpclog 227 | - health/grpc_health_v1 228 | - internal 229 | - keepalive 230 | - metadata 231 | - naming 232 | - peer 233 | - resolver 234 | - stats 235 | - status 236 | - tap 237 | - transport 238 | - name: gopkg.in/go-playground/validator.v9 239 | version: a021b2ec9a8a8bb970f3f15bc42617cb520e8a64 240 | - name: gopkg.in/validator.v2 241 | version: 3e4f037f12a1221a0864cf0dd2e81c452ab22448 242 | - name: gopkg.in/yaml.v2 243 | version: 5420a8b6744d3b0345ab293f6fcba19c978f1183 244 | repo: https://github.com/go-yaml/yaml.git 245 | testImports: 246 | - name: github.com/davecgh/go-spew 247 | version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d 248 | subpackages: 249 | - spew 250 | - name: github.com/pmezard/go-difflib 251 | version: d8ed2627bdf02c080bf22230dbb337003b7aba2d 252 | subpackages: 253 | - difflib 254 | - name: github.com/stretchr/testify 255 | version: 6fe211e493929a8aac0469b93f28b1d0688a9a3a 256 | subpackages: 257 | - assert 258 | - require 259 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/m3db/m3ctl 2 | 3 | import: 4 | - package: github.com/m3db/m3 5 | version: 16e2dfb2206a8ead03f733a7032b1bc2b83d4b93 6 | - package: github.com/m3db/m3x 7 | version: 943173a151c8a6b2da1f93f814c27c4e4a1e2050 8 | - package: github.com/apache/thrift 9 | version: 9549b25c77587b29be4e0b5c258221a4ed85d37a 10 | - package: github.com/beorn7/perks 11 | version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9 12 | - package: github.com/coreos/etcd 13 | version: 3.2.10 14 | - package: google.golang.org/grpc 15 | version: 1.7.3 16 | - package: gopkg.in/validator.v2 17 | version: 3e4f037f12a1221a0864cf0dd2e81c452ab22448 18 | - package: github.com/facebookgo/clock 19 | version: 600d898af40aa09a7a93ecb9265d87b0504b6f03 20 | - package: github.com/ghodss/yaml 21 | version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee 22 | - package: github.com/go-ole/go-ole 23 | version: de8695c8edbf8236f30d6e1376e20b198a028d42 24 | - package: github.com/golang/mock 25 | version: ^1 26 | - package: github.com/golang/protobuf 27 | version: 5a0f697c9ed9d68fef0116532c6e05cfeae00e55 28 | - package: github.com/grpc-ecosystem/go-grpc-prometheus 29 | version: 6b7015e65d366bf3f19b2b2a000a831940f0f7e0 30 | - package: github.com/grpc-ecosystem/grpc-gateway 31 | version: 84398b94e188ee336f307779b57b3aa91af7063c 32 | - package: github.com/matttproud/golang_protobuf_extensions 33 | version: c12348ce28de40eed0136aa2b644d0ee0650e56c 34 | - package: github.com/prometheus/client_golang 35 | version: c5b7fccd204277076155f10851dad72b76a49317 36 | - package: github.com/prometheus/client_model 37 | version: fa8ad6fec33561be4280a8f0514318c79d7f6cb6 38 | - package: github.com/prometheus/common 39 | version: 195bde7883f7c39ea62b0d92ab7359b5327065cb 40 | - package: github.com/prometheus/procfs 41 | version: 1878d9fbb537119d24b21ca07effd591627cd160 42 | - package: github.com/shirou/gopsutil 43 | version: b62e301a8b9958eebb7299683eb57fab229a9501 44 | - package: github.com/shirou/w32 45 | version: bb4de0191aa41b5507caa14b0650cdbddcd9280b 46 | - package: github.com/StackExchange/wmi 47 | version: e542ed97d15e640bdc14b5c12162d59e8fc67324 48 | - package: github.com/uber-go/atomic 49 | version: e682c1008ac17bf26d2e4b5ad6cdd08520ed0b22 50 | - package: github.com/uber-go/tally 51 | version: <4.0.0 52 | - package: golang.org/x/net 53 | version: ab5485076ff3407ad2d02db054635913f017b0ed 54 | - package: github.com/gorilla/mux 55 | version: 1.6.1 56 | - package: github.com/go-playground/locales 57 | version: 1e5f1161c6416a5ff48840eb8724a394e48cc534 58 | - package: github.com/go-playground/universal-translator 59 | version: 71201497bace774495daed26a3874fd339e0b538 60 | - package: gopkg.in/go-playground/validator.v9 61 | version: a021b2ec9a8a8bb970f3f15bc42617cb520e8a64 62 | - package: github.com/rakyll/statik/fs 63 | version: 19b88da8fc15428620782ba18f68423130e7ac7d 64 | testImport: 65 | - package: github.com/stretchr/testify 66 | version: 6fe211e493929a8aac0469b93f28b1d0688a9a3a 67 | - package: github.com/pmezard/go-difflib 68 | version: d8ed2627bdf02c080bf22230dbb337003b7aba2d 69 | - package: github.com/davecgh/go-spew 70 | version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d 71 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Public 5 | 6 | 7 | 8 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/r2/v1/swagger/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3db/m3ctl/65204284e1d73ef9b6d56163cfcddfd93b627921/public/r2/v1/swagger/favicon-16x16.png -------------------------------------------------------------------------------- /public/r2/v1/swagger/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3db/m3ctl/65204284e1d73ef9b6d56163cfcddfd93b627921/public/r2/v1/swagger/favicon-32x32.png -------------------------------------------------------------------------------- /public/r2/v1/swagger/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | 70 | 71 | 72 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /public/r2/v1/swagger/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 54 | -------------------------------------------------------------------------------- /public/r2/v1/swagger/swagger-ui-bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui-bundle.js","sources":["webpack:///swagger-ui-bundle.js"],"mappings":"AAAA;;;;;AAoyKA;;;;;;AAy+EA;;;;;;;;;;;;;;;;;;;;;;;;;;AAw1TA;;;;;;;;;;;;;;AAs8JA;;;;;;;;;AAw6oBA;;;;;AAirQA;AAm4DA;;;;;;AAo4YA;;;;;;AA8jaA;AAumvBA","sourceRoot":""} -------------------------------------------------------------------------------- /public/r2/v1/swagger/swagger-ui-standalone-preset.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui-standalone-preset.js","sources":["webpack:///swagger-ui-standalone-preset.js"],"mappings":"AAAA;;;;;AA00CA;;;;;;AAmlFA","sourceRoot":""} -------------------------------------------------------------------------------- /public/r2/v1/swagger/swagger-ui.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui.css","sources":[],"mappings":"","sourceRoot":""} -------------------------------------------------------------------------------- /public/r2/v1/swagger/swagger-ui.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui.js","sources":["webpack:///swagger-ui.js"],"mappings":"AAAA;;;;;;AAowcA","sourceRoot":""} -------------------------------------------------------------------------------- /server/http/options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package http 22 | 23 | import ( 24 | "time" 25 | 26 | "github.com/m3db/m3x/instrument" 27 | ) 28 | 29 | const ( 30 | defaultReadTimeout = 10 * time.Second 31 | defaultWriteTimeout = 10 * time.Second 32 | ) 33 | 34 | // Options is a set of server options. 35 | type Options interface { 36 | // SetReadTimeout sets the read timeout. 37 | SetReadTimeout(value time.Duration) Options 38 | 39 | // ReadTimeout returns the read timeout. 40 | ReadTimeout() time.Duration 41 | 42 | // SetWriteTimeout sets the write timeout. 43 | SetWriteTimeout(value time.Duration) Options 44 | 45 | // WriteTimeout returns the write timeout. 46 | WriteTimeout() time.Duration 47 | 48 | // SetInstrumentOptions returns the write timeout. 49 | SetInstrumentOptions(value instrument.Options) Options 50 | 51 | // InstrumentOptions returns the write timeout. 52 | InstrumentOptions() instrument.Options 53 | } 54 | 55 | type options struct { 56 | instrumentOpts instrument.Options 57 | readTimeout time.Duration 58 | writeTimeout time.Duration 59 | } 60 | 61 | // NewOptions creates a new set of server options. 62 | func NewOptions() Options { 63 | return &options{ 64 | readTimeout: defaultReadTimeout, 65 | writeTimeout: defaultWriteTimeout, 66 | instrumentOpts: instrument.NewOptions(), 67 | } 68 | } 69 | 70 | func (o *options) SetInstrumentOptions(value instrument.Options) Options { 71 | opts := *o 72 | opts.instrumentOpts = value 73 | return &opts 74 | } 75 | 76 | func (o *options) InstrumentOptions() instrument.Options { 77 | return o.instrumentOpts 78 | } 79 | 80 | func (o *options) SetReadTimeout(value time.Duration) Options { 81 | opts := *o 82 | opts.readTimeout = value 83 | return &opts 84 | } 85 | 86 | func (o *options) ReadTimeout() time.Duration { 87 | return o.readTimeout 88 | } 89 | 90 | func (o *options) SetWriteTimeout(value time.Duration) Options { 91 | opts := *o 92 | opts.writeTimeout = value 93 | return &opts 94 | } 95 | 96 | func (o *options) WriteTimeout() time.Duration { 97 | return o.writeTimeout 98 | } 99 | -------------------------------------------------------------------------------- /server/http/server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package http 22 | 23 | import ( 24 | "net/http" 25 | "sync" 26 | 27 | _ "github.com/m3db/m3ctl/generated/ui/statik" // Generated UI statik package 28 | mserver "github.com/m3db/m3ctl/server" 29 | "github.com/m3db/m3ctl/service" 30 | "github.com/m3db/m3x/log" 31 | 32 | "github.com/gorilla/mux" 33 | "github.com/rakyll/statik/fs" 34 | ) 35 | 36 | const ( 37 | publicPathPrefix = "/public" 38 | staticPathPrefix = "/static" 39 | indexFile = "/index.html" 40 | ) 41 | 42 | var ( 43 | indexPaths = []string{"/", indexFile} 44 | ) 45 | 46 | type server struct { 47 | server *http.Server 48 | services []service.Service 49 | logger log.Logger 50 | wg sync.WaitGroup 51 | } 52 | 53 | // NewServer creates a new HTTP server. 54 | func NewServer(address string, opts Options, services ...service.Service) (mserver.Server, error) { 55 | // Make a copy of the services passed in so they cannot be mutated externally 56 | // once the server is constructed. 57 | cloned := make([]service.Service, len(services)) 58 | copy(cloned, services) 59 | handler, err := initRouter(cloned) 60 | if err != nil { 61 | return nil, err 62 | } 63 | s := &http.Server{ 64 | Addr: address, 65 | Handler: handler, 66 | ReadTimeout: opts.ReadTimeout(), 67 | WriteTimeout: opts.WriteTimeout(), 68 | } 69 | return &server{ 70 | server: s, 71 | services: cloned, 72 | logger: opts.InstrumentOptions().Logger(), 73 | }, nil 74 | } 75 | 76 | func (s *server) ListenAndServe() error { 77 | s.wg.Add(1) 78 | go func() { 79 | defer s.wg.Done() 80 | if err := s.server.ListenAndServe(); err != nil { 81 | s.logger.Errorf("could not start listening and serving traffic: %v", err) 82 | } 83 | }() 84 | return nil 85 | } 86 | 87 | func (s *server) Close() { 88 | s.server.Close() 89 | s.wg.Wait() 90 | for _, service := range s.services { 91 | service.Close() 92 | } 93 | } 94 | 95 | func initRouter(services []service.Service) (http.Handler, error) { 96 | router := mux.NewRouter() 97 | if err := registerStaticRoutes(router); err != nil { 98 | return nil, err 99 | } 100 | if err := registerServiceRoutes(router, services); err != nil { 101 | return nil, err 102 | } 103 | return router, nil 104 | } 105 | 106 | func registerStaticRoutes(router *mux.Router) error { 107 | // Register static and public handler. 108 | fileServer, err := fs.New() 109 | if err != nil { 110 | return err 111 | } 112 | 113 | fileServerHandler := http.FileServer(fileServer) 114 | router.PathPrefix(publicPathPrefix).Handler(fileServerHandler) 115 | router.PathPrefix(staticPathPrefix).Handler(fileServerHandler) 116 | 117 | // Register index handlers. 118 | indexHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 119 | fileServerHandler.ServeHTTP(w, r) 120 | }) 121 | for _, path := range indexPaths { 122 | router.Path(path).HandlerFunc(indexHandler) 123 | } 124 | 125 | return nil 126 | } 127 | 128 | func registerServiceRoutes(router *mux.Router, services []service.Service) error { 129 | for _, service := range services { 130 | pathPrefix := service.URLPrefix() 131 | subRouter := router.PathPrefix(pathPrefix).Subrouter() 132 | if err := service.RegisterHandlers(subRouter); err != nil { 133 | return err 134 | } 135 | } 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE 20 | 21 | package server 22 | 23 | // Server is a server capable of listening to incoming traffic and closing itself 24 | // when it's shut down. 25 | type Server interface { 26 | // ListenAndServe forever listens to new incoming connections and 27 | // handles data from those connections. 28 | ListenAndServe() error 29 | 30 | // Close closes the server. 31 | Close() 32 | } 33 | -------------------------------------------------------------------------------- /service/health/service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE 20 | 21 | package health 22 | 23 | import ( 24 | "encoding/json" 25 | "fmt" 26 | "net/http" 27 | "os" 28 | "time" 29 | 30 | mservice "github.com/m3db/m3ctl/service" 31 | "github.com/m3db/m3x/instrument" 32 | 33 | "github.com/gorilla/mux" 34 | ) 35 | 36 | const ( 37 | ok healthStatus = "OK" 38 | healthURL = "/health" 39 | unknownName = "unknown" 40 | ) 41 | 42 | type healthStatus string 43 | 44 | type healthCheckResult struct { 45 | Host string `json:"host"` 46 | Timestamp time.Time `json:"timestamp"` 47 | ResponseTime time.Duration `json:"response_time"` 48 | Status healthStatus `json:"status"` 49 | } 50 | 51 | type service struct { 52 | iOpts instrument.Options 53 | } 54 | 55 | // NewService creates a new rules controller. 56 | func NewService(iOpts instrument.Options) mservice.Service { 57 | return &service{iOpts: iOpts} 58 | } 59 | 60 | func (s *service) URLPrefix() string { 61 | return healthURL 62 | } 63 | 64 | func (s *service) RegisterHandlers(router *mux.Router) error { 65 | log := s.iOpts.Logger() 66 | router.HandleFunc("", healthCheck) 67 | log.Infof("Registered health endpoints") 68 | return nil 69 | } 70 | 71 | func (s *service) Close() {} 72 | 73 | func status() healthStatus { 74 | return ok 75 | } 76 | 77 | func hostName() string { 78 | host, err := os.Hostname() 79 | if err != nil { 80 | host = unknownName 81 | } 82 | return host 83 | } 84 | 85 | func healthCheck(w http.ResponseWriter, r *http.Request) { 86 | start := time.Now() 87 | host := hostName() 88 | status := status() 89 | h := healthCheckResult{Host: host, Timestamp: start, Status: status} 90 | h.ResponseTime = time.Since(start) 91 | 92 | body, err := json.Marshal(h) 93 | if err != nil { 94 | w.WriteHeader(http.StatusInternalServerError) 95 | fmt.Fprintf(w, "Could not generate health check result") 96 | return 97 | } 98 | 99 | w.Header().Set("Content-Type", "application/json") 100 | w.Write(body) 101 | } 102 | -------------------------------------------------------------------------------- /service/health/service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE 20 | 21 | package health 22 | 23 | import ( 24 | "encoding/json" 25 | "net/http" 26 | "net/http/httptest" 27 | "os" 28 | "testing" 29 | 30 | "github.com/gorilla/mux" 31 | "github.com/m3db/m3x/instrument" 32 | "github.com/stretchr/testify/require" 33 | ) 34 | 35 | func TestHostName(t *testing.T) { 36 | expectedName, err := os.Hostname() 37 | require.NoError(t, err, "Failed to get system hostname") 38 | actualName := hostName() 39 | 40 | require.Equal(t, expectedName, actualName) 41 | } 42 | 43 | func TestHealthCheck(t *testing.T) { 44 | rr := httptest.NewRecorder() 45 | // Create a request to pass to our handler. We don't have any query parameters for now, so we'll 46 | // pass 'nil' as the third parameter. 47 | req, err := http.NewRequest("GET", "/health", nil) 48 | require.NoError(t, err) 49 | 50 | opts := instrument.NewOptions() 51 | service := NewService(opts) 52 | mux := mux.NewRouter().PathPrefix(service.URLPrefix()).Subrouter() 53 | err = service.RegisterHandlers(mux) 54 | require.NoError(t, err) 55 | 56 | mux.ServeHTTP(rr, req) 57 | 58 | rawResult := make([]byte, rr.Body.Len()) 59 | _, err = rr.Body.Read(rawResult) 60 | require.NoError(t, err, "Encountered error parsing response") 61 | 62 | var actualResult healthCheckResult 63 | json.Unmarshal(rawResult, &actualResult) 64 | 65 | name, _ := os.Hostname() 66 | 67 | require.Equal(t, name, actualResult.Host) 68 | require.Equal(t, ok, actualResult.Status) 69 | } 70 | -------------------------------------------------------------------------------- /service/r2/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package r2 22 | 23 | import "errors" 24 | 25 | // NewInternalError returns a new error that isn't covered by the other error types. 26 | func NewInternalError(msg string) error { return errors.New(msg) } 27 | 28 | // ConflictError represents either a version mismatch writing data or a data conflict issue. 29 | type conflictError string 30 | 31 | // NewConflictError creates a new Conflict Error 32 | func NewConflictError(msg string) error { return conflictError(msg) } 33 | 34 | func (e conflictError) Error() string { return string(e) } 35 | 36 | // VersionError represents a mismatch in the Namespaces or Ruleset version specified in the request 37 | // and the latest one. 38 | type versionError string 39 | 40 | // NewVersionError creates a new Version Error 41 | func NewVersionError(msg string) error { return versionError(msg) } 42 | 43 | func (e versionError) Error() string { return string(e) } 44 | 45 | // BadInputError represents an error due to malformed or invalid metrics. 46 | type badInputError string 47 | 48 | // NewBadInputError creates a new Bad Input Error. 49 | func NewBadInputError(msg string) error { return badInputError(msg) } 50 | 51 | func (e badInputError) Error() string { return string(e) } 52 | 53 | // NotFoundError represents an error due to malformed or invalid metrics. 54 | type notFoundError string 55 | 56 | // NewNotFoundError creates a new not found Error. 57 | func NewNotFoundError(msg string) error { return notFoundError(msg) } 58 | 59 | func (e notFoundError) Error() string { return string(e) } 60 | 61 | // AuthError represents an error due to missing or invalid auth information. 62 | type authError string 63 | 64 | // NewAuthError creates a new not found Error. 65 | func NewAuthError(msg string) error { return authError(msg) } 66 | 67 | func (e authError) Error() string { return string(e) } 68 | -------------------------------------------------------------------------------- /service/r2/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package r2 22 | 23 | import ( 24 | "fmt" 25 | "net/http" 26 | 27 | "github.com/m3db/m3ctl/auth" 28 | "github.com/m3db/m3x/log" 29 | ) 30 | 31 | type r2HandlerFunc func(http.ResponseWriter, *http.Request) error 32 | 33 | type r2Handler struct { 34 | logger log.Logger 35 | auth auth.HTTPAuthService 36 | } 37 | 38 | func (h r2Handler) wrap(authType auth.AuthorizationType, fn r2HandlerFunc) http.Handler { 39 | f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 40 | if err := fn(w, r); err != nil { 41 | h.handleError(w, err) 42 | } 43 | }) 44 | return h.auth.NewAuthHandler(authType, f, writeAPIResponse) 45 | } 46 | 47 | func (h r2Handler) handleError(w http.ResponseWriter, opError error) { 48 | h.logger.Errorf(opError.Error()) 49 | 50 | var err error 51 | switch opError.(type) { 52 | case conflictError: 53 | err = writeAPIResponse(w, http.StatusConflict, opError.Error()) 54 | case badInputError: 55 | err = writeAPIResponse(w, http.StatusBadRequest, opError.Error()) 56 | case versionError: 57 | err = writeAPIResponse(w, http.StatusConflict, opError.Error()) 58 | case notFoundError: 59 | err = writeAPIResponse(w, http.StatusNotFound, opError.Error()) 60 | case authError: 61 | err = writeAPIResponse(w, http.StatusUnauthorized, opError.Error()) 62 | default: 63 | err = writeAPIResponse(w, http.StatusInternalServerError, opError.Error()) 64 | } 65 | 66 | // Getting here means that the error handling failed. Trying to convey what was supposed to happen. 67 | if err != nil { 68 | msg := fmt.Sprintf("Could not generate error response for: %s", opError.Error()) 69 | h.logger.Errorf(msg) 70 | http.Error(w, msg, http.StatusInternalServerError) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /service/r2/io.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package r2 22 | 23 | import ( 24 | "encoding/json" 25 | "fmt" 26 | "io" 27 | "net/http" 28 | "reflect" 29 | "strings" 30 | 31 | "github.com/m3db/m3/src/metrics/rules/view/changes" 32 | 33 | validator "gopkg.in/go-playground/validator.v9" 34 | ) 35 | 36 | // TODO(dgromov): Make this return a list of validation errors 37 | func parseRequest(s interface{}, body io.ReadCloser) error { 38 | if err := json.NewDecoder(body).Decode(s); err != nil { 39 | return NewBadInputError(fmt.Sprintf("Malformed Json: %s", err.Error())) 40 | } 41 | 42 | // Invoking the validation explictely to have control over the format of the error output. 43 | validate := validator.New() 44 | validate.RegisterTagNameFunc(func(fld reflect.StructField) string { 45 | parts := strings.SplitN(fld.Tag.Get("json"), ",", 2) 46 | if len(parts) > 0 { 47 | return parts[0] 48 | } 49 | return fld.Name 50 | }) 51 | 52 | var required []string 53 | if err := validate.Struct(s); err != nil { 54 | for _, e := range err.(validator.ValidationErrors) { 55 | if e.ActualTag() == "required" { 56 | required = append(required, e.Namespace()) 57 | } 58 | } 59 | } 60 | 61 | if len(required) > 0 { 62 | return NewBadInputError(fmt.Sprintf("Required: [%v]", strings.Join(required, ", "))) 63 | } 64 | return nil 65 | } 66 | 67 | func writeAPIResponse(w http.ResponseWriter, code int, msg string) error { 68 | j, err := json.Marshal(apiResponse{Code: code, Message: msg}) 69 | if err != nil { 70 | return err 71 | } 72 | return sendResponse(w, j, code) 73 | } 74 | 75 | func sendResponse(w http.ResponseWriter, data []byte, status int) error { 76 | w.Header().Set("Content-Type", "application/json") 77 | w.WriteHeader(status) 78 | _, err := w.Write(data) 79 | return err 80 | } 81 | 82 | type apiResponse struct { 83 | Code int `json:"code"` 84 | Message string `json:"message"` 85 | } 86 | 87 | type updateRuleSetRequest struct { 88 | RuleSetChanges changes.RuleSetChanges `json:"rulesetChanges"` 89 | RuleSetVersion int `json:"rulesetVersion"` 90 | } 91 | -------------------------------------------------------------------------------- /service/r2/routes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package r2 22 | 23 | import ( 24 | "fmt" 25 | "net/http" 26 | 27 | "github.com/m3db/m3/src/metrics/rules/view" 28 | 29 | "github.com/gorilla/mux" 30 | ) 31 | 32 | func fetchNamespaces(s *service, _ *http.Request) (data interface{}, err error) { 33 | return s.store.FetchNamespaces() 34 | } 35 | 36 | func fetchNamespace(s *service, r *http.Request) (data interface{}, err error) { 37 | return s.store.FetchRuleSetSnapshot(mux.Vars(r)[namespaceIDVar]) 38 | } 39 | 40 | func createNamespace(s *service, r *http.Request) (data interface{}, err error) { 41 | var n view.Namespace 42 | if err := parseRequest(&n, r.Body); err != nil { 43 | return nil, err 44 | } 45 | 46 | uOpts, err := s.newUpdateOptions(r) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return s.store.CreateNamespace(n.ID, uOpts) 52 | } 53 | 54 | func validateRuleSet(s *service, r *http.Request) (data interface{}, err error) { 55 | vars := mux.Vars(r) 56 | var ruleset view.RuleSet 57 | if err := parseRequest(&ruleset, r.Body); err != nil { 58 | return nil, err 59 | } 60 | 61 | if vars[namespaceIDVar] != ruleset.Namespace { 62 | return nil, fmt.Errorf( 63 | "namespaceID param %s and ruleset namespaceID %s do not match", 64 | vars[namespaceIDVar], 65 | ruleset.Namespace, 66 | ) 67 | } 68 | 69 | if err := s.store.ValidateRuleSet(ruleset); err != nil { 70 | return nil, err 71 | } 72 | 73 | return "Ruleset is valid", nil 74 | } 75 | 76 | func updateRuleSet(s *service, r *http.Request) (data interface{}, err error) { 77 | var req updateRuleSetRequest 78 | if err := parseRequest(&req, r.Body); err != nil { 79 | return nil, NewBadInputError(err.Error()) 80 | } 81 | if len(req.RuleSetChanges.MappingRuleChanges) == 0 && 82 | len(req.RuleSetChanges.RollupRuleChanges) == 0 { 83 | return nil, NewBadInputError( 84 | "invalid request: no ruleset changes detected", 85 | ) 86 | } 87 | 88 | uOpts, err := s.newUpdateOptions(r) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | return s.store.UpdateRuleSet(req.RuleSetChanges, req.RuleSetVersion, uOpts) 94 | } 95 | 96 | func deleteNamespace(s *service, r *http.Request) (data interface{}, err error) { 97 | vars := mux.Vars(r) 98 | namespaceID := vars[namespaceIDVar] 99 | 100 | uOpts, err := s.newUpdateOptions(r) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | if err := s.store.DeleteNamespace(namespaceID, uOpts); err != nil { 106 | return nil, err 107 | } 108 | return fmt.Sprintf("Deleted namespace %s", namespaceID), nil 109 | } 110 | 111 | func fetchMappingRule(s *service, r *http.Request) (data interface{}, err error) { 112 | vars := mux.Vars(r) 113 | return s.store.FetchMappingRule(vars[namespaceIDVar], vars[ruleIDVar]) 114 | } 115 | 116 | func createMappingRule(s *service, r *http.Request) (data interface{}, err error) { 117 | vars := mux.Vars(r) 118 | var mr view.MappingRule 119 | if err := parseRequest(&mr, r.Body); err != nil { 120 | return nil, err 121 | } 122 | 123 | uOpts, err := s.newUpdateOptions(r) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | return s.store.CreateMappingRule(vars[namespaceIDVar], mr, uOpts) 129 | } 130 | 131 | func updateMappingRule(s *service, r *http.Request) (data interface{}, err error) { 132 | vars := mux.Vars(r) 133 | 134 | var mrj view.MappingRule 135 | if err := parseRequest(&mrj, r.Body); err != nil { 136 | return nil, err 137 | } 138 | 139 | uOpts, err := s.newUpdateOptions(r) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | return s.store.UpdateMappingRule(vars[namespaceIDVar], vars[ruleIDVar], mrj, uOpts) 145 | } 146 | 147 | func deleteMappingRule(s *service, r *http.Request) (data interface{}, err error) { 148 | vars := mux.Vars(r) 149 | namespaceID := vars[namespaceIDVar] 150 | mappingRuleID := vars[ruleIDVar] 151 | 152 | uOpts, err := s.newUpdateOptions(r) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | if err := s.store.DeleteMappingRule(namespaceID, mappingRuleID, uOpts); err != nil { 158 | return nil, err 159 | } 160 | 161 | return fmt.Sprintf("Deleted mapping rule: %s in namespace %s", mappingRuleID, namespaceID), nil 162 | } 163 | 164 | func fetchMappingRuleHistory(s *service, r *http.Request) (data interface{}, err error) { 165 | vars := mux.Vars(r) 166 | snapshots, err := s.store.FetchMappingRuleHistory(vars[namespaceIDVar], vars[ruleIDVar]) 167 | if err != nil { 168 | return nil, err 169 | } 170 | return view.MappingRuleSnapshots{MappingRules: snapshots}, nil 171 | } 172 | 173 | func fetchRollupRule(s *service, r *http.Request) (data interface{}, err error) { 174 | vars := mux.Vars(r) 175 | return s.store.FetchRollupRule(vars[namespaceIDVar], vars[ruleIDVar]) 176 | } 177 | 178 | func createRollupRule(s *service, r *http.Request) (data interface{}, err error) { 179 | vars := mux.Vars(r) 180 | namespaceID := vars[namespaceIDVar] 181 | 182 | var rrj view.RollupRule 183 | if err := parseRequest(&rrj, r.Body); err != nil { 184 | return nil, err 185 | } 186 | 187 | uOpts, err := s.newUpdateOptions(r) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | return s.store.CreateRollupRule(namespaceID, rrj, uOpts) 193 | } 194 | 195 | func updateRollupRule(s *service, r *http.Request) (data interface{}, err error) { 196 | vars := mux.Vars(r) 197 | var rrj view.RollupRule 198 | if err := parseRequest(&rrj, r.Body); err != nil { 199 | return nil, err 200 | } 201 | 202 | uOpts, err := s.newUpdateOptions(r) 203 | if err != nil { 204 | return nil, err 205 | } 206 | 207 | return s.store.UpdateRollupRule(vars[namespaceIDVar], vars[ruleIDVar], rrj, uOpts) 208 | } 209 | 210 | func deleteRollupRule(s *service, r *http.Request) (data interface{}, err error) { 211 | vars := mux.Vars(r) 212 | namespaceID := vars[namespaceIDVar] 213 | rollupRuleID := vars[ruleIDVar] 214 | 215 | uOpts, err := s.newUpdateOptions(r) 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | if err := s.store.DeleteRollupRule(namespaceID, rollupRuleID, uOpts); err != nil { 221 | return nil, err 222 | } 223 | 224 | return fmt.Sprintf("Deleted rollup rule: %s in namespace %s", rollupRuleID, namespaceID), nil 225 | } 226 | 227 | func fetchRollupRuleHistory(s *service, r *http.Request) (data interface{}, err error) { 228 | vars := mux.Vars(r) 229 | snapshots, err := s.store.FetchRollupRuleHistory(vars[namespaceIDVar], vars[ruleIDVar]) 230 | if err != nil { 231 | return nil, err 232 | } 233 | return view.RollupRuleSnapshots{RollupRules: snapshots}, nil 234 | } 235 | -------------------------------------------------------------------------------- /service/r2/service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | package r2 21 | 22 | import ( 23 | "net/http" 24 | "testing" 25 | 26 | "github.com/m3db/m3ctl/auth" 27 | 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | func TestDefaultAuthorizationTypeForHTTPMethodGet(t *testing.T) { 32 | actual, err := defaultAuthorizationTypeForHTTPMethod(http.MethodGet) 33 | require.NoError(t, err) 34 | require.EqualValues(t, auth.ReadOnlyAuthorization, actual) 35 | } 36 | func TestDefaultAuthorizationTypeForHTTPMethodPost(t *testing.T) { 37 | actual, err := defaultAuthorizationTypeForHTTPMethod(http.MethodPost) 38 | require.NoError(t, err) 39 | require.EqualValues(t, auth.ReadWriteAuthorization, actual) 40 | } 41 | 42 | func TestDefaultAuthorizationTypeForHTTPMethodPut(t *testing.T) { 43 | actual, err := defaultAuthorizationTypeForHTTPMethod(http.MethodPut) 44 | require.NoError(t, err) 45 | require.EqualValues(t, auth.ReadWriteAuthorization, actual) 46 | } 47 | 48 | func TestDefaultAuthorizationTypeForHTTPMethodPatch(t *testing.T) { 49 | actual, err := defaultAuthorizationTypeForHTTPMethod(http.MethodPatch) 50 | require.NoError(t, err) 51 | require.EqualValues(t, auth.ReadWriteAuthorization, actual) 52 | } 53 | 54 | func TestDefaultAuthorizationTypeForHTTPMethodDelete(t *testing.T) { 55 | actual, err := defaultAuthorizationTypeForHTTPMethod(http.MethodDelete) 56 | require.NoError(t, err) 57 | require.EqualValues(t, auth.ReadWriteAuthorization, actual) 58 | 59 | } 60 | 61 | func TestDefaultAuthorizationTypeForHTTPMethodUnrecognizedMethod(t *testing.T) { 62 | actual, err := defaultAuthorizationTypeForHTTPMethod(http.MethodOptions) 63 | require.Error(t, err) 64 | require.EqualValues(t, auth.UnknownAuthorization, actual) 65 | } 66 | -------------------------------------------------------------------------------- /service/r2/store/kv/options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package kv 22 | 23 | import ( 24 | "time" 25 | 26 | "github.com/m3db/m3/src/metrics/rules" 27 | "github.com/m3db/m3x/clock" 28 | "github.com/m3db/m3x/instrument" 29 | ) 30 | 31 | const ( 32 | defaultRuleUpdatePropagationDelay = time.Minute 33 | ) 34 | 35 | // StoreOptions is a set of options for a kv backed store. 36 | type StoreOptions interface { 37 | // SetClockOptions sets the clock options. 38 | SetClockOptions(value clock.Options) StoreOptions 39 | 40 | // ClockOptions returns the clock options 41 | ClockOptions() clock.Options 42 | 43 | // SetInstrumentOptions sets the instrument options. 44 | SetInstrumentOptions(value instrument.Options) StoreOptions 45 | 46 | // InstrumentOptions returns the instrument options. 47 | InstrumentOptions() instrument.Options 48 | 49 | // SetRuleUpdatePropagationDelay sets the propagation delay for rule updates. 50 | SetRuleUpdatePropagationDelay(value time.Duration) StoreOptions 51 | 52 | // RuleUpdatePropagationDelay returns the propagation delay for rule updates. 53 | RuleUpdatePropagationDelay() time.Duration 54 | 55 | // SetStoreValidator sets the validator for the store. 56 | SetValidator(value rules.Validator) StoreOptions 57 | 58 | // ValidatprOptions returns the validator for the store. 59 | Validator() rules.Validator 60 | } 61 | 62 | type storeOptions struct { 63 | clockOpts clock.Options 64 | instrumentOpts instrument.Options 65 | ruleUpdatePropagationDelay time.Duration 66 | validator rules.Validator 67 | } 68 | 69 | // NewStoreOptions creates a new set of store options. 70 | func NewStoreOptions() StoreOptions { 71 | return &storeOptions{ 72 | clockOpts: clock.NewOptions(), 73 | instrumentOpts: instrument.NewOptions(), 74 | ruleUpdatePropagationDelay: defaultRuleUpdatePropagationDelay, 75 | } 76 | } 77 | 78 | func (o *storeOptions) SetClockOptions(value clock.Options) StoreOptions { 79 | opts := *o 80 | opts.clockOpts = value 81 | return &opts 82 | } 83 | 84 | func (o *storeOptions) ClockOptions() clock.Options { 85 | return o.clockOpts 86 | } 87 | 88 | func (o *storeOptions) SetInstrumentOptions(value instrument.Options) StoreOptions { 89 | opts := *o 90 | opts.instrumentOpts = value 91 | return &opts 92 | } 93 | 94 | func (o *storeOptions) InstrumentOptions() instrument.Options { 95 | return o.instrumentOpts 96 | } 97 | 98 | func (o *storeOptions) SetRuleUpdatePropagationDelay(value time.Duration) StoreOptions { 99 | opts := *o 100 | opts.ruleUpdatePropagationDelay = value 101 | return &opts 102 | } 103 | 104 | func (o *storeOptions) RuleUpdatePropagationDelay() time.Duration { 105 | return o.ruleUpdatePropagationDelay 106 | } 107 | 108 | func (o *storeOptions) SetValidator(value rules.Validator) StoreOptions { 109 | opts := *o 110 | opts.validator = value 111 | return &opts 112 | } 113 | 114 | func (o *storeOptions) Validator() rules.Validator { 115 | return o.validator 116 | } 117 | -------------------------------------------------------------------------------- /service/r2/store/kv/store_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE 20 | 21 | package kv 22 | 23 | import ( 24 | "testing" 25 | "time" 26 | 27 | "github.com/m3db/m3/src/metrics/aggregation" 28 | merrors "github.com/m3db/m3/src/metrics/errors" 29 | "github.com/m3db/m3/src/metrics/pipeline" 30 | "github.com/m3db/m3/src/metrics/policy" 31 | "github.com/m3db/m3/src/metrics/rules" 32 | "github.com/m3db/m3/src/metrics/rules/view" 33 | "github.com/m3db/m3/src/metrics/rules/view/changes" 34 | "github.com/m3db/m3ctl/service/r2" 35 | r2store "github.com/m3db/m3ctl/service/r2/store" 36 | "github.com/m3db/m3x/clock" 37 | 38 | "github.com/golang/mock/gomock" 39 | "github.com/stretchr/testify/require" 40 | ) 41 | 42 | func TestUpdateRuleSet(t *testing.T) { 43 | helper := rules.NewRuleSetUpdateHelper(time.Minute) 44 | initialRuleSet, err := testRuleSet(1, helper.NewUpdateMetadata(100, "validUser")) 45 | require.NoError(t, err) 46 | 47 | mrs, err := initialRuleSet.MappingRules() 48 | require.NoError(t, err) 49 | rrs, err := initialRuleSet.RollupRules() 50 | require.NoError(t, err) 51 | rsChanges := newTestRuleSetChanges(mrs, rrs) 52 | require.NoError(t, err) 53 | 54 | proto, err := initialRuleSet.ToMutableRuleSet().Proto() 55 | require.NoError(t, err) 56 | expected, err := rules.NewRuleSetFromProto(1, proto, rules.NewOptions()) 57 | require.NoError(t, err) 58 | expectedMutable := expected.ToMutableRuleSet() 59 | err = expectedMutable.ApplyRuleSetChanges(rsChanges, helper.NewUpdateMetadata(200, "validUser")) 60 | require.NoError(t, err) 61 | 62 | ctrl := gomock.NewController(t) 63 | defer ctrl.Finish() 64 | mockedStore := rules.NewMockStore(ctrl) 65 | mockedStore.EXPECT().ReadRuleSet("testNamespace").Return( 66 | initialRuleSet, 67 | nil, 68 | ).Times(2) 69 | 70 | mockedStore.EXPECT().WriteRuleSet(gomock.Any()).Do(func(rs rules.MutableRuleSet) { 71 | // mock library can not match rules.MutableRuleSet interface so use this function 72 | expectedProto, err := expectedMutable.Proto() 73 | require.NoError(t, err) 74 | rsProto, err := rs.Proto() 75 | require.NoError(t, err) 76 | require.Equal(t, expectedProto, rsProto) 77 | }).Return(nil) 78 | 79 | storeOpts := NewStoreOptions().SetClockOptions( 80 | clock.NewOptions().SetNowFn(func() time.Time { 81 | return time.Unix(0, 200) 82 | }), 83 | ) 84 | rulesStore := NewStore(mockedStore, storeOpts) 85 | uOpts := r2store.NewUpdateOptions().SetAuthor("validUser") 86 | _, err = rulesStore.UpdateRuleSet(rsChanges, 1, uOpts) 87 | require.NoError(t, err) 88 | } 89 | 90 | func TestUpdateRuleSetVersionMisMatch(t *testing.T) { 91 | helper := rules.NewRuleSetUpdateHelper(time.Minute) 92 | initialRuleSet, err := newEmptyTestRuleSet(2, helper.NewUpdateMetadata(100, "validUser")) 93 | require.NoError(t, err) 94 | 95 | rsChanges := newTestRuleSetChanges( 96 | view.MappingRules{}, 97 | view.RollupRules{}, 98 | ) 99 | 100 | ctrl := gomock.NewController(t) 101 | defer ctrl.Finish() 102 | mockedStore := rules.NewMockStore(ctrl) 103 | mockedStore.EXPECT().ReadRuleSet("testNamespace").Return( 104 | initialRuleSet, 105 | nil, 106 | ) 107 | 108 | storeOpts := NewStoreOptions().SetClockOptions( 109 | clock.NewOptions().SetNowFn(func() time.Time { 110 | return time.Unix(0, 200) 111 | }), 112 | ) 113 | rulesStore := NewStore(mockedStore, storeOpts) 114 | uOpts := r2store.NewUpdateOptions().SetAuthor("validUser") 115 | _, err = rulesStore.UpdateRuleSet(rsChanges, 1, uOpts) 116 | require.Error(t, err) 117 | require.IsType(t, r2.NewConflictError(""), err) 118 | } 119 | 120 | func TestUpdateRuleSetFetchNotFound(t *testing.T) { 121 | rsChanges := newTestRuleSetChanges( 122 | view.MappingRules{}, 123 | view.RollupRules{}, 124 | ) 125 | 126 | ctrl := gomock.NewController(t) 127 | defer ctrl.Finish() 128 | mockedStore := rules.NewMockStore(ctrl) 129 | mockedStore.EXPECT().ReadRuleSet("testNamespace").Return( 130 | nil, 131 | merrors.NewNotFoundError("something bad has happened"), 132 | ) 133 | 134 | storeOpts := NewStoreOptions().SetClockOptions( 135 | clock.NewOptions().SetNowFn(func() time.Time { 136 | return time.Unix(0, 200) 137 | }), 138 | ) 139 | rulesStore := NewStore(mockedStore, storeOpts) 140 | uOpts := r2store.NewUpdateOptions().SetAuthor("validUser") 141 | _, err := rulesStore.UpdateRuleSet(rsChanges, 1, uOpts) 142 | require.Error(t, err) 143 | require.IsType(t, r2.NewNotFoundError(""), err) 144 | } 145 | 146 | func TestUpdateRuleSetFetchFailure(t *testing.T) { 147 | rsChanges := newTestRuleSetChanges( 148 | view.MappingRules{}, 149 | view.RollupRules{}, 150 | ) 151 | 152 | ctrl := gomock.NewController(t) 153 | defer ctrl.Finish() 154 | mockedStore := rules.NewMockStore(ctrl) 155 | mockedStore.EXPECT().ReadRuleSet("testNamespace").Return( 156 | nil, 157 | merrors.NewValidationError("something bad has happened"), 158 | ) 159 | 160 | storeOpts := NewStoreOptions().SetClockOptions( 161 | clock.NewOptions().SetNowFn(func() time.Time { 162 | return time.Unix(0, 200) 163 | }), 164 | ) 165 | rulesStore := NewStore(mockedStore, storeOpts) 166 | uOpts := r2store.NewUpdateOptions().SetAuthor("validUser") 167 | _, err := rulesStore.UpdateRuleSet(rsChanges, 1, uOpts) 168 | require.Error(t, err) 169 | require.IsType(t, r2.NewBadInputError(""), err) 170 | } 171 | 172 | func TestUpdateRuleSetMutationFail(t *testing.T) { 173 | helper := rules.NewRuleSetUpdateHelper(time.Minute) 174 | initialRuleSet, err := newEmptyTestRuleSet(1, helper.NewUpdateMetadata(100, "validUser")) 175 | 176 | rsChanges := newTestRuleSetChanges( 177 | view.MappingRules{ 178 | "invalidMappingRule": []view.MappingRule{}, 179 | }, 180 | view.RollupRules{}, 181 | ) 182 | require.NoError(t, err) 183 | 184 | ctrl := gomock.NewController(t) 185 | defer ctrl.Finish() 186 | mockedStore := rules.NewMockStore(ctrl) 187 | mockedStore.EXPECT().ReadRuleSet("testNamespace").Return( 188 | initialRuleSet, 189 | nil, 190 | ) 191 | 192 | storeOpts := NewStoreOptions().SetClockOptions( 193 | clock.NewOptions().SetNowFn(func() time.Time { 194 | return time.Unix(0, 200) 195 | }), 196 | ) 197 | rulesStore := NewStore(mockedStore, storeOpts) 198 | uOpts := r2store.NewUpdateOptions().SetAuthor("validUser") 199 | _, err = rulesStore.UpdateRuleSet(rsChanges, 1, uOpts) 200 | require.Error(t, err) 201 | require.IsType(t, r2.NewConflictError(""), err) 202 | } 203 | 204 | func TestUpdateRuleSetWriteFailure(t *testing.T) { 205 | helper := rules.NewRuleSetUpdateHelper(time.Minute) 206 | initialRuleSet, err := testRuleSet(1, helper.NewUpdateMetadata(100, "validUser")) 207 | require.NoError(t, err) 208 | 209 | mrs, err := initialRuleSet.MappingRules() 210 | require.NoError(t, err) 211 | rrs, err := initialRuleSet.RollupRules() 212 | require.NoError(t, err) 213 | rsChanges := newTestRuleSetChanges(mrs, rrs) 214 | require.NoError(t, err) 215 | 216 | proto, err := initialRuleSet.ToMutableRuleSet().Proto() 217 | require.NoError(t, err) 218 | expected, err := rules.NewRuleSetFromProto(1, proto, rules.NewOptions()) 219 | require.NoError(t, err) 220 | expectedMutable := expected.ToMutableRuleSet() 221 | err = expectedMutable.ApplyRuleSetChanges(rsChanges, helper.NewUpdateMetadata(200, "validUser")) 222 | require.NoError(t, err) 223 | 224 | ctrl := gomock.NewController(t) 225 | defer ctrl.Finish() 226 | mockedStore := rules.NewMockStore(ctrl) 227 | mockedStore.EXPECT().ReadRuleSet("testNamespace").Return( 228 | initialRuleSet, 229 | nil, 230 | ) 231 | 232 | mockedStore.EXPECT().WriteRuleSet(gomock.Any()).Do(func(rs rules.MutableRuleSet) { 233 | // mock library can not match rules.MutableRuleSet interface so use this function 234 | expectedProto, err := expectedMutable.Proto() 235 | require.NoError(t, err) 236 | rsProto, err := rs.Proto() 237 | require.NoError(t, err) 238 | require.Equal(t, expectedProto, rsProto) 239 | }).Return(merrors.NewStaleDataError("something has gone wrong")) 240 | 241 | storeOpts := NewStoreOptions().SetClockOptions( 242 | clock.NewOptions().SetNowFn(func() time.Time { 243 | return time.Unix(0, 200) 244 | }), 245 | ) 246 | rulesStore := NewStore(mockedStore, storeOpts) 247 | uOpts := r2store.NewUpdateOptions().SetAuthor("validUser") 248 | _, err = rulesStore.UpdateRuleSet(rsChanges, 1, uOpts) 249 | require.Error(t, err) 250 | require.IsType(t, r2.NewConflictError(""), err) 251 | } 252 | 253 | func newTestRuleSetChanges(mrs view.MappingRules, rrs view.RollupRules) changes.RuleSetChanges { 254 | mrChanges := make([]changes.MappingRuleChange, 0, len(mrs)) 255 | for uuid := range mrs { 256 | mrChanges = append( 257 | mrChanges, 258 | changes.MappingRuleChange{ 259 | Op: changes.ChangeOp, 260 | RuleID: &uuid, 261 | RuleData: &view.MappingRule{ 262 | ID: uuid, 263 | Name: "updateMappingRule", 264 | }, 265 | }, 266 | ) 267 | } 268 | 269 | rrChanges := make([]changes.RollupRuleChange, 0, len(rrs)) 270 | for uuid := range rrs { 271 | rrChanges = append( 272 | rrChanges, 273 | changes.RollupRuleChange{ 274 | Op: changes.ChangeOp, 275 | RuleID: &uuid, 276 | RuleData: &view.RollupRule{ 277 | ID: uuid, 278 | Name: "updateRollupRule", 279 | }, 280 | }, 281 | ) 282 | } 283 | 284 | return changes.RuleSetChanges{ 285 | Namespace: "testNamespace", 286 | RollupRuleChanges: rrChanges, 287 | MappingRuleChanges: mrChanges, 288 | } 289 | } 290 | 291 | // nolint: unparam 292 | func testRuleSet(version int, meta rules.UpdateMetadata) (rules.RuleSet, error) { 293 | mutable := rules.NewEmptyRuleSet("testNamespace", meta) 294 | err := mutable.ApplyRuleSetChanges( 295 | changes.RuleSetChanges{ 296 | Namespace: "testNamespace", 297 | RollupRuleChanges: []changes.RollupRuleChange{ 298 | changes.RollupRuleChange{ 299 | Op: changes.AddOp, 300 | RuleData: &view.RollupRule{ 301 | Name: "rollupRule3", 302 | Targets: []view.RollupTarget{ 303 | { 304 | Pipeline: pipeline.NewPipeline([]pipeline.OpUnion{ 305 | { 306 | Type: pipeline.RollupOpType, 307 | Rollup: pipeline.RollupOp{ 308 | NewName: []byte("testTarget"), 309 | Tags: [][]byte{[]byte("tag1"), []byte("tag2")}, 310 | AggregationID: aggregation.MustCompressTypes(aggregation.Min), 311 | }, 312 | }, 313 | }), 314 | StoragePolicies: policy.StoragePolicies{ 315 | policy.MustParseStoragePolicy("1m:10d"), 316 | }, 317 | }, 318 | }, 319 | }, 320 | }, 321 | }, 322 | MappingRuleChanges: []changes.MappingRuleChange{ 323 | changes.MappingRuleChange{ 324 | Op: changes.AddOp, 325 | RuleData: &view.MappingRule{ 326 | Name: "mappingRule3", 327 | StoragePolicies: policy.StoragePolicies{ 328 | policy.MustParseStoragePolicy("1s:6h"), 329 | }, 330 | }, 331 | }, 332 | }, 333 | }, 334 | meta, 335 | ) 336 | if err != nil { 337 | return nil, err 338 | } 339 | proto, err := mutable.Proto() 340 | if err != nil { 341 | return nil, err 342 | } 343 | ruleSet, err := rules.NewRuleSetFromProto(version, proto, rules.NewOptions()) 344 | if err != nil { 345 | return nil, err 346 | } 347 | 348 | return ruleSet, nil 349 | } 350 | 351 | func newEmptyTestRuleSet(version int, meta rules.UpdateMetadata) (rules.RuleSet, error) { 352 | proto, err := rules.NewEmptyRuleSet("testNamespace", meta).Proto() 353 | if err != nil { 354 | return nil, err 355 | } 356 | ruleSet, err := rules.NewRuleSetFromProto(version, proto, rules.NewOptions()) 357 | if err != nil { 358 | return nil, err 359 | } 360 | 361 | return ruleSet, nil 362 | } 363 | -------------------------------------------------------------------------------- /service/r2/store/store.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE 20 | 21 | package store 22 | 23 | import ( 24 | "github.com/m3db/m3/src/metrics/rules/view" 25 | "github.com/m3db/m3/src/metrics/rules/view/changes" 26 | ) 27 | 28 | // Store is a construct that can perform operations against a backing rule store. 29 | type Store interface { 30 | // FetchNamespaces fetches namespaces. 31 | FetchNamespaces() (view.Namespaces, error) 32 | 33 | // CreateNamespace creates a namespace for the given namespace ID. 34 | CreateNamespace(namespaceID string, uOpts UpdateOptions) (view.Namespace, error) 35 | 36 | // DeleteNamespace deletes the namespace for the given namespace ID. 37 | DeleteNamespace(namespaceID string, uOpts UpdateOptions) error 38 | 39 | // FetchRuleSetSnapshot fetches the latest ruleset snapshot for the given namespace ID. 40 | FetchRuleSetSnapshot(namespaceID string) (view.RuleSet, error) 41 | 42 | // ValidateRuleSet validates a namespace's ruleset. 43 | ValidateRuleSet(rs view.RuleSet) error 44 | 45 | // UpdateRuleSet updates a ruleset with a given namespace. 46 | UpdateRuleSet(rsChanges changes.RuleSetChanges, version int, uOpts UpdateOptions) (view.RuleSet, error) 47 | 48 | // FetchMappingRule fetches the mapping rule for the given namespace ID and rule ID. 49 | FetchMappingRule(namespaceID, mappingRuleID string) (view.MappingRule, error) 50 | 51 | // CreateMappingRule creates a mapping rule for the given namespace ID and rule data. 52 | CreateMappingRule(namespaceID string, mrv view.MappingRule, uOpts UpdateOptions) (view.MappingRule, error) 53 | 54 | // UpdateMappingRule updates a mapping rule for the given namespace ID and rule data. 55 | UpdateMappingRule(namespaceID, mappingRuleID string, mrv view.MappingRule, uOpts UpdateOptions) (view.MappingRule, error) 56 | 57 | // DeleteMappingRule deletes the mapping rule for the given namespace ID and rule ID. 58 | DeleteMappingRule(namespaceID, mappingRuleID string, uOpts UpdateOptions) error 59 | 60 | // FetchMappingRuleHistory fetches the history of the mapping rule for the given namespace ID 61 | // and rule ID. 62 | FetchMappingRuleHistory(namespaceID, mappingRuleID string) ([]view.MappingRule, error) 63 | 64 | // FetchRollupRule fetches the rollup rule for the given namespace ID and rule ID. 65 | FetchRollupRule(namespaceID, rollupRuleID string) (view.RollupRule, error) 66 | 67 | // CreateRollupRule creates a rollup rule for the given namespace ID and rule data. 68 | CreateRollupRule(namespaceID string, rrv view.RollupRule, uOpts UpdateOptions) (view.RollupRule, error) 69 | 70 | // UpdateRollupRule updates a rollup rule for the given namespace ID and rule data. 71 | UpdateRollupRule(namespaceID, rollupRuleID string, rrv view.RollupRule, uOpts UpdateOptions) (view.RollupRule, error) 72 | 73 | // DeleteRollupRule deletes the rollup rule for the given namespace ID and rule ID. 74 | DeleteRollupRule(namespaceID, rollupRuleID string, uOpts UpdateOptions) error 75 | 76 | // FetchRollupRuleHistory fetches the history of the rollup rule for the given namespace ID 77 | // and rule ID. 78 | FetchRollupRuleHistory(namespaceID, rollupRuleID string) ([]view.RollupRule, error) 79 | 80 | // Close closes the store. 81 | Close() 82 | } 83 | -------------------------------------------------------------------------------- /service/r2/store/store_mock.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // Automatically generated by MockGen. DO NOT EDIT! 22 | // Source: github.com/m3db/m3ctl/service/r2/store (interfaces: Store) 23 | 24 | package store 25 | 26 | import ( 27 | "github.com/m3db/m3/src/metrics/rules/view" 28 | "github.com/m3db/m3/src/metrics/rules/view/changes" 29 | 30 | "github.com/golang/mock/gomock" 31 | ) 32 | 33 | // Mock of Store interface 34 | type MockStore struct { 35 | ctrl *gomock.Controller 36 | recorder *_MockStoreRecorder 37 | } 38 | 39 | // Recorder for MockStore (not exported) 40 | type _MockStoreRecorder struct { 41 | mock *MockStore 42 | } 43 | 44 | func NewMockStore(ctrl *gomock.Controller) *MockStore { 45 | mock := &MockStore{ctrl: ctrl} 46 | mock.recorder = &_MockStoreRecorder{mock} 47 | return mock 48 | } 49 | 50 | func (_m *MockStore) EXPECT() *_MockStoreRecorder { 51 | return _m.recorder 52 | } 53 | 54 | func (_m *MockStore) Close() { 55 | _m.ctrl.Call(_m, "Close") 56 | } 57 | 58 | func (_mr *_MockStoreRecorder) Close() *gomock.Call { 59 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Close") 60 | } 61 | 62 | func (_m *MockStore) CreateMappingRule(_param0 string, _param1 view.MappingRule, _param2 UpdateOptions) (view.MappingRule, error) { 63 | ret := _m.ctrl.Call(_m, "CreateMappingRule", _param0, _param1, _param2) 64 | ret0, _ := ret[0].(view.MappingRule) 65 | ret1, _ := ret[1].(error) 66 | return ret0, ret1 67 | } 68 | 69 | func (_mr *_MockStoreRecorder) CreateMappingRule(arg0, arg1, arg2 interface{}) *gomock.Call { 70 | return _mr.mock.ctrl.RecordCall(_mr.mock, "CreateMappingRule", arg0, arg1, arg2) 71 | } 72 | 73 | func (_m *MockStore) CreateNamespace(_param0 string, _param1 UpdateOptions) (view.Namespace, error) { 74 | ret := _m.ctrl.Call(_m, "CreateNamespace", _param0, _param1) 75 | ret0, _ := ret[0].(view.Namespace) 76 | ret1, _ := ret[1].(error) 77 | return ret0, ret1 78 | } 79 | 80 | func (_mr *_MockStoreRecorder) CreateNamespace(arg0, arg1 interface{}) *gomock.Call { 81 | return _mr.mock.ctrl.RecordCall(_mr.mock, "CreateNamespace", arg0, arg1) 82 | } 83 | 84 | func (_m *MockStore) CreateRollupRule(_param0 string, _param1 view.RollupRule, _param2 UpdateOptions) (view.RollupRule, error) { 85 | ret := _m.ctrl.Call(_m, "CreateRollupRule", _param0, _param1, _param2) 86 | ret0, _ := ret[0].(view.RollupRule) 87 | ret1, _ := ret[1].(error) 88 | return ret0, ret1 89 | } 90 | 91 | func (_mr *_MockStoreRecorder) CreateRollupRule(arg0, arg1, arg2 interface{}) *gomock.Call { 92 | return _mr.mock.ctrl.RecordCall(_mr.mock, "CreateRollupRule", arg0, arg1, arg2) 93 | } 94 | 95 | func (_m *MockStore) DeleteMappingRule(_param0 string, _param1 string, _param2 UpdateOptions) error { 96 | ret := _m.ctrl.Call(_m, "DeleteMappingRule", _param0, _param1, _param2) 97 | ret0, _ := ret[0].(error) 98 | return ret0 99 | } 100 | 101 | func (_mr *_MockStoreRecorder) DeleteMappingRule(arg0, arg1, arg2 interface{}) *gomock.Call { 102 | return _mr.mock.ctrl.RecordCall(_mr.mock, "DeleteMappingRule", arg0, arg1, arg2) 103 | } 104 | 105 | func (_m *MockStore) DeleteNamespace(_param0 string, _param1 UpdateOptions) error { 106 | ret := _m.ctrl.Call(_m, "DeleteNamespace", _param0, _param1) 107 | ret0, _ := ret[0].(error) 108 | return ret0 109 | } 110 | 111 | func (_mr *_MockStoreRecorder) DeleteNamespace(arg0, arg1 interface{}) *gomock.Call { 112 | return _mr.mock.ctrl.RecordCall(_mr.mock, "DeleteNamespace", arg0, arg1) 113 | } 114 | 115 | func (_m *MockStore) DeleteRollupRule(_param0 string, _param1 string, _param2 UpdateOptions) error { 116 | ret := _m.ctrl.Call(_m, "DeleteRollupRule", _param0, _param1, _param2) 117 | ret0, _ := ret[0].(error) 118 | return ret0 119 | } 120 | 121 | func (_mr *_MockStoreRecorder) DeleteRollupRule(arg0, arg1, arg2 interface{}) *gomock.Call { 122 | return _mr.mock.ctrl.RecordCall(_mr.mock, "DeleteRollupRule", arg0, arg1, arg2) 123 | } 124 | 125 | func (_m *MockStore) FetchMappingRule(_param0 string, _param1 string) (view.MappingRule, error) { 126 | ret := _m.ctrl.Call(_m, "FetchMappingRule", _param0, _param1) 127 | ret0, _ := ret[0].(view.MappingRule) 128 | ret1, _ := ret[1].(error) 129 | return ret0, ret1 130 | } 131 | 132 | func (_mr *_MockStoreRecorder) FetchMappingRule(arg0, arg1 interface{}) *gomock.Call { 133 | return _mr.mock.ctrl.RecordCall(_mr.mock, "FetchMappingRule", arg0, arg1) 134 | } 135 | 136 | func (_m *MockStore) FetchMappingRuleHistory(_param0 string, _param1 string) ([]view.MappingRule, error) { 137 | ret := _m.ctrl.Call(_m, "FetchMappingRuleHistory", _param0, _param1) 138 | ret0, _ := ret[0].([]view.MappingRule) 139 | ret1, _ := ret[1].(error) 140 | return ret0, ret1 141 | } 142 | 143 | func (_mr *_MockStoreRecorder) FetchMappingRuleHistory(arg0, arg1 interface{}) *gomock.Call { 144 | return _mr.mock.ctrl.RecordCall(_mr.mock, "FetchMappingRuleHistory", arg0, arg1) 145 | } 146 | 147 | func (_m *MockStore) FetchNamespaces() (view.Namespaces, error) { 148 | ret := _m.ctrl.Call(_m, "FetchNamespaces") 149 | ret0, _ := ret[0].(view.Namespaces) 150 | ret1, _ := ret[1].(error) 151 | return ret0, ret1 152 | } 153 | 154 | func (_mr *_MockStoreRecorder) FetchNamespaces() *gomock.Call { 155 | return _mr.mock.ctrl.RecordCall(_mr.mock, "FetchNamespaces") 156 | } 157 | 158 | func (_m *MockStore) FetchRollupRule(_param0 string, _param1 string) (view.RollupRule, error) { 159 | ret := _m.ctrl.Call(_m, "FetchRollupRule", _param0, _param1) 160 | ret0, _ := ret[0].(view.RollupRule) 161 | ret1, _ := ret[1].(error) 162 | return ret0, ret1 163 | } 164 | 165 | func (_mr *_MockStoreRecorder) FetchRollupRule(arg0, arg1 interface{}) *gomock.Call { 166 | return _mr.mock.ctrl.RecordCall(_mr.mock, "FetchRollupRule", arg0, arg1) 167 | } 168 | 169 | func (_m *MockStore) FetchRollupRuleHistory(_param0 string, _param1 string) ([]view.RollupRule, error) { 170 | ret := _m.ctrl.Call(_m, "FetchRollupRuleHistory", _param0, _param1) 171 | ret0, _ := ret[0].([]view.RollupRule) 172 | ret1, _ := ret[1].(error) 173 | return ret0, ret1 174 | } 175 | 176 | func (_mr *_MockStoreRecorder) FetchRollupRuleHistory(arg0, arg1 interface{}) *gomock.Call { 177 | return _mr.mock.ctrl.RecordCall(_mr.mock, "FetchRollupRuleHistory", arg0, arg1) 178 | } 179 | 180 | func (_m *MockStore) FetchRuleSetSnapshot(_param0 string) (view.RuleSet, error) { 181 | ret := _m.ctrl.Call(_m, "FetchRuleSetSnapshot", _param0) 182 | ret0, _ := ret[0].(view.RuleSet) 183 | ret1, _ := ret[1].(error) 184 | return ret0, ret1 185 | } 186 | 187 | func (_mr *_MockStoreRecorder) FetchRuleSetSnapshot(arg0 interface{}) *gomock.Call { 188 | return _mr.mock.ctrl.RecordCall(_mr.mock, "FetchRuleSetSnapshot", arg0) 189 | } 190 | 191 | func (_m *MockStore) UpdateMappingRule(_param0 string, _param1 string, _param2 view.MappingRule, _param3 UpdateOptions) (view.MappingRule, error) { 192 | ret := _m.ctrl.Call(_m, "UpdateMappingRule", _param0, _param1, _param2, _param3) 193 | ret0, _ := ret[0].(view.MappingRule) 194 | ret1, _ := ret[1].(error) 195 | return ret0, ret1 196 | } 197 | 198 | func (_mr *_MockStoreRecorder) UpdateMappingRule(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 199 | return _mr.mock.ctrl.RecordCall(_mr.mock, "UpdateMappingRule", arg0, arg1, arg2, arg3) 200 | } 201 | 202 | func (_m *MockStore) UpdateRollupRule(_param0 string, _param1 string, _param2 view.RollupRule, _param3 UpdateOptions) (view.RollupRule, error) { 203 | ret := _m.ctrl.Call(_m, "UpdateRollupRule", _param0, _param1, _param2, _param3) 204 | ret0, _ := ret[0].(view.RollupRule) 205 | ret1, _ := ret[1].(error) 206 | return ret0, ret1 207 | } 208 | 209 | func (_mr *_MockStoreRecorder) UpdateRollupRule(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { 210 | return _mr.mock.ctrl.RecordCall(_mr.mock, "UpdateRollupRule", arg0, arg1, arg2, arg3) 211 | } 212 | 213 | func (_m *MockStore) UpdateRuleSet(_param0 changes.RuleSetChanges, _param1 int, _param2 UpdateOptions) (view.RuleSet, error) { 214 | ret := _m.ctrl.Call(_m, "UpdateRuleSet", _param0, _param1, _param2) 215 | ret0, _ := ret[0].(view.RuleSet) 216 | ret1, _ := ret[1].(error) 217 | return ret0, ret1 218 | } 219 | 220 | func (_mr *_MockStoreRecorder) UpdateRuleSet(arg0, arg1, arg2 interface{}) *gomock.Call { 221 | return _mr.mock.ctrl.RecordCall(_mr.mock, "UpdateRuleSet", arg0, arg1, arg2) 222 | } 223 | 224 | func (_m *MockStore) ValidateRuleSet(_param0 view.RuleSet) error { 225 | ret := _m.ctrl.Call(_m, "ValidateRuleSet", _param0) 226 | ret0, _ := ret[0].(error) 227 | return ret0 228 | } 229 | 230 | func (_mr *_MockStoreRecorder) ValidateRuleSet(arg0 interface{}) *gomock.Call { 231 | return _mr.mock.ctrl.RecordCall(_mr.mock, "ValidateRuleSet", arg0) 232 | } 233 | -------------------------------------------------------------------------------- /service/r2/store/update_options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package store 22 | 23 | // UpdateOptions is a set of ruleset or namespace update options. 24 | type UpdateOptions interface { 25 | // SetAuthor sets the author for an update. 26 | SetAuthor(value string) UpdateOptions 27 | 28 | // Author returns the author for an update. 29 | Author() string 30 | } 31 | 32 | type updateOptions struct { 33 | author string 34 | } 35 | 36 | // NewUpdateOptions creates a new set of update options. 37 | func NewUpdateOptions() UpdateOptions { 38 | return &updateOptions{} 39 | } 40 | 41 | func (o *updateOptions) SetAuthor(value string) UpdateOptions { 42 | opts := *o 43 | opts.author = value 44 | return &opts 45 | } 46 | 47 | func (o *updateOptions) Author() string { 48 | return o.author 49 | } 50 | -------------------------------------------------------------------------------- /service/service.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE 20 | 21 | package service 22 | 23 | import "github.com/gorilla/mux" 24 | 25 | // Service defines routes and handlers for a given entity. 26 | type Service interface { 27 | // URLPrefix returns the prefix for all routes of this service. 28 | URLPrefix() string 29 | 30 | // RegisterHandlers wires the http handlers for this Service with the given router. 31 | RegisterHandlers(router *mux.Router) error 32 | 33 | // Close closes the service. 34 | Close() 35 | } 36 | -------------------------------------------------------------------------------- /services/r2ctl/config/r2ctl.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package config 22 | 23 | import ( 24 | "errors" 25 | "time" 26 | 27 | "github.com/m3db/m3/src/cluster/client/etcd" 28 | clusterkv "github.com/m3db/m3/src/cluster/kv" 29 | "github.com/m3db/m3/src/metrics/rules" 30 | ruleskv "github.com/m3db/m3/src/metrics/rules/store/kv" 31 | "github.com/m3db/m3/src/metrics/rules/validator" 32 | "github.com/m3db/m3ctl/auth" 33 | r2store "github.com/m3db/m3ctl/service/r2/store" 34 | r2kv "github.com/m3db/m3ctl/service/r2/store/kv" 35 | "github.com/m3db/m3ctl/service/r2/store/stub" 36 | "github.com/m3db/m3x/instrument" 37 | "github.com/m3db/m3x/log" 38 | ) 39 | 40 | var ( 41 | errKVConfigRequired = errors.New("must provide kv configuration if not using stub store") 42 | ) 43 | 44 | // Configuration is the global configuration for r2ctl. 45 | type Configuration struct { 46 | // Logging configuration. 47 | Logging log.Configuration `yaml:"logging"` 48 | 49 | // HTTP server configuration. 50 | HTTP serverConfig `yaml:"http"` 51 | 52 | // Metrics configuration. 53 | Metrics instrument.MetricsConfiguration `yaml:"metrics"` 54 | 55 | // Store configuration. 56 | Store r2StoreConfiguration `yaml:"store"` 57 | 58 | // Simple Auth Config. 59 | Auth *auth.SimpleAuthConfig `yaml:"auth"` 60 | } 61 | 62 | // r2StoreConfiguration has all the fields necessary for an R2 store. 63 | type r2StoreConfiguration struct { 64 | // Stub means use the stub store. 65 | Stub bool `yaml:"stub"` 66 | 67 | // KV is the configuration for the etcd backed implementation of the kv store. 68 | KV *kvStoreConfig `yaml:"kv,omitempty"` 69 | } 70 | 71 | // NewR2Store creates a new R2 store. 72 | func (c r2StoreConfiguration) NewR2Store(instrumentOpts instrument.Options) (r2store.Store, error) { 73 | if c.Stub { 74 | return stub.NewStore(instrumentOpts), nil 75 | } 76 | 77 | if c.KV == nil { 78 | return nil, errKVConfigRequired 79 | } 80 | 81 | return c.KV.NewStore(instrumentOpts) 82 | } 83 | 84 | // kvStoreConfig is the configuration for the KV backed implementation of the R2 store. 85 | type kvStoreConfig struct { 86 | // KVClient configures the client for key value store. 87 | KVClient *etcd.Configuration `yaml:"kvClient" validate:"nonzero"` 88 | 89 | // KV configuration for the rules store. 90 | KVConfig clusterkv.OverrideConfiguration `yaml:"kvConfig"` 91 | 92 | // NamespacesKey is KV key associated with namespaces.. 93 | NamespacesKey string `yaml:"namespacesKey" validate:"nonzero"` 94 | 95 | // RuleSet key format. 96 | RuleSetKeyFmt string `yaml:"ruleSetKeyFmt" validate:"nonzero"` 97 | 98 | // Propagation delay for rule updates. 99 | PropagationDelay time.Duration `yaml:"propagationDelay" validate:"nonzero"` 100 | 101 | // Validation configuration. 102 | Validation *validator.Configuration `yaml:"validation"` 103 | } 104 | 105 | // NewStore creates a new KV backed R2 store. 106 | func (c kvStoreConfig) NewStore(instrumentOpts instrument.Options) (r2store.Store, error) { 107 | // Create rules store. 108 | kvClient, err := c.KVClient.NewClient(instrumentOpts) 109 | if err != nil { 110 | return nil, err 111 | } 112 | kvOpts, err := c.KVConfig.NewOverrideOptions() 113 | if err != nil { 114 | return nil, err 115 | } 116 | kvStore, err := kvClient.TxnStore(kvOpts) 117 | if err != nil { 118 | return nil, err 119 | } 120 | var validator rules.Validator 121 | if c.Validation != nil { 122 | validator, err = c.Validation.NewValidator(kvClient) 123 | if err != nil { 124 | return nil, err 125 | } 126 | } 127 | rulesStoreOpts := ruleskv.NewStoreOptions(c.NamespacesKey, c.RuleSetKeyFmt, validator) 128 | rulesStore := ruleskv.NewStore(kvStore, rulesStoreOpts) 129 | 130 | // Create kv store. 131 | r2StoreOpts := r2kv.NewStoreOptions(). 132 | SetInstrumentOptions(instrumentOpts). 133 | SetRuleUpdatePropagationDelay(c.PropagationDelay). 134 | SetValidator(validator) 135 | return r2kv.NewStore(rulesStore, r2StoreOpts), nil 136 | } 137 | -------------------------------------------------------------------------------- /services/r2ctl/config/server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package config 22 | 23 | import ( 24 | "time" 25 | 26 | httpserver "github.com/m3db/m3ctl/server/http" 27 | "github.com/m3db/m3x/instrument" 28 | ) 29 | 30 | type serverConfig struct { 31 | // Host is the host name the HTTP server shoud listen on. 32 | Host string `yaml:"host" validate:"nonzero"` 33 | 34 | // Port is the port the HTTP server should listen on. 35 | Port int `yaml:"port"` 36 | 37 | // ReadTimeout is the HTTP server read timeout. 38 | ReadTimeout time.Duration `yaml:"readTimeout"` 39 | 40 | // WriteTimeout HTTP server write timeout. 41 | WriteTimeout time.Duration `yaml:"writeTimeout"` 42 | } 43 | 44 | func (c *serverConfig) NewServerOptions( 45 | instrumentOpts instrument.Options, 46 | ) httpserver.Options { 47 | opts := httpserver.NewOptions().SetInstrumentOptions(instrumentOpts) 48 | if c.ReadTimeout != 0 { 49 | opts = opts.SetReadTimeout(c.ReadTimeout) 50 | } 51 | if c.WriteTimeout != 0 { 52 | opts = opts.SetWriteTimeout(c.WriteTimeout) 53 | } 54 | 55 | return opts 56 | } 57 | -------------------------------------------------------------------------------- /services/r2ctl/main/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "flag" 25 | "fmt" 26 | "os" 27 | "os/signal" 28 | "strconv" 29 | "syscall" 30 | "time" 31 | 32 | "github.com/m3db/m3ctl/auth" 33 | "github.com/m3db/m3ctl/server/http" 34 | "github.com/m3db/m3ctl/service/health" 35 | "github.com/m3db/m3ctl/service/r2" 36 | "github.com/m3db/m3ctl/services/r2ctl/config" 37 | "github.com/m3db/m3x/clock" 38 | xconfig "github.com/m3db/m3x/config" 39 | "github.com/m3db/m3x/instrument" 40 | ) 41 | 42 | const ( 43 | portEnvVar = "R2CTL_PORT" 44 | r2apiPrefix = "/r2/v1/" 45 | gracefulShutdownTimeout = 15 * time.Second 46 | ) 47 | 48 | func main() { 49 | configFile := flag.String("f", "config/base.yaml", "configuration file") 50 | flag.Parse() 51 | 52 | if len(*configFile) == 0 { 53 | flag.Usage() 54 | os.Exit(1) 55 | } 56 | 57 | var cfg config.Configuration 58 | if err := xconfig.LoadFile(&cfg, *configFile, xconfig.Options{}); err != nil { 59 | fmt.Printf("error loading config file: %v\n", err) 60 | os.Exit(1) 61 | } 62 | 63 | logger, err := cfg.Logging.BuildLogger() 64 | if err != nil { 65 | fmt.Printf("error creating logger: %v\n", err) 66 | os.Exit(1) 67 | } 68 | 69 | envPort := os.Getenv(portEnvVar) 70 | if envPort != "" { 71 | if p, err := strconv.Atoi(envPort); err == nil { 72 | logger.Infof("using env supplied port var: %s=%d", portEnvVar, p) 73 | cfg.HTTP.Port = p 74 | } else { 75 | logger.Fatalf("%s (%s) is not a valid port number", envPort, portEnvVar) 76 | } 77 | } 78 | 79 | if cfg.HTTP.Port == 0 { 80 | logger.Fatalf("no valid port configured. Can't start.") 81 | } 82 | 83 | scope, closer, err := cfg.Metrics.NewRootScope() 84 | if err != nil { 85 | logger.Fatalf("error creating metrics root scope: %v", err) 86 | } 87 | defer closer.Close() 88 | 89 | instrumentOpts := instrument.NewOptions(). 90 | SetLogger(logger). 91 | SetMetricsScope(scope). 92 | SetMetricsSamplingRate(cfg.Metrics.SampleRate()). 93 | SetReportInterval(cfg.Metrics.ReportInterval()) 94 | 95 | // Create R2 store. 96 | storeScope := scope.SubScope("r2-store") 97 | store, err := cfg.Store.NewR2Store(instrumentOpts.SetMetricsScope(storeScope)) 98 | if err != nil { 99 | logger.Fatalf("error initializing backing store: %v", err) 100 | } 101 | 102 | // Create R2 service. 103 | authService := auth.NewNoopAuth() 104 | if cfg.Auth != nil { 105 | authService = cfg.Auth.NewSimpleAuth() 106 | } 107 | r2ServiceScope := scope.Tagged(map[string]string{ 108 | "service-name": "r2", 109 | }) 110 | r2ServiceInstrumentOpts := instrumentOpts.SetMetricsScope(r2ServiceScope) 111 | r2Service := r2.NewService( 112 | r2apiPrefix, 113 | authService, 114 | store, 115 | r2ServiceInstrumentOpts, 116 | clock.NewOptions(), 117 | ) 118 | 119 | // Create health service. 120 | healthServiceScope := scope.Tagged(map[string]string{ 121 | "service-name": "health", 122 | }) 123 | healthServiceInstrumentOpts := instrumentOpts.SetMetricsScope(healthServiceScope) 124 | healthService := health.NewService(healthServiceInstrumentOpts) 125 | 126 | // Create HTTP server. 127 | listenAddr := fmt.Sprintf("%s:%d", cfg.HTTP.Host, cfg.HTTP.Port) 128 | httpServerScope := scope.Tagged(map[string]string{ 129 | "server-type": "http", 130 | }) 131 | httpServerInstrumentOpts := instrumentOpts.SetMetricsScope(httpServerScope) 132 | httpServerOpts := cfg.HTTP.NewServerOptions(httpServerInstrumentOpts) 133 | server, err := http.NewServer(listenAddr, httpServerOpts, r2Service, healthService) 134 | if err != nil { 135 | logger.Fatalf("could not create new server: %v", err) 136 | } 137 | 138 | logger.Infof("starting HTTP server on: %s", listenAddr) 139 | if err := server.ListenAndServe(); err != nil { 140 | logger.Fatalf("could not start serving traffic: %v", err) 141 | } 142 | 143 | // Handle interrupts. 144 | logger.Warnf("interrupt: %v", interrupt()) 145 | 146 | doneCh := make(chan struct{}) 147 | go func() { 148 | server.Close() 149 | logger.Infof("HTTP server closed") 150 | doneCh <- struct{}{} 151 | }() 152 | 153 | select { 154 | case <-doneCh: 155 | logger.Infof("clean shutdown") 156 | case <-time.After(gracefulShutdownTimeout): 157 | logger.Warnf("forced shutdown due to timeout after waiting for %v", gracefulShutdownTimeout) 158 | } 159 | } 160 | 161 | func interrupt() error { 162 | c := make(chan os.Signal, 1) 163 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 164 | return fmt.Errorf("%s", <-c) 165 | } 166 | -------------------------------------------------------------------------------- /tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "Tools": [ 3 | { 4 | "Repository": "github.com/rakyll/statik", 5 | "Commit": "19b88da8fc15428620782ba18f68423130e7ac7d" 6 | } 7 | ], 8 | "RetoolVersion": "1.3.7" 9 | } 10 | -------------------------------------------------------------------------------- /ui/.env: -------------------------------------------------------------------------------- 1 | NODE_PATH=src -------------------------------------------------------------------------------- /ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "react-app", 5 | "plugin:flowtype/recommended", 6 | "plugin:react/recommended", 7 | "eslint-config-uber-es2015", 8 | "eslint-config-uber-jsx", 9 | "prettier", 10 | "prettier/flowtype", 11 | "prettier/react" 12 | ], 13 | 14 | "plugins": [ 15 | "import", 16 | "flowtype", 17 | "react", 18 | "prettier" 19 | ], 20 | 21 | "env": { 22 | "jest": true, 23 | "browser": true 24 | }, 25 | 26 | "rules": { 27 | "no-inline-comments": 1, 28 | "max-len": 0, 29 | "func-style": 1, 30 | "valid-jsdoc": 2, 31 | "import/no-unresolved": "off", 32 | "import/named": 2, 33 | "import/namespace": 2, 34 | "import/default": 2, 35 | "import/export": 2, 36 | "flowtype/generic-spacing": 0, 37 | "prettier/prettier": ["error", "fb"], 38 | "react/no-string-refs": 0, 39 | "react/jsx-key": 0, 40 | "react/no-children-prop": 0 41 | } 42 | } -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /ui/.nvmrc: -------------------------------------------------------------------------------- 1 | 6 2 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # R2 UI 2 | 3 | Note: Work in progess 4 | 5 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 6 | 7 | ### Running the Application 8 | 9 | First, install yarn for faster npm install 10 | ``` 11 | brew install yarn 12 | ``` 13 | 14 | We need atleast Node v6, to switch use nvm: 15 | ``` 16 | nvm install 6 17 | nvm use 6 18 | ``` 19 | 20 | Install dependencies 21 | 22 | ``` 23 | yarn install 24 | ``` 25 | 26 | Run the UI in dev mode. The API is expected to be available at: http://localhost:9000/r2/v1/ 27 | 28 | ``` 29 | npm start 30 | ``` -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "r2-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:9000", 6 | "dependencies": { 7 | "antd": "^2.12.5", 8 | "axios": "^0.16.2", 9 | "basscss": "^8.0.3", 10 | "date-fns": "^1.29.0", 11 | "formik": "^0.9.4", 12 | "lodash": "^4.17.4", 13 | "moment": "2.18.1", 14 | "nprogress": "^0.2.0", 15 | "qs": "^6.5.1", 16 | "react": "^15.6.1", 17 | "react-dom": "^15.6.1", 18 | "react-refetch": "^1.0.0", 19 | "react-router-dom": "^4.1.2", 20 | "react-scripts": "1.0.10", 21 | "recompose": "^0.24.0", 22 | "yup": "^0.22.0" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test --env=jsdom", 28 | "eject": "react-scripts eject", 29 | "add-license": "cd src && uber-licence", 30 | "lint": "eslint src" 31 | }, 32 | "devDependencies": { 33 | "babel-eslint": "7.2.3", 34 | "eslint": "3.19.0", 35 | "eslint-config-prettier": "^2.3.0", 36 | "eslint-config-react-app": "^1.0.5", 37 | "eslint-config-uber-es2015": "^3.1.2", 38 | "eslint-config-uber-jsx": "^3.3.3", 39 | "eslint-plugin-flowtype": "2.33.0", 40 | "eslint-plugin-import": "2.2.0", 41 | "eslint-plugin-jsx-a11y": "5.0.1", 42 | "eslint-plugin-prettier": "^2.1.2", 43 | "eslint-plugin-react": "7.0.1", 44 | "prettier": "^1.5.3", 45 | "uber-licence": "^3.1.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3db/m3ctl/65204284e1d73ef9b6d56163cfcddfd93b627921/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | R2Ctrl 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/components/HelpTooltip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Popover, Icon} from 'antd'; 3 | import {getHelpText} from 'utils/helpText'; 4 | 5 | export default function HelpTooltip({helpTextKey, title, ...rest}) { 6 | return ( 7 | {getHelpText(helpTextKey)}} 10 | trigger="click" 11 | {...rest}> 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/components/MappingRuleEditor.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | import React from 'react'; 22 | import {Button, Input, Form} from 'antd'; 23 | import {withFormik} from 'formik'; 24 | import * as util from 'utils'; 25 | import yup from 'yup'; 26 | import PoliciesEditor from './PolicyEditor'; 27 | import {filterPoliciesBasedOnTag} from 'utils'; 28 | import {getHelpText} from 'utils/helpText'; 29 | const schema = yup.object().shape({ 30 | name: yup.string('Name filter is required').required(), 31 | filter: yup.string().required('Metric filter is required'), 32 | }); 33 | 34 | const FormItem = Form.Item; 35 | const formItemLayout = { 36 | labelCol: { 37 | xs: {span: 24}, 38 | sm: {span: 4}, 39 | }, 40 | wrapperCol: { 41 | xs: {span: 24}, 42 | sm: {span: 18}, 43 | }, 44 | }; 45 | function MappingRuleEditor({ 46 | mappingRule, 47 | values, 48 | handleChange, 49 | handleSubmit, 50 | setFieldValue, 51 | ...rest 52 | }) { 53 | const typeTag = util.getTypeTag(values.filter); 54 | 55 | return ( 56 |
57 |
58 |
59 | 64 | 70 | 71 |
72 |
73 | 79 | { 84 | const newTypeTag = util.getTypeTag(e.target.value); 85 | setFieldValue( 86 | 'policies', 87 | filterPoliciesBasedOnTag(values.policies, newTypeTag), 88 | ); 89 | handleChange(e); 90 | }} 91 | /> 92 | 93 |
94 |
95 | 101 | setFieldValue('policies', e)} 105 | /> 106 | 107 |
108 | 111 |
112 |
113 | ); 114 | } 115 | 116 | export default withFormik({ 117 | enableReinitialize: true, 118 | mapPropsToValues: ({mappingRule}) => { 119 | return mappingRule || {}; 120 | }, 121 | handleSubmit: (values, {props}) => { 122 | props.onSubmit(values); 123 | }, 124 | validationSchema: schema, 125 | })(MappingRuleEditor); 126 | -------------------------------------------------------------------------------- /ui/src/components/MappingRulesTable.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | import React from 'react'; 22 | import {Table, Tag} from 'antd'; 23 | import _ from 'lodash'; 24 | import MappingRuleEditor from 'components/MappingRuleEditor'; 25 | import TableActions from './TableActions'; 26 | import {compose} from 'recompose'; 27 | import {connectR2API} from 'hocs'; 28 | import {formatTimestampMilliseconds} from 'utils'; 29 | import HelpTooltip from './HelpTooltip'; 30 | const {Column} = Table; 31 | 32 | function MappingRuleHistoryBase(props) { 33 | const loading = _.get(props.mappingRuleHistory, 'pending'); 34 | const mappingRules = _.get(props.mappingRuleHistory, 'value.mappingRules'); 35 | return ( 36 | index} 38 | mappingRules={mappingRules} 39 | loading={loading} 40 | showActions={false} 41 | /> 42 | ); 43 | } 44 | export const MappingRuleHistory = compose( 45 | connectR2API(props => { 46 | const {mappingRuleID, namespaceID} = props; 47 | return { 48 | mappingRuleHistory: { 49 | url: `/namespaces/${namespaceID}/mapping-rules/${mappingRuleID}/history`, 50 | }, 51 | }; 52 | }), 53 | )(MappingRuleHistoryBase); 54 | 55 | function MappingRulesTable(props) { 56 | const { 57 | namespaceID, 58 | loading, 59 | mappingRules, 60 | showActions = true, 61 | rowKey = 'id', 62 | saveMappingRule, 63 | deleteMappingRule, 64 | } = props; 65 | return ( 66 | 72 | 73 | 76 | Metric Filter 77 | 78 | } 79 | dataIndex="filter" 80 | render={filter => {filter}} 81 | /> 82 | 85 | Policies 86 | 87 | } 88 | dataIndex="policies" 89 | render={policies => { 90 | return _.map(policies, policy => {policy}); 91 | }} 92 | /> 93 | user || 'N/A'} 97 | /> 98 | formatTimestampMilliseconds(timestamp)} 102 | /> 103 | 106 | Effective Time (Local) 107 | 108 | } 109 | dataIndex="cutoverMillis" 110 | render={cutoverMillis => formatTimestampMilliseconds(cutoverMillis)} 111 | /> 112 | {showActions && ( 113 | ( 119 | 121 | props.setModal({ 122 | open: true, 123 | title: 'Edit Mapping Rule', 124 | content: ( 125 | { 128 | saveMappingRule(values); 129 | }} 130 | /> 131 | ), 132 | })} 133 | onDeleteClicked={() => deleteMappingRule(mappingRule)} 134 | onHistoryClicked={() => 135 | props.setModal({ 136 | open: true, 137 | title: 'Mapping Rule History', 138 | content: ( 139 | 143 | ), 144 | })} 145 | /> 146 | )} 147 | /> 148 | )} 149 |
150 | ); 151 | } 152 | 153 | export default MappingRulesTable; 154 | -------------------------------------------------------------------------------- /ui/src/components/PolicyEditor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // Copyright (c) 2017 Uber Technologies, Inc. 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | import React from 'react'; 24 | import {Select, Icon, Button} from 'antd'; 25 | import _ from 'lodash'; 26 | 27 | import HelpTooltip from './HelpTooltip'; 28 | 29 | export const AGGREGATION_FUNCTIONS = [ 30 | 'Min', 31 | 'Max', 32 | 'Mean', 33 | 'Median', 34 | 'Count', 35 | 'Sum', 36 | 'SumSq', 37 | 'Stdev', 38 | 'P10', 39 | 'P20', 40 | 'P30', 41 | 'P40', 42 | 'P50', 43 | 'P60', 44 | 'P70', 45 | 'P80', 46 | 'P90', 47 | 'P95', 48 | 'P99', 49 | 'P999', 50 | 'P9999', 51 | ]; 52 | 53 | export const POLICIES = [ 54 | '10s:2d', 55 | '1m:2d', 56 | '1m:40d', 57 | '10m:180d', 58 | '10m:1y', 59 | '10m:3y', 60 | '10m:5y', 61 | '1h:1y', 62 | '1h:3y', 63 | '1h:5y', 64 | ]; 65 | 66 | const Option = Select.Option; 67 | 68 | type Props = { 69 | value: any, 70 | onChange: e => any, 71 | showDelete: boolean, 72 | onDeleteClicked: () => any, 73 | typeTag: string, 74 | }; 75 | 76 | type PolicyObject = { 77 | timePolicy: string, 78 | aggFunctions: array, 79 | }; 80 | 81 | export function parsePolicy(policy: string): PolicyObject { 82 | const policyParsed = policy.split('|'); 83 | return { 84 | timePolicy: _.first(policyParsed), 85 | aggFunctions: _.chain(policyParsed) 86 | .nth(1) 87 | .thru(value => { 88 | if (_.isEmpty(value)) { 89 | return []; 90 | } 91 | return value.split(','); 92 | }) 93 | .value(), 94 | }; 95 | } 96 | 97 | export function stringifyPolicy(policy: PolicyObject): string { 98 | return `${policy.timePolicy}${_.isEmpty(policy.aggFunctions) 99 | ? '' 100 | : `|${policy.aggFunctions.join(',')}`}`; 101 | } 102 | 103 | export function PolicyEditor(props: Props) { 104 | const { 105 | value = _.first(POLICIES), 106 | onDeleteClicked = noop => noop, 107 | showDelete = false, 108 | onChange = noop => noop, 109 | typeTag = '', 110 | policyList = POLICIES, 111 | } = props; 112 | const policy = parsePolicy(value); 113 | const handleChange = (field, newValue) => { 114 | onChange( 115 | stringifyPolicy({ 116 | ...policy, 117 | [field]: newValue, 118 | }), 119 | ); 120 | }; 121 | const showAggs = typeTag === 'timer'; 122 | let policies = policyList; 123 | if (!_.includes(['gauge', 'counter'], typeTag)) { 124 | policies = policies.filter(p => !p.startsWith('1h')); 125 | } 126 | 127 | return ( 128 |
129 | {showDelete && ( 130 | 134 | 135 | 136 | )} 137 |
138 | 142 | 153 | {typeTag === 'timer' && ( 154 | 155 | Resolutions {'>'} 1 minute are not supported for timers 156 | 157 | )} 158 |
159 |
160 | 164 | {showAggs ? ( 165 | 178 | ) : ( 179 | Only supported for timers 180 | )} 181 |
182 |
183 | ); 184 | } 185 | 186 | function PoliciesEditor(props) { 187 | const {value: policies, onChange, typeTag} = props; 188 | return ( 189 |
195 | {_.map(policies, (p, i) => { 196 | return ( 197 |
204 | { 206 | onChange(policies.filter((__, idx) => i !== idx)); 207 | }} 208 | showDelete={_.size(policies) > 1} 209 | value={p} 210 | typeTag={typeTag} 211 | onChange={e => { 212 | const newPolicy = policies.slice(); 213 | newPolicy[i] = e; 214 | onChange(newPolicy); 215 | }} 216 | /> 217 |
218 | ); 219 | })} 220 |
221 | 230 |
231 |
232 | ); 233 | } 234 | 235 | export default PoliciesEditor; 236 | -------------------------------------------------------------------------------- /ui/src/components/RollupRuleEditor.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | import React from 'react'; 22 | import {Button, Input, Form, Icon, Select, Card, Tag} from 'antd'; 23 | import {toClass} from 'recompose'; 24 | import _ from 'lodash'; 25 | import {withFormik} from 'formik'; 26 | import * as util from 'utils'; 27 | import PoliciesEditor from './PolicyEditor'; 28 | import {filterPoliciesBasedOnTag} from 'utils'; 29 | import {getHelpText} from 'utils/helpText'; 30 | import HelpTooltip from './HelpTooltip'; 31 | 32 | // @TODO Move to config service 33 | const REQUIRED_TAGS = ['dc', 'env', 'service', 'type']; 34 | 35 | const FormItem = Form.Item; 36 | const formItemLayout = { 37 | labelCol: { 38 | xs: {span: 24}, 39 | sm: {span: 4}, 40 | }, 41 | wrapperCol: { 42 | xs: {span: 24}, 43 | sm: {span: 18}, 44 | }, 45 | }; 46 | 47 | function removeRequiredTags(tags, requiredTags) { 48 | return _.filter(tags, t => !_.includes(requiredTags, t)); 49 | } 50 | 51 | function addRequiredTags(tags, requiredTags) { 52 | return _.concat(tags, requiredTags); 53 | } 54 | 55 | function RollupRuleEditor({values, handleChange, handleSubmit, setFieldValue}) { 56 | const typeTag = util.getTypeTag(values.filter); 57 | return ( 58 |
59 |
60 |
61 | 66 | 72 | 73 |
74 |
75 | 81 | { 86 | const newTypeTag = util.getTypeTag(e.target.value); 87 | const targets = _.map(values.targets, t => ({ 88 | ...t, 89 | policies: filterPoliciesBasedOnTag(t.policies, newTypeTag), 90 | })); 91 | setFieldValue('target', targets); 92 | handleChange(e); 93 | }} 94 | /> 95 | 96 |
97 |
98 | 102 | Targets 103 | 104 | } 105 | colon={false} 106 | {...formItemLayout}> 107 | setFieldValue('targets', e)} 111 | /> 112 | 113 |
114 |
115 | 118 |
119 |
120 |
121 | ); 122 | } 123 | 124 | const TargetsEditorBase = props => { 125 | const {value: targets = [], onChange, typeTag} = props; 126 | const handleChange = (index, property, newValue) => { 127 | targets[index][property] = newValue; 128 | onChange(targets); 129 | }; 130 | return ( 131 |
132 | {_.isEmpty(targets) &&
No targets
} 133 | {_.map(targets, (t, i) => { 134 | return ( 135 | { 142 | onChange(_.filter(targets, (__, index) => index !== i)); 143 | }}> 144 | 145 | 146 | }> 147 | 148 | handleChange(i, 'name', e.target.value)} 152 | /> 153 | 156 |
157 | {_.map(REQUIRED_TAGS, requiredTag => {requiredTag})} 158 | props.setMappingRulesFilter(e.target.value)} 98 | placeholder="Mapping Rule Name Filter" 99 | /> 100 | 106 | 107 | 108 |
109 |
110 |

Rollup Rules

111 |

{getHelpText('rollup-rule')}

112 |
113 |
114 | 131 |
132 |
133 | props.setRollupRuleFilter(e.target.value)} 137 | placeholder="Rollup Rule Name Filter" 138 | /> 139 | 145 |
146 | 147 | 148 |
149 | ); 150 | } 151 | 152 | export default compose( 153 | withProps(props => { 154 | return { 155 | namespaceID: props.match.params.id, 156 | tab: props.match.params.tab || 'mapping-rules', 157 | }; 158 | }), 159 | connectR2API(props => { 160 | const namespaceFetch = { 161 | url: `/namespaces/${props.namespaceID}`, 162 | refreshing: true, 163 | }; 164 | return { 165 | namespaceFetch: { 166 | ...namespaceFetch, 167 | force: false, 168 | }, 169 | saveMappingRule: mappingRule => { 170 | const {namespaceID} = props; 171 | const isNewRule = !_.has(mappingRule, 'id'); 172 | const method = isNewRule ? 'POST' : 'PUT'; 173 | const urlPath = isNewRule ? '' : `/${mappingRule.id}`; 174 | return { 175 | saveMappingRuleFetch: { 176 | meta: { 177 | successMessage: 'Mapping rule saved!', 178 | }, 179 | url: `/namespaces/${namespaceID}/mapping-rules${urlPath}`, 180 | method, 181 | body: mappingRule, 182 | force: true, 183 | refreshing: true, 184 | andThen: () => ({namespaceFetch}), 185 | }, 186 | }; 187 | }, 188 | deleteMappingRule: mappingRule => { 189 | const {namespaceID} = props; 190 | return { 191 | namespaceFetch: { 192 | meta: { 193 | successMessage: true, 194 | }, 195 | url: `/namespaces/${namespaceID}/mapping-rules/${mappingRule.id}`, 196 | method: 'DELETE', 197 | force: true, 198 | refreshing: true, 199 | andThen: () => ({namespaceFetch}), 200 | }, 201 | }; 202 | }, 203 | saveRollupRule: rollupRule => { 204 | const {namespaceID} = props; 205 | const isNewRule = !_.has(rollupRule, 'id'); 206 | const method = isNewRule ? 'POST' : 'PUT'; 207 | const urlPath = isNewRule ? '' : `/${rollupRule.id}`; 208 | return { 209 | saveRollupRuleFetch: { 210 | meta: { 211 | successMessage: 'Rollup rule saved!', 212 | }, 213 | url: `/namespaces/${namespaceID}/rollup-rules${urlPath}`, 214 | method, 215 | body: rollupRule, 216 | force: true, 217 | refreshing: true, 218 | andThen: () => ({namespaceFetch}), 219 | }, 220 | }; 221 | }, 222 | deleteRollupRule: rollupRule => { 223 | const {namespaceID} = props; 224 | return { 225 | namespaceFetch: { 226 | meta: { 227 | successMessage: true, 228 | }, 229 | url: `/namespaces/${namespaceID}/rollup-rules/${rollupRule.id}`, 230 | method: 'DELETE', 231 | force: true, 232 | refreshing: true, 233 | andThen: () => ({namespaceFetch}), 234 | }, 235 | }; 236 | }, 237 | }; 238 | }), 239 | withProps(props => { 240 | const {namespaceFetch} = props; 241 | return { 242 | loading: namespaceFetch.pending || namespaceFetch.refreshing, 243 | mappingRules: _.get(namespaceFetch.value, 'mappingRules', []), 244 | rollupRules: _.get(namespaceFetch.value, 'rollupRules', []), 245 | }; 246 | }), 247 | withReducer( 248 | 'modal', 249 | 'setModal', 250 | (state, payload) => { 251 | return { 252 | ...state, 253 | ...payload, 254 | }; 255 | }, 256 | props => ({open: false, content: null}), 257 | ), 258 | withPromiseStateChangeCallback(['saveRollupRuleFetch'], props => { 259 | if (props.saveRollupRuleFetch.fulfilled) { 260 | props.setModal({open: false}); 261 | } 262 | }), 263 | withPromiseStateChangeCallback(['saveMappingRuleFetch'], props => { 264 | if (props.saveMappingRuleFetch.fulfilled) { 265 | props.setModal({open: false}); 266 | } 267 | }), 268 | withFilter({ 269 | propMapper: props => props.mappingRules, 270 | propName: 'mappingRules', 271 | propField: 'name', 272 | filterPropName: 'mappingRulesFilter', 273 | filterChangeHandlerName: 'setMappingRulesFilter', 274 | }), 275 | withFilter({ 276 | propMapper: props => props.rollupRules, 277 | propName: 'rollupRules', 278 | propField: 'name', 279 | filterPropName: 'rollupRuleFilter', 280 | filterChangeHandlerName: 'setRollupRuleFilter', 281 | }), 282 | )(Namespace); 283 | -------------------------------------------------------------------------------- /ui/src/pages/Namespaces.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | import React from 'react'; 22 | import {compose, withProps, withState} from 'recompose'; 23 | import {Button, Card, Input, Popconfirm, Table, Icon} from 'antd'; 24 | import _ from 'lodash'; 25 | import {Link} from 'react-router-dom'; 26 | import {connectR2API, withFilter} from 'hocs'; 27 | import {getHelpText} from 'utils/helpText'; 28 | const {Column} = Table; 29 | 30 | function NamespaceTable(props) { 31 | return ( 32 | 37 | 39 | namespaceA.id.localeCompare(namespaceB.id)} 40 | title="Name" 41 | dataIndex="id" 42 | render={namespace => { 43 | return {namespace}; 44 | }} 45 | /> 46 | ( 52 | props.deleteNamespace(namespace)} 56 | okText="Yes" 57 | cancelText="No"> 58 | 59 | 60 | 61 | 62 | )} 63 | /> 64 |
65 | ); 66 | } 67 | 68 | function CreateNamespaceFormBase(props) { 69 | const {namespace, onNamespaceChange, onSaveClick} = props; 70 | return ( 71 |
72 | onNamespaceChange(e.target.value)} 79 | /> 80 | 87 |
88 | ); 89 | } 90 | 91 | const CreateNamespaceForm = compose( 92 | withState('namespace', 'onNamespaceChange', ''), 93 | withProps(props => { 94 | return { 95 | onSaveClick: () => { 96 | props.onSaveNamespace(props.namespace); 97 | props.onNamespaceChange(''); 98 | }, 99 | }; 100 | }), 101 | )(CreateNamespaceFormBase); 102 | 103 | function Namespaces(props) { 104 | return ( 105 |
106 |
107 |

Namespaces

108 |

{getHelpText('namespace')}

109 |
110 | 111 | 112 | 113 | props.setNameFilter(e.target.value)} 117 | placeholder="Namespace Filter" 118 | /> 119 | 120 | 121 |
122 | ); 123 | } 124 | 125 | export default compose( 126 | connectR2API(props => { 127 | const namespacesFetch = {url: '/namespaces', refreshing: true}; 128 | return { 129 | namespacesFetch: { 130 | ...namespacesFetch, 131 | force: false, 132 | }, 133 | saveNamespace: namespace => { 134 | return { 135 | saveNamespaceResponse: { 136 | meta: { 137 | successMessage: 'Namespace created!', 138 | }, 139 | url: '/namespaces', 140 | method: 'POST', 141 | force: true, 142 | body: { 143 | id: namespace, 144 | }, 145 | andThen: () => ({namespacesFetch}), 146 | }, 147 | }; 148 | }, 149 | deleteNamespace: namespace => { 150 | return { 151 | deleteNamespaceResponse: { 152 | meta: { 153 | successMessage: 'Namespace deleted!', 154 | }, 155 | url: `/namespaces/${namespace.id}`, 156 | method: 'DELETE', 157 | force: true, 158 | andThen: () => ({namespacesFetch}), 159 | }, 160 | }; 161 | }, 162 | }; 163 | }), 164 | withFilter({ 165 | propMapper: props => _.get(props.namespacesFetch.value, 'namespaces', []), 166 | propName: 'namespaces', 167 | propField: 'id', 168 | queryParam: 'q', 169 | filterPropName: 'nameFilter', 170 | filterChangeHandlerName: 'setNameFilter', 171 | }), 172 | )(Namespaces); 173 | -------------------------------------------------------------------------------- /ui/src/utils/helpText.js: -------------------------------------------------------------------------------- 1 | const helpText = { 2 | namespace: 3 | 'Logical grouping of rules. The namespace name must be a valid service name.', 4 | 'mapping-rule': 5 | 'Configure the resolution, retention, and optional custom aggregation functions for metrics matching the filter.', 6 | 'rollup-rule': 7 | 'Configure how to roll up metrics matching the filter and the corresponding policies. A matching metric triggers the generation of a new rollup metric whose name, tags, and policies are defined by the rollup targets. ', 8 | 'metric-filter': 9 | ' A list of tag name:glob pattern pairs identifying matching metrics.', 10 | policy: 11 | 'Defines the resolution, retention period, and optional custom aggregation functions for given metric.', 12 | target: 13 | 'Defines how a new rollup metric is generated and its policies. The rollup metric will use “Rollup Metric Name” as its name, “Rollup Tags” as its tags, and “Policies” as its policies. ', 14 | 'rollup-tag': 'Tags retained by the generated rollup metric.', 15 | 'resolution:retention-period': 16 | 'Describes the resolution at which the metric will be aggregated, and how long the metric will be stored for. ', 17 | 'aggregation-function': 18 | 'Defines how the given metric is aggregated. For example, an aggregation function of P99 returns the 99th percentile of the given metric.', 19 | 'effective-time': 'The time at which the rule will be in effect', 20 | }; 21 | 22 | export function getHelpText(helpTextKey) { 23 | if (!helpText[helpTextKey]) { 24 | console.error(`The helpKey: ${helpTextKey} does not exist`); // eslint-disable-line 25 | } 26 | return helpText[helpTextKey] || ''; 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/utils/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import _ from 'lodash'; 3 | import dateFns from 'date-fns'; 4 | 5 | export function getTypeTag(tagString: string): string { 6 | const regex = /type:([^\s]*)/g; 7 | const results = regex.exec(tagString); 8 | return _.nth(results, 1) || null; 9 | } 10 | 11 | export function filterPoliciesBasedOnTag( 12 | policies: Array, 13 | typeTag: string, 14 | ) { 15 | if (_.includes(['gauge', 'counter'], typeTag)) { 16 | return policies; 17 | } 18 | return _.filter(policies, p => !p.startsWith('10m')); 19 | } 20 | 21 | export function formatTimestampMilliseconds(timestamp: number): string { 22 | if (!timestamp) { 23 | return 'N/A'; 24 | } 25 | return dateFns.format(timestamp, 'MM-DD-YYYY hh:mm:ss a'); 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/utils/index.test.js: -------------------------------------------------------------------------------- 1 | import {getTypeTag, filterPoliciesBasedOnTag} from './index'; 2 | 3 | describe('getTypeTag()', () => { 4 | it('should parse out the type tag from a metric filter', () => { 5 | expect(getTypeTag('type:timer')).toBe('timer'); 6 | expect(getTypeTag('type:a')).toBe('a'); 7 | expect(getTypeTag('this:test type')).toBe(null); 8 | expect(getTypeTag('type:counts ')).toBe('counts'); 9 | }); 10 | }); 11 | 12 | describe('filterPoliciesBasedOnTag()', () => { 13 | it('should not remove any policies for gauges', () => { 14 | expect( 15 | filterPoliciesBasedOnTag(['1m:10s', '10m:10s', '10m:30d'], 'gauge'), 16 | ).toEqual(['1m:10s', '10m:10s', '10m:30d']); 17 | }); 18 | 19 | it('should not remove any policies for counters', () => { 20 | expect( 21 | filterPoliciesBasedOnTag(['1m:10s', '10m:10s', '10m:30d'], 'counter'), 22 | ).toEqual(['1m:10s', '10m:10s', '10m:30d']); 23 | }); 24 | 25 | it('should remove any policies over 1m resolution when not a gauge or counter', () => { 26 | const input = ['1m:10s', '10m:10s', '10m:30d']; 27 | const output = ['1m:10s']; 28 | const typesToTest = ['', null, 'timers']; 29 | 30 | typesToTest.forEach(type => 31 | expect(filterPoliciesBasedOnTag(input, type)).toEqual(output), 32 | ); 33 | }); 34 | }); 35 | --------------------------------------------------------------------------------