diff --git a/.github/workflows/esad-cli-publish-artifact-workflow.yml b/.github/workflows/esad-cli-publish-artifact-workflow.yml new file mode 100644 index 00000000..86b58946 --- /dev/null +++ b/.github/workflows/esad-cli-publish-artifact-workflow.yml @@ -0,0 +1,57 @@ +name: Publish +on: [pull_request, push] + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: cli + steps: + - name: Set up Go ubuntu-latest + uses: actions/setup-go@v2 + with: + go-version: 1.14 + + - name: Check out AD CLI + uses: actions/checkout@v2 + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + version: latest + args: --snapshot --skip-publish + workdir: cli + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload macOS(amd64) Artifact + uses: actions/upload-artifact@v2 + with: + name: esad-darwin-amd64 + path: cli/dist/esad_darwin_amd64/esad + + - name: Upload Linux(amd64) Artifact + uses: actions/upload-artifact@v2 + with: + name: esad-linux-amd64 + path: cli/dist/esad_linux_amd64/esad + + - name: Upload Linux(arm64) Artifact + uses: actions/upload-artifact@v2 + with: + name: esad-linux-arm64 + path: cli/dist/esad_linux_arm64/esad + + - name: Upload Windows(i386) Artifact + uses: actions/upload-artifact@v2 + with: + name: esad-windows-386 + path: cli/dist/esad_windows_386/esad.exe + + - name: Upload Windows(amd64) Artifact + uses: actions/upload-artifact@v2 + with: + name: esad-windows-amd64 + path: cli/dist/esad_windows_amd64/esad.exe + diff --git a/.github/workflows/esad-cli-test-and-build-workflow.yml b/.github/workflows/esad-cli-test-and-build-workflow.yml new file mode 100644 index 00000000..840a0897 --- /dev/null +++ b/.github/workflows/esad-cli-test-and-build-workflow.yml @@ -0,0 +1,74 @@ +name: AD CLI Test and Build +on: [pull_request, push] + +jobs: + build: + defaults: + run: + working-directory: cli + strategy: + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + go-version: [1.14] + runs-on: ${{ matrix.platform }} + steps: + - name: Set up Go ${{ matrix.platform }} + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Check out AD CLI + uses: actions/checkout@v2 + + - name: Build + env: + GOPROXY: "https://proxy.golang.org" + run: go build . + + code-analysis: + defaults: + run: + working-directory: cli + runs-on: ubuntu-latest + steps: + - name: Set up Go ubuntu-latest + uses: actions/setup-go@v2 + with: + go-version: 1.14 + + - name: Check out AD CLI + uses: actions/checkout@v2 + + - name: gofmt + run: gofmt -s -w . + - name: Check for modified files + id: git-check + run: | + echo ::set-output name=modified::$(if git diff-index --quiet HEAD --; then echo "false"; else echo "true"; fi) + - name: Display unformated changes + if: steps.git-check.outputs.modified == 'true' + run: | + echo "Failed to format using go-fmt". + git diff + + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v1 + with: + version: v1.28 + working-directory: cli + + - name: Run coverage + env: + GOPROXY: "https://proxy.golang.org" + run: | + go test ./... -coverprofile=coverage.out + go tool cover -func=coverage.out + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1.0.3 + with: + token: ${{secrets.CODECOV_TOKEN}} + file: cli/coverage.out + flags: unittests + name: codecov-umbrella diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 00000000..c63b07d0 --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1,23 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# IDE Settings +.idea/* + +#OS Settings +.DS_Store + +# Test binary, built with `go test -c` +*.test + +bin +.go +.push-* +.container-* +.dockerfile-* + +/coverage.out diff --git a/cli/.goreleaser.yml b/cli/.goreleaser.yml new file mode 100644 index 00000000..bf09ccee --- /dev/null +++ b/cli/.goreleaser.yml @@ -0,0 +1,39 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +project_name: esad +dist: ./dist +before: + hooks: + # You may remove this if you don't use go modules. + - go mod download +builds: +- env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - 386 + - amd64 + - arm + - arm64 + ignore: + - goos: darwin + goarch: 386 +archives: +- + name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + replacements: + darwin: Darwin + linux: Linux + windows: Windows + 386: i386 + amd64: x86_64 + format_overrides: + - goos: windows + format: zip +checksum: + name_template: '{{ .ProjectName }}_checksums.txt' +snapshot: + name_template: '{{ .ProjectName }}_{{ .Version }}' diff --git a/cli/LICENSE b/cli/LICENSE new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/cli/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/cli/NOTICE b/cli/NOTICE new file mode 100644 index 00000000..56196b2e --- /dev/null +++ b/cli/NOTICE @@ -0,0 +1,2 @@ +Open Distro for Elasticsearch Anomaly Detection CLI +Copyright 2019-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000..4ff674b4 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,105 @@ +![AD CLI Test and Build](https://github.com/opendistro-for-elasticsearch/anomaly-detection/workflows/AD%20CLI%20Test%20and%20Build/badge.svg) +# Open Distro for Elasticsearch AD CLI + +The AD CLI component in Open Distro for Elasticsearch (ODFE) is a command line interface for ODFE AD plugin. +This CLI provides greater flexibility of use. User can use CLI to easily do things that are difficult or sometimes impossible to do with kibana UI. This doesn’t use any additional system resources to load any of graphical part, thus making it simpler and faster than UI. + +It only supports [Open Distro for Elasticsearch (ODFE) AD Plugin](https://opendistro.github.io/for-elasticsearch-docs/docs/ad/) +You must have the ODFE AD plugin installed to your Elasticsearch instance to connect. +Users can run this CLI from MacOS, Windows, Linux and connect to any valid Elasticsearch end-point such as Amazon Elasticsearch Service (AES).The ESAD CLI implements AD APIs. + +## Version Compatibility Matrix + +| ESAD Version | ODFE Version | +| ------------- |:-------------------:| +| 0.1 | 1.7.X, 1.8.X, 1.9.X | + +## Features + +* Create Detectors +* Start, Stop, Delete Detectors +* Create named profiles to connect to ES cluster + +## Install + +Launch your local Elasticsearch instance and make sure you have the Open Distro for Elasticsearch AD plugin installed. + +To install the AD CLI: + + +1. Install from source: + + ``` + $ go get github.com/opendistro-for-elasticsearch/anomaly-detection/cli + ``` + +## Configure + +Before using the ESAD CLI, you need to configure your ESAD credentials. You can do this in several ways: + +* Configuration command +* Config file + +The quickest way to get started is to run the `esad profile create` + +``` +$ esad profile create +Enter profile's name: dev +ES Anomaly Detection Endpoint: https://localhost:9200 +ES Anomaly Detection User: admin +ES Anomaly Detection Password: +``` +Make sure profile name is unique within config file. `create` command will not allow user to create duplicate profile. + +To use a config file, create a YAML file like this +``` +profiles: +- endpoint: https://localhost:9200 + username: admin + password: foobar + name: default +- endpoint: https://odfe-node1:9200 + username: admin + password: foobar + name: dev +``` +and place it on ~/.esad/config.yaml. +If you wish to place the shared config file in a different location than the one specified above, you need to tell esad where to find it. Do this by setting the appropriate environment variable: + +``` +export ESAD_CONFIG_FILE=/path/to/config_file +``` +You can have multiple profiles defined in the configuration file. +You can then specify which profile to use by using the --profile option. `default` profile will be used if profile parameter is skipped. + + + +## Basic Commands + +An ESAD CLI has following structure +``` +$ esad [flags and parameters] +``` +For example to start detector: +``` +$ esad start [detector-name-pattern] +``` +To view help documentation, use one of the following: +``` +$ esad --help +$ esad --help +$ esad --help +``` +To get the version of the ESAD CLI: +``` +$ esad --version +``` + +## Getting Help + +The best way to interact with our team is through GitHub. You can open an [issue](https://github.com/opendistro-for-elasticsearch/anomaly-detection/issues) and tag accordingly. + +## Copyright + +Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + diff --git a/cli/cmd/create.go b/cli/cmd/create.go new file mode 100644 index 00000000..12a68bd4 --- /dev/null +++ b/cli/cmd/create.go @@ -0,0 +1,79 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cmd + +import ( + handler "esad/internal/handler/ad" + "fmt" + "github.com/spf13/cobra" +) + +const ( + commandCreate = "create" + interactive = "interactive" + generate = "generate-skeleton" +) + +//createCmd creates detectors based on file configuration, if interactive mode is on, +//this command will prompt for confirmation on number of detectors will be created on executions. +var createCmd = &cobra.Command{ + Use: commandCreate + " [list of file-path] [flags]", + Short: "Creates detectors based on configurations", + Long: `Creates detectors based on configurations specified by file path`, + Run: func(cmd *cobra.Command, args []string) { + generate, _ := cmd.Flags().GetBool(generate) + if generate { + generateTemplate() + return + } + //If no args, display usage + if len(args) < 1 { + if err := cmd.Usage(); err != nil { + fmt.Println(err) + } + return + } + status, _ := cmd.Flags().GetBool(interactive) + err := createDetectors(args, status) + if err != nil { + fmt.Println(commandCreate, "command failed") + fmt.Println("Reason:", err) + } + }, +} + +func generateTemplate() { + detector, _ := handler.GenerateAnomalyDetector() + fmt.Println(string(detector)) +} + +func init() { + esadCmd.AddCommand(createCmd) + createCmd.Flags().BoolP(interactive, "i", false, "Create Detectors in an interactive way") + createCmd.Flags().BoolP(generate, "g", false, "Outputs Detector's configuration") + +} + +func createDetectors(fileNames []string, status bool) error { + commandHandler, err := getCommandHandler() + if err != nil { + return err + } + for _, name := range fileNames { + err = handler.CreateAnomalyDetector(commandHandler, name, status) + if err != nil { + return err + } + } + return nil +} diff --git a/cli/cmd/delete.go b/cli/cmd/delete.go new file mode 100644 index 00000000..737d790c --- /dev/null +++ b/cli/cmd/delete.go @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cmd + +import ( + handler "esad/internal/handler/ad" + "fmt" + "github.com/spf13/cobra" +) + +const commandDelete = "delete" + +//deleteCmd deletes detectors based on id and name pattern. +//default input is name pattern, one can change this format to be id by passing --id flag +var deleteCmd = &cobra.Command{ + Use: commandDelete + " [flags] [list of detectors]", + Short: "Deletes detectors", + Long: `Deletes detectors based on value, use "" to make sure the name is not matched with pwd lists'`, + Run: func(cmd *cobra.Command, args []string) { + //If no args, display usage + if len(args) < 1 { + if err := cmd.Usage(); err != nil { + fmt.Println(err) + } + return + } + force, _ := cmd.Flags().GetBool("force") + detectorID, _ := cmd.Flags().GetBool("id") + action := handler.DeleteAnomalyDetectorByNamePattern + if detectorID { + action = handler.DeleteAnomalyDetectorByID + } + err := deleteDetectors(args, force, action) + if err != nil { + fmt.Println(commandDelete, "command failed") + fmt.Println("Reason:", err) + } + }, +} + +func init() { + esadCmd.AddCommand(deleteCmd) + deleteCmd.Flags().BoolP("force", "f", false, "Force deletion even if it is running") + deleteCmd.Flags().BoolP("id", "", false, "Input is id") +} + +func deleteDetectors(detectors []string, force bool, f func(*handler.Handler, string, bool) error) error { + commandHandler, err := getCommandHandler() + if err != nil { + return err + } + for _, detector := range detectors { + err = f(commandHandler, detector, force) + if err != nil { + return err + } + } + return nil +} diff --git a/cli/cmd/profile.go b/cli/cmd/profile.go new file mode 100644 index 00000000..539502a0 --- /dev/null +++ b/cli/cmd/profile.go @@ -0,0 +1,338 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cmd + +import ( + "errors" + "esad/internal/client" + entity "esad/internal/entity/ad" + "fmt" + "github.com/mitchellh/mapstructure" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "golang.org/x/crypto/ssh/terminal" + "os" + "strings" + "text/tabwriter" +) + +const ( + profileBaseCmdName = "profile" + createNewProfileCmdName = "create" + deleteNewProfileCmdName = "delete" + listProfileCmdName = "list" + esadProfile = "ESAD_PROFILE" +) + +//profilesCmd is main command for profile operations like list, create and delete +var profilesCmd = &cobra.Command{ + Use: profileBaseCmdName + " [flags] [command] [sub command]", + Short: "profile is a collection of settings and credentials that you can apply to an esad command", + Long: ` A named profile is a collection of settings and credentials that you can apply to an ESAD command. + When you specify a profile to run a command, the settings and credentials are used to run that command. + You can specify a profile in an environment variable (ESAD_PROFILE) which essentially acts as the default profile for commands if default doesn't exists. + The ESAD CLI supports using any of multiple named profiles that are stored in the config and credentials files.`, +} + +//createProfilesCmd creates profile interactively by prompting for name (distinct), user, endpoint, password. +var createProfilesCmd = &cobra.Command{ + Use: createNewProfileCmdName, + Short: "Create new named profile", + Long: `A named profile is a collection of settings and credentials that you can apply to an ESAD command.`, + Run: func(cmd *cobra.Command, args []string) { + createProfile() + }, +} + +//deleteProfileCmd deletes list of profiles passed as an arguments, provided profiles are already exists. +var deleteProfileCmd = &cobra.Command{ + Use: deleteNewProfileCmdName + " [list of profile names to be deleted]", + Short: "Delete named profiles", + Long: `Delete profile permanently from configuration files`, + Run: func(cmd *cobra.Command, args []string) { + //If no args, display usage + if len(args) < 1 { + if err := cmd.Usage(); err != nil { + fmt.Println(err) + } + return + } + deleteProfiles(args) + }, +} + +//listProfilesCmd lists profiles from config profile in tabular format. +var listProfilesCmd = &cobra.Command{ + Use: listProfileCmdName, + Short: "lists named profiles", + Long: `A named profile is a collection of settings and credentials that you can apply to an ESAD command.`, + Run: func(cmd *cobra.Command, args []string) { + displayProfiles() + }, +} + +func displayProfiles() { + config := &entity.Configuration{ + Profiles: []entity.Profile{}, + } + err := mapstructure.Decode(viper.AllSettings(), config) + if err != nil { + fmt.Println("failed to load config due to ", err) + return + } + const padding = 3 + w := tabwriter.NewWriter(os.Stdout, 0, 0, padding, ' ', tabwriter.AlignRight) + fmt.Fprintln(w, "Name\t\tUserName\t\tEndpoint-url\t") + fmt.Fprintf(w, "%s\t\t%s\t\t%s\t\n", "----", "--------", "------------") + for _, profile := range config.Profiles { + fmt.Fprintf(w, "%s\t\t%s\t\t%s\t\n", profile.Name, profile.Username, profile.Endpoint) + } + w.Flush() + +} + +func init() { + profilesCmd.AddCommand(createProfilesCmd) + profilesCmd.AddCommand(deleteProfileCmd) + esadCmd.AddCommand(profilesCmd) + profilesCmd.AddCommand(listProfilesCmd) + +} + +func createProfile() { + var name string + profiles := getProfiles() + for { + fmt.Printf("Enter profile's name: ") + name = getUserInputAsText(checkInputIsNotEmpty) + if _, ok := profiles[name]; !ok { + break + } + fmt.Println("profile", name, "already exists.") + } + fmt.Printf("ES Anomaly Detection Endpoint: ") + endpoint := getUserInputAsText(checkInputIsNotEmpty) + fmt.Printf("ES Anomaly Detection User: ") + user := getUserInputAsText(checkInputIsNotEmpty) + fmt.Printf("ES Anomaly Detection Password: ") + password := getUserInputAsMaskedText(checkInputIsNotEmpty) + newProfile := entity.Profile{ + Name: name, + Endpoint: endpoint, + Username: user, + Password: password, + } + var profileLists []entity.Profile + for _, profile := range profiles { + profileLists = append(profileLists, profile) + } + profileLists = append(profileLists, newProfile) + saveProfiles(profileLists) +} + +func saveProfiles(profiles []entity.Profile) { + viper.Set("profiles", profiles) + err := viper.WriteConfig() + if err == nil { + return + } + err = viper.SafeWriteConfig() + if err != nil { + fmt.Println("failed to save profile due to ", err) + } +} + +func getUserInputAsText(isValid func(string) bool) string { + var response string + //Ignore return value since validation is applied below + _, _ = fmt.Scanln(&response) + if !isValid(response) { + return getUserInputAsText(isValid) + } + return strings.TrimSpace(response) +} + +func checkInputIsNotEmpty(input string) bool { + if len(input) < 1 { + fmt.Print("value cannot be empty. Please enter non-empty value") + return false + } + return true +} + +func getUserInputAsMaskedText(isValid func(string) bool) string { + maskedValue, err := terminal.ReadPassword(0) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + value := fmt.Sprintf("%s", maskedValue) + if !isValid(value) { + return getUserInputAsMaskedText(isValid) + } + fmt.Println() + return value +} + +func deleteProfiles(names []string) { + profiles := getProfiles() + for _, name := range names { + if _, ok := profiles[name]; !ok { + fmt.Println("profile", name, "doesn't exists.") + continue + } + delete(profiles, name) + } + var remainingProfiles []entity.Profile + for _, profile := range profiles { + remainingProfiles = append(remainingProfiles, profile) + } + saveProfiles(remainingProfiles) +} + +func getProfiles() map[string]entity.Profile { + config := &entity.Configuration{ + Profiles: []entity.Profile{}, + } + err := mapstructure.Decode(viper.AllSettings(), config) + profiles := map[string]entity.Profile{} + if err != nil { + fmt.Println("failed to load config due to ", err) + return profiles + } + for _, profile := range config.Profiles { + profiles[profile.Name] = profile + } + return profiles +} + +func getValue(flagName string) (*string, error) { + val, err := esadCmd.Flags().GetString(flagName) + if err != nil { + return nil, err + } + if len(val) > 0 { + return &val, nil + } + return nil, err +} + +//isEmpty checks whether input is empty or not +func isEmpty(value *string) bool { + if value == nil { + return true + } + if len(*value) < 1 { + return true + } + return false +} + +//getUserProfile select's profile from the list of saved profile +/** +1. First priority is passed as parameters +2. Second priority is by flag --profile [name] +3. Third is get default profile from env or profile named "default" +*/ +func getUserProfile() (*client.UserConfig, error) { + + endpoint, err := getValue(FlagEndpoint) + if err != nil { + return nil, err + } + user, err := getValue(FlagUser) + if err != nil { + return nil, err + } + password, err := getValue(FlagPassword) + if err != nil { + return nil, err + } + profile, err := getProfileFromFlag() + if err != nil { + return nil, err + } + if profile == nil { + profile, err = getDefaultProfile() + if err != nil { + return nil, err + } + } + if profile == nil { + return nil, errors.New("connection details are not set. Set either by passing or set default profile") + } + if !isEmpty(endpoint) { + profile.Endpoint = *endpoint + } + if !isEmpty(user) { + profile.Username = *user + } + if !isEmpty(password) { + profile.Password = *password + } + return profile, nil +} + +func getProfileFromFlag() (*client.UserConfig, error) { + profileName, err := esadCmd.Flags().GetString(FlagProfile) + if err != nil { + return nil, err + } + if len(profileName) < 1 { + return nil, nil + } + profile, err := getProfileByName(profileName) + if err != nil { + return nil, err + } + return profile, nil + +} +func getDefaultProfile() (*client.UserConfig, error) { + + if profileName, ok := os.LookupEnv(esadProfile); ok { + return getProfileByName(profileName) + } + return getUserConfig("default") +} + +func getProfileByName(profileName string) (*client.UserConfig, error) { + userConfig, err := getUserConfig(profileName) + if err == nil && userConfig == nil { + return nil, fmt.Errorf("no profile found for name: %s", profileName) + } + return userConfig, err +} + +func getUserConfig(profileName string) (*client.UserConfig, error) { + config := &entity.Configuration{ + Profiles: []entity.Profile{}, + } + err := mapstructure.Decode(viper.AllSettings(), config) + if err != nil { + return nil, fmt.Errorf("failed to load config due to %s", err) + } + if len(config.Profiles) == 0 { + return nil, errors.New("no profiles found in config. Add profiles using add command") + } + for _, userConfig := range config.Profiles { + if userConfig.Name == profileName { + return &client.UserConfig{ + Endpoint: userConfig.Endpoint, + Username: userConfig.Username, + Password: userConfig.Password, + }, nil + } + } + return nil, nil +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go new file mode 100644 index 00000000..a85653c0 --- /dev/null +++ b/cli/cmd/root.go @@ -0,0 +1,112 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cmd + +import ( + "esad/internal/client" + controller "esad/internal/controller/ad" + esctrl "esad/internal/controller/es" + gateway "esad/internal/gateway/ad" + "esad/internal/gateway/es" + handler "esad/internal/handler/ad" + "esad/pkg" + "fmt" + "github.com/mitchellh/go-homedir" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "os" + "path/filepath" +) + +const ( + defaultFileType = "yaml" + defaultConfigFileName = "config" + cliName = "esad" + esadConfigFile = "ESAD_CONFIG_FILE" + FlagConfig = "config" + FlagUser = "user" + FlagPassword = "password" + FlagEndpoint = "endpoint" + FlagProfile = "profile" +) + +var cfgFile string +var profile string +var user string +var password string +var endpoint string + +var esadCmd = &cobra.Command{ + Use: cliName, + Short: "CLI to interact with Anomaly Detection plugin in your ES cluster", + Long: `The ESAD Command Line Interface is a tool to manage your Anomaly Detection Plugin`, + Version: pkg.VERSION, +} + +// Execute executes the root command. +func Execute() error { + return esadCmd.Execute() +} + +func init() { + cobra.OnInitialize(initConfig) + + esadCmd.PersistentFlags().StringVar(&cfgFile, FlagConfig, "", "config file (default is $HOME/.esad/config.yaml)") + esadCmd.PersistentFlags().StringVar(&user, FlagUser, "", "user to use. Overrides config/env settings.") + esadCmd.PersistentFlags().StringVar(&password, FlagPassword, "", "password to use. Overrides config/env settings.") + esadCmd.PersistentFlags().StringVar(&endpoint, FlagEndpoint, "", "endpoint to use. Overrides config/env settings.") + esadCmd.PersistentFlags().StringVar(&profile, FlagProfile, "", "Use a specific profile from your credential file.") + +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + + if cfgFile != "" { + viper.SetConfigFile(cfgFile) + } else if value, ok := os.LookupEnv(esadConfigFile); ok { + viper.SetConfigFile(value) + } else { + createDefaultConfigFile() + } + // If a config file is found, read it in. + viper.AutomaticEnv() // read in environment variables that match + _ = viper.ReadInConfig() +} + +func createDefaultConfigFile() { + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + folderPath := filepath.Join(home, fmt.Sprintf(".%s", cliName)) + if _, err := os.Stat(folderPath); os.IsNotExist(err) { + if err = os.Mkdir(folderPath, 0755); err != nil { + fmt.Println(err) + os.Exit(1) + } + } + viper.AddConfigPath(folderPath) + viper.SetConfigName(defaultConfigFileName) + viper.SetConfigType(defaultFileType) +} + +//GetHandler returns handler by wiring the dependency manually +func GetHandler(c *client.Client, u *client.UserConfig) *handler.Handler { + g := gateway.New(c, u) + esg := es.New(c, u) + esc := esctrl.New(esg) + ctr := controller.New(os.Stdin, esc, g) + return handler.New(ctr) +} diff --git a/cli/cmd/start_stop.go b/cli/cmd/start_stop.go new file mode 100644 index 00000000..19cb90a8 --- /dev/null +++ b/cli/cmd/start_stop.go @@ -0,0 +1,109 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package cmd + +import ( + "esad/internal/client" + "esad/internal/handler/ad" + "fmt" + "github.com/spf13/cobra" +) + +const ( + commandStart = "start" + commandStop = "stop" +) + +//startCmd stops detectors based on id and name pattern. +//default input is name pattern, one can change this format to be id by passing --id flag +var startCmd = &cobra.Command{ + Use: commandStart + " [flags] [list of detectors]", + Short: "Start detectors based on id or name pattern", + Long: `Start detectors based on pattern, use "" to make sure the name is not matched with pwd lists'`, + Run: func(cmd *cobra.Command, args []string) { + idStatus, _ := cmd.Flags().GetBool("id") + action := ad.StartAnomalyDetectorByNamePattern + if idStatus { + action = ad.StartAnomalyDetectorByID + } + err := execute(action, args) + if err != nil { + fmt.Println(commandStart, "command failed") + fmt.Println("Reason:", err) + } + }, +} + +//stopCmd stops detectors based on id and name pattern. +//default input is name pattern, one can change this format to be id by passing --id flag +var stopCmd = &cobra.Command{ + Use: commandStop + " [flags] [list of detectors]", + Short: "Stop detectors based on id or name pattern", + Long: `Stops detectors based on pattern, use "" to make sure the name is not matched with pwd lists'`, + Run: func(cmd *cobra.Command, args []string) { + //If no args, display usage + if len(args) < 1 { + if err := cmd.Usage(); err != nil { + fmt.Println(err) + } + return + } + idStatus, _ := cmd.Flags().GetBool("id") + action := ad.StopAnomalyDetectorByNamePattern + if idStatus { + action = ad.StopAnomalyDetectorByID + } + err := execute(action, args) + if err != nil { + fmt.Println(commandStop, "command failed") + fmt.Println("Reason:", err) + } + }, +} + +func init() { + esadCmd.AddCommand(startCmd) + startCmd.Flags().BoolP("name", "", true, "Input is name or pattern") + startCmd.Flags().BoolP("id", "", false, "Input is id") + esadCmd.AddCommand(stopCmd) + stopCmd.Flags().BoolP("name", "", true, "Input is name or pattern") + stopCmd.Flags().BoolP("id", "", false, "Input is id") +} + +func execute(f func(*ad.Handler, string) error, detectors []string) error { + // iterate over the arguments + // the first return value is index of fileNames, we can omit it using _ + commandHandler, err := getCommandHandler() + if err != nil { + return err + } + for _, detector := range detectors { + err := f(commandHandler, detector) + if err != nil { + return err + } + } + return nil +} + +func getCommandHandler() (*ad.Handler, error) { + newClient, err := client.New(nil) + if err != nil { + return nil, err + } + u, err := getUserProfile() + if err != nil { + return nil, err + } + return GetHandler(newClient, u), nil +} diff --git a/cli/esad b/cli/esad new file mode 100755 index 00000000..51a94b31 Binary files /dev/null and b/cli/esad differ diff --git a/cli/go.mod b/cli/go.mod new file mode 100644 index 00000000..06476d59 --- /dev/null +++ b/cli/go.mod @@ -0,0 +1,27 @@ +module esad + +go 1.14 + +require ( + github.com/cheggaaa/pb/v3 v3.0.4 + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/golang/mock v1.4.3 + github.com/gosuri/uilive v0.0.4 // indirect + github.com/hashicorp/go-retryablehttp v0.6.6 + github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/mapstructure v1.3.2 + github.com/pelletier/go-toml v1.8.0 // indirect + github.com/spf13/afero v1.3.1 // indirect + github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/cobra v1.0.0 + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.7.0 + github.com/stretchr/testify v1.5.1 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect + golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect + golang.org/x/text v0.3.3 // indirect + golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6 // indirect + gopkg.in/ini.v1 v1.57.0 // indirect +) diff --git a/cli/go.sum b/cli/go.sum new file mode 100644 index 00000000..d9f95d26 --- /dev/null +++ b/cli/go.sum @@ -0,0 +1,409 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= +github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cheggaaa/pb v1.0.28 h1:kWGpdAcSp3MxMU9CCHOwz/8V0kCHN4+9yQm2MzWuI98= +github.com/cheggaaa/pb v2.0.7+incompatible h1:gLKifR1UkZ/kLkda5gC0K6c8g+jU2sINPtBeOiNlMhU= +github.com/cheggaaa/pb/v3 v3.0.4 h1:QZEPYOj2ix6d5oEg63fbHmpolrnNiwjUsk+h74Yt4bM= +github.com/cheggaaa/pb/v3 v3.0.4/go.mod h1:7rgWxLrAUcFMkvJuv09+DYi7mMUYi8nO9iOWcvGJPfw= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY= +github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI= +github.com/gosuri/uiprogress v0.0.1 h1:0kpv/XY/qTmFWl/SkaJykZXrBBzwwadmW8fRb7RJSxw= +github.com/gosuri/uiprogress v0.0.1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= +github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= +github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.3.1 h1:GPTpEAuNr98px18yNQ66JllNil98wfRZ/5Ukny8FeQA= +github.com/spf13/afero v1.3.1/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6 h1:nULzSsKgihxFGLnQFv2T7lE5vIhOtg8ZPpJHapEt7o0= +golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= +gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/cli/internal/client/config.go b/cli/internal/client/config.go new file mode 100644 index 00000000..e84e96d1 --- /dev/null +++ b/cli/internal/client/config.go @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package client + +//UserConfig represents User configurations +type UserConfig struct { + Endpoint string `json:"endpoint"` + Username string `json:"username"` + Password string `json:"password"` +} diff --git a/cli/internal/client/mocks/mock_client.go b/cli/internal/client/mocks/mock_client.go new file mode 100644 index 00000000..73d53d7a --- /dev/null +++ b/cli/internal/client/mocks/mock_client.go @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package mocks + +import ( + "esad/internal/client" + "fmt" + "net/http" +) + +// RoundTripFunc . +type RoundTripFunc func(req *http.Request) *http.Response + +// RoundTrip . +func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +//NewTestClient returns *http.Client with Transport replaced to avoid making real calls +func NewTestClient(fn RoundTripFunc) *client.Client { + c, err := client.New(fn) + if err == nil { + fmt.Println("Fatal: failed to get client") + } + return c +} diff --git a/cli/internal/client/request.go b/cli/internal/client/request.go new file mode 100644 index 00000000..7bfd9962 --- /dev/null +++ b/cli/internal/client/request.go @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package client + +import ( + "crypto/tls" + "github.com/hashicorp/go-retryablehttp" + "net/http" +) + +//Client is an Abstraction for actual client +type Client struct { + HTTPClient *retryablehttp.Client +} + +//NewDefaultClient return new instance of client +func NewDefaultClient(tripper http.RoundTripper) (*Client, error) { + + client := retryablehttp.NewClient() + client.HTTPClient.Transport = tripper + client.Logger = nil + return &Client{ + HTTPClient: client, + }, nil +} + +//New takes transport and uses accordingly +func New(tripper http.RoundTripper) (*Client, error) { + if tripper == nil { + tripper = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + defaultClient, err := NewDefaultClient(tripper) + if err != nil { + return nil, err + } + return defaultClient, err +} diff --git a/cli/internal/controller/ad/ad.go b/cli/internal/controller/ad/ad.go new file mode 100644 index 00000000..9a3f4f05 --- /dev/null +++ b/cli/internal/controller/ad/ad.go @@ -0,0 +1,471 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package ad + +import ( + "context" + "encoding/json" + "errors" + "esad/internal/controller/es" + entity "esad/internal/entity/ad" + "esad/internal/gateway/ad" + cmapper "esad/internal/mapper" + mapper "esad/internal/mapper/ad" + "fmt" + "github.com/cheggaaa/pb/v3" + "io" + "os" + "strings" +) + +//go:generate mockgen -destination=mocks/mock_ad.go -package=mocks . Controller + +//Controller is an interface for the AD plugin controllers +type Controller interface { + StartDetector(context.Context, string) error + StopDetector(context.Context, string) error + DeleteDetector(context.Context, string, bool, bool) error + CreateAnomalyDetector(context.Context, entity.CreateDetectorRequest) (*string, error) + CreateMultiEntityAnomalyDetector(ctx context.Context, request entity.CreateDetectorRequest, interactive bool, display bool) ([]string, error) + SearchDetectorByName(context.Context, string) ([]entity.Detector, error) + StartDetectorByName(context.Context, string, bool) error + StopDetectorByName(context.Context, string, bool) error + DeleteDetectorByName(context.Context, string, bool, bool) error +} + +type controller struct { + reader io.Reader + gateway ad.Gateway + esCtrl es.Controller +} + +//New returns new Controller instance +func New(reader io.Reader, esCtrl es.Controller, gateway ad.Gateway) Controller { + return &controller{ + reader, + gateway, + esCtrl, + } +} + +func validateCreateRequest(r entity.CreateDetectorRequest) error { + if len(r.Name) < 1 { + return fmt.Errorf("name field cannot be empty") + } + if len(r.Features) < 1 { + return fmt.Errorf("features cannot be empty") + } + if len(r.Index) < 1 || len(r.Index[0]) < 1 { + return fmt.Errorf("index field cannot be empty and it should have at least one valid index") + } + if len(r.Interval) < 1 { + return fmt.Errorf("interval field cannot be empty") + } + return nil +} + +//StartDetector start detector based on DetectorID +func (c controller) StartDetector(ctx context.Context, ID string) error { + if len(ID) < 1 { + return fmt.Errorf("detector Id: %s cannot be empty", ID) + } + err := c.gateway.StartDetector(ctx, ID) + if err != nil { + return err + } + return nil +} + +//StopDetector stops detector based on DetectorID +func (c controller) StopDetector(ctx context.Context, ID string) error { + if len(ID) < 1 { + return fmt.Errorf("detector Id: %s cannot be empty", ID) + } + _, err := c.gateway.StopDetector(ctx, ID) + if err != nil { + return err + } + return nil +} + +func (c controller) askForConfirmation(message *string) bool { + + if message == nil { + return true + } + if len(*message) > 0 { + fmt.Print(*message) + } + + var response string + _, err := fmt.Fscanln(c.reader, &response) + if err != nil { + //Exit if for some reason, we are not able to accept user input + fmt.Printf("failed to accept value from user due to %s", err) + os.Exit(1) + } + switch strings.ToLower(response) { + case "y", "yes": + return true + case "n", "no": + return false + default: + fmt.Printf("I'm sorry but I didn't get what you meant, please type (y)es or (n)o and then press enter:") + return c.askForConfirmation(cmapper.StringToStringPtr("")) + } +} + +//DeleteDetector deletes detector based on DetectorID, if force is enabled, it stops before deletes +func (c controller) DeleteDetector(ctx context.Context, id string, interactive bool, force bool) error { + if len(id) < 1 { + return fmt.Errorf("detector Id cannot be empty") + } + proceed := true + if interactive { + proceed = c.askForConfirmation( + cmapper.StringToStringPtr( + fmt.Sprintf( + "esad will delete detector: %s . Do you want to proceed? please type (y)es or (n)o and then press enter:", + id, + ), + ), + ) + } + if !proceed { + return nil + } + if force { + res, err := c.gateway.StopDetector(ctx, id) + if err != nil { + return err + } + if interactive { + fmt.Println(*res) + } + + } + err := c.gateway.DeleteDetector(ctx, id) + if err != nil { + return err + } + return nil +} +func processEntityError(err error) error { + var c entity.CreateError + data := fmt.Sprintf("%v", err) + responseErr := json.Unmarshal([]byte(data), &c) + if responseErr != nil { + return err + } + if len(c.Error.Reason) > 0 { + return errors.New(c.Error.Reason) + } + return err +} + +//CreateAnomalyDetector creates detector based on user request +func (c controller) CreateAnomalyDetector(ctx context.Context, r entity.CreateDetectorRequest) (*string, error) { + + if err := validateCreateRequest(r); err != nil { + return nil, err + } + payload, err := mapper.MapToCreateDetector(r) + if err != nil { + return nil, err + } + response, err := c.gateway.CreateDetector(ctx, payload) + if err != nil { + return nil, processEntityError(err) + } + var data map[string]interface{} + _ = json.Unmarshal(response, &data) + + detectorID := fmt.Sprintf("%s", data["_id"]) + if !r.Start { + return cmapper.StringToStringPtr(detectorID), nil + } + + err = c.StartDetector(ctx, detectorID) + if err != nil { + return nil, fmt.Errorf("detector is created with id: %s, but failed to start due to %v", detectorID, err) + } + return cmapper.StringToStringPtr(detectorID), nil +} + +func (c controller) cleanupCreatedDetectors(ctx context.Context, detectors []entity.Detector) { + + if len(detectors) < 1 { + return + } + var unDeleted []entity.Detector + for _, d := range detectors { + err := c.DeleteDetector(ctx, d.ID, false, true) + if err != nil { + unDeleted = append(unDeleted, d) + } + } + if len(unDeleted) > 0 { + var names []string + for _, d := range unDeleted { + names = append(names, d.Name) + } + fmt.Println("failed to clean-up created detectors. Please clean up manually following detectors: ", strings.Join(names, ", ")) + } +} + +func getFilterValues(ctx context.Context, request entity.CreateDetectorRequest, c controller) ([]interface{}, error) { + var filterValues []interface{} + for _, index := range request.Index { + v, err := c.esCtrl.GetDistinctValues(ctx, index, *request.PartitionField) + if err != nil { + return nil, err + } + filterValues = append(filterValues, v...) + } + return filterValues, nil +} + +func createProgressBar(total int) *pb.ProgressBar { + template := `{{string . "prefix"}}{{percent . }} {{bar . "[" "=" ">" "_" "]" }} {{counters . }}{{string . "suffix"}}` + bar := pb.New(total) + bar.SetTemplateString(template) + bar.SetMaxWidth(65) + bar.Start() + return bar +} + +func buildCompoundQuery(field string, value interface{}, userFilter json.RawMessage) json.RawMessage { + + leaf1 := []byte(fmt.Sprintf(`{ + "bool": { + "filter": { + "term": { + "%s" : "%v" + } + } + } + }`, field, value)) + if userFilter == nil { + return leaf1 + } + marshal, _ := json.Marshal(entity.Query{ + Bool: entity.Bool{ + Must: []json.RawMessage{ + leaf1, userFilter, + }, + }, + }) + return marshal +} + +//CreateMultiEntityAnomalyDetector creates multiple entity detector based on partition_by field +func (c controller) CreateMultiEntityAnomalyDetector(ctx context.Context, request entity.CreateDetectorRequest, interactive bool, display bool) ([]string, error) { + if request.PartitionField == nil || len(*request.PartitionField) < 1 { + result, err := c.CreateAnomalyDetector(ctx, request) + if err != nil { + return nil, err + } + return []string{*result}, err + } + filterValues, err := getFilterValues(ctx, request, c) + if err != nil { + return nil, err + } + if len(filterValues) < 1 { + return nil, fmt.Errorf( + "failed to get values for partition field: %s, check whether any data is available in index %s", + *request.PartitionField, + request.Index, + ) + } + proceed := true + if interactive { + proceed = c.askForConfirmation( + cmapper.StringToStringPtr( + fmt.Sprintf( + "esad will create %d detector(s). Do you want to proceed? please type (y)es or (n)o and then press enter:", + len(filterValues), + ), + ), + ) + } + if !proceed { + return nil, nil + } + var bar *pb.ProgressBar + if display { + bar = createProgressBar(len(filterValues)) + } + var detectors []string + name := request.Name + filter := request.Filter + var createdDetectors []entity.Detector + for _, value := range filterValues { + request.Filter = buildCompoundQuery(*request.PartitionField, value, filter) + request.Name = fmt.Sprintf("%s-%s", name, value) + result, err := c.CreateAnomalyDetector(ctx, request) + if err != nil { + c.cleanupCreatedDetectors(ctx, createdDetectors) + return nil, err + } + createdDetectors = append(createdDetectors, entity.Detector{ + ID: *result, + Name: request.Name, + }) + detectors = append(detectors, request.Name) + if bar != nil { + bar.Increment() + } + } + if bar != nil { + bar.Finish() + } + return detectors, nil +} + +//SearchDetectorByName searches detector based on name +func (c controller) SearchDetectorByName(ctx context.Context, name string) ([]entity.Detector, error) { + if len(name) < 1 { + return nil, fmt.Errorf("detector name cannot be empty") + } + payload := entity.SearchRequest{ + Query: entity.SearchQuery{ + Match: entity.Match{ + Name: name, + }, + }, + } + response, err := c.gateway.SearchDetector(ctx, payload) + if err != nil { + return nil, err + } + detectors, err := mapper.MapToDetectors(response, name) + if err != nil { + return nil, err + } + return detectors, nil +} + +func (c controller) getDetectorsToProcess(ctx context.Context, method string, pattern string) ([]entity.Detector, error) { + if len(pattern) < 1 { + return nil, fmt.Errorf("name cannot be empty") + } + //Search Detector By Name to get ID + matchedDetectors, err := c.SearchDetectorByName(ctx, pattern) + if err != nil { + return nil, err + } + if len(matchedDetectors) < 1 { + fmt.Printf("no detectors matched by name %s\n", pattern) + return nil, nil + } + fmt.Printf("%d detectors matched by name %s\n", len(matchedDetectors), pattern) + for _, detector := range matchedDetectors { + fmt.Println(detector.Name) + } + + proceed := c.askForConfirmation( + cmapper.StringToStringPtr( + fmt.Sprintf("esad will %s above matched detector(s). Do you want to proceed? please type (y)es or (n)o and then press enter:", method), + ), + ) + if !proceed { + return nil, nil + } + return matchedDetectors, nil +} + +func (c controller) processDetectorByAction(ctx context.Context, pattern string, action string, f func(c context.Context, s string) error, display bool) error { + matchedDetectors, err := c.getDetectorsToProcess(ctx, action, pattern) + if err != nil { + return err + } + if matchedDetectors == nil { + return nil + } + var bar *pb.ProgressBar + if display { + bar = createProgressBar(len(matchedDetectors)) + } + var failedDetectors []string + for _, detector := range matchedDetectors { + err := f(ctx, detector.ID) + if err != nil { + failedDetectors = append(failedDetectors, fmt.Sprintf("%s \t Reason: %s", detector.Name, err)) + continue + } + if bar != nil { + bar.Increment() + } + } + if bar != nil { + bar.Finish() + } + if len(failedDetectors) > 0 { + fmt.Printf("\nfailed to %s %d following detector(s)\n", action, len(failedDetectors)) + for _, detector := range failedDetectors { + fmt.Println(detector) + } + } + return nil +} + +//StartDetectorByName starts detector based on name pattern. It first calls SearchDetectorByName and then +// gets lists of detectorId and call StartDetector to start individual detectors +func (c controller) StartDetectorByName(ctx context.Context, pattern string, display bool) error { + return c.processDetectorByAction(ctx, pattern, "start", c.StartDetector, display) +} + +//StopDetectorByName stops detector based on name pattern. It first calls SearchDetectorByName and then +// gets lists of detectorId and call StopDetector to stop individual detectors +func (c controller) StopDetectorByName(ctx context.Context, pattern string, display bool) error { + return c.processDetectorByAction(ctx, pattern, "stop", c.StopDetector, display) +} + +//DeleteDetectorByName deletes detector based on name pattern. It first calls SearchDetectorByName and then +// gets lists of detectorId and call DeleteDetector to delete individual detectors +func (c controller) DeleteDetectorByName(ctx context.Context, name string, force bool, display bool) error { + matchedDetectors, err := c.getDetectorsToProcess(ctx, "delete", name) + if err != nil { + return err + } + if matchedDetectors == nil { + return nil + } + var bar *pb.ProgressBar + if display { + bar = createProgressBar(len(matchedDetectors)) + } + var failedDetectors []string + for _, detector := range matchedDetectors { + err := c.DeleteDetector(ctx, detector.ID, false, force) + if err != nil { + failedDetectors = append(failedDetectors, fmt.Sprintf("%s \t Reason: %s", detector.Name, err)) + continue + } + if bar != nil { + bar.Increment() + } + } + if bar != nil { + bar.Finish() + } + if len(failedDetectors) > 0 { + fmt.Printf("failed to delete %d following detector(s)\n", len(failedDetectors)) + for _, detector := range failedDetectors { + fmt.Println(detector) + } + } + return nil + +} diff --git a/cli/internal/controller/ad/ad_test.go b/cli/internal/controller/ad/ad_test.go new file mode 100644 index 00000000..22590459 --- /dev/null +++ b/cli/internal/controller/ad/ad_test.go @@ -0,0 +1,726 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package ad + +import ( + "bytes" + "context" + "encoding/json" + "errors" + esmockctrl "esad/internal/controller/es/mocks" + entity "esad/internal/entity/ad" + adgateway "esad/internal/gateway/ad/mocks" + mapper2 "esad/internal/mapper" + "fmt" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +const mockDetectorID = "m4ccEnIBTXsGi3mvMt9p" +const mockDetectorName = "detector" + +func helperLoadBytes(t *testing.T, name string) []byte { + path := filepath.Join("testdata", name) // relative path + contents, err := ioutil.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return contents +} +func helperConvertToInterface(input []string) []interface{} { + s := make([]interface{}, len(input)) + for i, v := range input { + s[i] = v + } + return s +} + +func getRawFilter() []byte { + return []byte(`{ + "bool": { + "filter": [{ + "exists": { + "field": "value", + "boost": 1 + } + }], + "adjust_pure_negative": true, + "boost": 1 + } + }`) +} +func getFinalFilter(additionalFilters ...json.RawMessage) []byte { + + filter1 := []byte(`{ + "bool": { + "filter": { + "term": { + "ip" : "localhost" + } + } + } + }`) + + if len(additionalFilters) < 1 { + return filter1 + } + filter := entity.Query{ + Bool: entity.Bool{ + Must: []json.RawMessage{ + filter1, + }, + }, + } + filter.Bool.Must = append(filter.Bool.Must, additionalFilters...) + marshal, _ := json.Marshal(filter) + return marshal +} + +func getCreateDetectorRequest() entity.CreateDetectorRequest { + return entity.CreateDetectorRequest{ + Name: "testdata-detector", + Description: "Test detector", + TimeField: "timestamp", + Index: []string{"order*"}, + Features: []entity.FeatureRequest{{ + AggregationType: []string{"sum"}, + Enabled: true, + Field: []string{"value"}, + }}, + Filter: getRawFilter(), + Interval: "1m", + Delay: "1m", + Start: true, + PartitionField: mapper2.StringToStringPtr("ip"), + } +} +func getRawFeatureAggregation() []byte { + return []byte(`{ + "sum_value": { + "sum": { + "field": "value" + } + } + }`) +} +func getCreateDetector() *entity.CreateDetector { + return &entity.CreateDetector{ + Name: "testdata-detector", + Description: "Test detector", + TimeField: "timestamp", + Index: []string{"order*"}, + Features: []entity.Feature{ + { + Name: "sum_value", + Enabled: true, + AggregationQuery: getRawFeatureAggregation(), + }, + }, + Filter: getRawFilter(), + Interval: entity.Interval{ + Period: entity.Period{ + Duration: 1, + Unit: "Minutes", + }, + }, + Delay: entity.Interval{ + Period: entity.Period{ + Duration: 1, + Unit: "Minutes", + }, + }, + } +} +func TestController_StartDetector(t *testing.T) { + t.Run("start empty detector", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctx := context.Background() + ctrl := New(os.Stdin, mockESController, mockADGateway) + assert.Error(t, ctrl.StartDetector(ctx, "")) + }) + t.Run("start detector gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().StartDetector(ctx, "detectorID").Return(errors.New("no connection")) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + assert.Error(t, ctrl.StartDetector(ctx, "detectorID")) + }) + t.Run("start detector", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().StartDetector(ctx, "detectorID").Return(nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + assert.NoError(t, ctrl.StartDetector(ctx, "detectorID")) + }) +} + +func TestController_StopDetector(t *testing.T) { + t.Run("stop empty detector", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctx := context.Background() + ctrl := New(os.Stdin, mockESController, mockADGateway) + err := ctrl.StopDetector(ctx, "") + assert.Error(t, err) + }) + t.Run("stop detector gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().StopDetector(ctx, "detectorID").Return(nil, errors.New("gateway failed")) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + err := ctrl.StopDetector(ctx, "detectorID") + assert.Error(t, err) + }) + t.Run("stop detector", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().StopDetector(ctx, "detectorID").Return(mapper2.StringToStringPtr("Stopped Detector"), nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + err := ctrl.StopDetector(ctx, "detectorID") + assert.NoError(t, err) + }) +} + +func TestController_CreateAnomalyDetector(t *testing.T) { + t.Run("gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().CreateDetector(ctx, getCreateDetector()).Return(nil, errors.New("failed to connect")) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + _, err := ctrl.CreateAnomalyDetector(ctx, r) + assert.EqualError(t, err, "failed to connect") + }) + t.Run("entity failed to create", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().CreateDetector(ctx, getCreateDetector()).Return(nil, errors.New(string(helperLoadBytes(t, "create_failed_response.json")))) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + _, err := ctrl.CreateAnomalyDetector(ctx, r) + assert.EqualError(t, err, "Cannot create anomaly detector with name [testdata-detector] as it's already used by detector [wR_1XXMBs3q1IVz33Sk-]") + }) + t.Run("entity succeeded without starting", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + r.Start = false + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().CreateDetector(ctx, getCreateDetector()).Return(helperLoadBytes(t, "create_response.json"), nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + detectorID, err := ctrl.CreateAnomalyDetector(ctx, r) + assert.NoError(t, err) + assert.NotNil(t, detectorID) + assert.EqualValues(t, mockDetectorID, *detectorID) + }) + t.Run("entity succeeded", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().CreateDetector(ctx, getCreateDetector()).Return(helperLoadBytes(t, "create_response.json"), nil) + mockADGateway.EXPECT().StartDetector(ctx, mockDetectorID).Return(nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + detectorID, err := ctrl.CreateAnomalyDetector(ctx, r) + assert.NoError(t, err) + assert.NotNil(t, detectorID) + assert.EqualValues(t, mockDetectorID, *detectorID) + }) + t.Run("entity failed because of failed to start", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().CreateDetector(ctx, getCreateDetector()).Return(helperLoadBytes(t, "create_response.json"), nil) + mockADGateway.EXPECT().StartDetector(ctx, mockDetectorID).Return(errors.New("error")) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + _, err := ctrl.CreateAnomalyDetector(ctx, r) + assert.EqualError(t, err, fmt.Sprintf("detector is created with id: %s, but failed to start due to error", mockDetectorID)) + }) +} + +func TestController_DeleteDetector(t *testing.T) { + t.Run("invalid detector Id", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetector(ctx, "", false, false) + assert.EqualError(t, err, "detector Id cannot be empty") + }) + t.Run("delete gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().DeleteDetector(ctx, mockDetectorID).Return(errors.New("gateway failed")) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetector(ctx, mockDetectorID, false, false) + assert.EqualError(t, err, "gateway failed") + }) + t.Run("delete gateway succeeded", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().DeleteDetector(ctx, mockDetectorID).Return(nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetector(ctx, mockDetectorID, false, false) + assert.NoError(t, err) + }) + t.Run("stop gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().StopDetector(ctx, mockDetectorID).Return(nil, errors.New("failed")) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetector(ctx, mockDetectorID, false, true) + assert.EqualError(t, err, "failed") + }) + t.Run("stop gateway succeeded", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().StopDetector(ctx, mockDetectorID).Return(mapper2.StringToStringPtr("Stopped Detector"), nil) + mockADGateway.EXPECT().DeleteDetector(ctx, mockDetectorID).Return(nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetector(ctx, mockDetectorID, false, true) + assert.NoError(t, err) + }) + + t.Run("cancelled from user", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockESController := esmockctrl.NewMockController(mockCtrl) + var stdin bytes.Buffer + stdin.Write([]byte("no\n")) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetector(ctx, mockDetectorID, true, false) + assert.NoError(t, err) + }) + t.Run("agreed by user", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockESController := esmockctrl.NewMockController(mockCtrl) + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + mockADGateway.EXPECT().DeleteDetector(ctx, mockDetectorID).Return(nil) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetector(ctx, mockDetectorID, true, false) + assert.NoError(t, err) + }) +} + +func TestController_CreateMultiEntityAnomalyDetector(t *testing.T) { + t.Run("create one detector", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + gatewayRequest := getCreateDetector() + gatewayRequest.Name = gatewayRequest.Name + "-" + "localhost" + gatewayRequest.Filter = getFinalFilter(getRawFilter()) + mockADGateway.EXPECT().CreateDetector(ctx, gatewayRequest).Return(helperLoadBytes(t, "create_response.json"), nil) + mockADGateway.EXPECT().StartDetector(ctx, mockDetectorID).Return(nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + mockESController.EXPECT().GetDistinctValues(ctx, r.Index[0], *r.PartitionField).Return(helperConvertToInterface([]string{"localhost"}), nil) + ctrl := New(os.Stdin, mockESController, mockADGateway) + detectorID, err := ctrl.CreateMultiEntityAnomalyDetector(ctx, r, false, false) + assert.NoError(t, err) + assert.NotNil(t, detectorID) + assert.EqualValues(t, gatewayRequest.Name, detectorID[0]) + }) + t.Run("create detector failed due to second detector", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + gatewayRequest := getCreateDetector() + gatewayRequest.Name = gatewayRequest.Name + "-" + "localhost" + gatewayRequest.Filter = getFinalFilter(getRawFilter()) + mockADGateway.EXPECT().CreateDetector(ctx, gatewayRequest).Return(helperLoadBytes(t, "create_response.json"), nil) + mockADGateway.EXPECT().CreateDetector(ctx, gatewayRequest).Return(nil, errors.New(string(helperLoadBytes(t, "create_failed_response.json")))) + mockADGateway.EXPECT().StartDetector(ctx, mockDetectorID).Return(nil) + mockADGateway.EXPECT().StopDetector(ctx, mockDetectorID).Return(mapper2.StringToStringPtr("stopped"), nil) + mockADGateway.EXPECT().DeleteDetector(ctx, mockDetectorID).Return(nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + mockESController.EXPECT().GetDistinctValues(ctx, r.Index[0], *r.PartitionField).Return(helperConvertToInterface([]string{"localhost", "localhost"}), nil) + ctrl := New(os.Stdin, mockESController, mockADGateway) + _, err := ctrl.CreateMultiEntityAnomalyDetector(ctx, r, false, false) + assert.EqualError(t, err, "Cannot create anomaly detector with name [testdata-detector] as it's already used by detector [wR_1XXMBs3q1IVz33Sk-]") + }) + t.Run("create one detector with no filter", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + r.Filter = nil + mockADGateway := adgateway.NewMockGateway(mockCtrl) + gatewayRequest := getCreateDetector() + gatewayRequest.Name = gatewayRequest.Name + "-" + "localhost" + gatewayRequest.Filter = getFinalFilter() + mockADGateway.EXPECT().CreateDetector(ctx, gatewayRequest).Return(helperLoadBytes(t, "create_response.json"), nil) + mockADGateway.EXPECT().StartDetector(ctx, mockDetectorID).Return(nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + mockESController.EXPECT().GetDistinctValues(ctx, r.Index[0], *r.PartitionField).Return(helperConvertToInterface([]string{"localhost"}), nil) + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + ctrl := New(&stdin, mockESController, mockADGateway) + detectorID, err := ctrl.CreateMultiEntityAnomalyDetector(ctx, r, true, false) + assert.NoError(t, err) + assert.NotNil(t, detectorID) + assert.EqualValues(t, gatewayRequest.Name, detectorID[0]) + }) + t.Run("create one detector interactive rejected", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + gatewayRequest := getCreateDetector() + gatewayRequest.Name = gatewayRequest.Name + "-" + "localhost" + gatewayRequest.Filter = getFinalFilter(getRawFilter()) + mockESController := esmockctrl.NewMockController(mockCtrl) + mockESController.EXPECT().GetDistinctValues(ctx, r.Index[0], *r.PartitionField).Return(helperConvertToInterface([]string{"localhost"}), nil) + var stdin bytes.Buffer + stdin.Write([]byte("no\n")) + ctrl := New(&stdin, mockESController, mockADGateway) + detectorID, err := ctrl.CreateMultiEntityAnomalyDetector(ctx, r, true, false) + assert.NoError(t, err) + assert.Nil(t, detectorID) + }) + t.Run("create detector failed since no values in partition field", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + gatewayRequest := getCreateDetector() + gatewayRequest.Name = gatewayRequest.Name + "-" + "localhost" + gatewayRequest.Filter = getFinalFilter(getRawFilter()) + mockESController := esmockctrl.NewMockController(mockCtrl) + mockESController.EXPECT().GetDistinctValues(ctx, r.Index[0], *r.PartitionField).Return(nil, nil) + ctrl := New(os.Stdin, mockESController, mockADGateway) + _, err := ctrl.CreateMultiEntityAnomalyDetector(ctx, r, false, false) + assert.EqualError(t, err, "failed to get values for partition field: ip, check whether any data is available in index [order*]") + }) + t.Run("create detector failed since es controller failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + gatewayRequest := getCreateDetector() + gatewayRequest.Name = gatewayRequest.Name + "-" + "localhost" + gatewayRequest.Filter = getFinalFilter(getRawFilter()) + mockESController := esmockctrl.NewMockController(mockCtrl) + mockESController.EXPECT().GetDistinctValues(ctx, r.Index[0], *r.PartitionField).Return(nil, errors.New("failed")) + ctrl := New(os.Stdin, mockESController, mockADGateway) + _, err := ctrl.CreateMultiEntityAnomalyDetector(ctx, r, false, false) + assert.EqualError(t, err, "failed") + }) + t.Run("create detector failed due to second detector, failed to cleanup automatically", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + r := getCreateDetectorRequest() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + gatewayRequest := getCreateDetector() + gatewayRequest.Name = gatewayRequest.Name + "-" + "localhost" + gatewayRequest.Filter = getFinalFilter(getRawFilter()) + mockADGateway.EXPECT().CreateDetector(ctx, gatewayRequest).Return(helperLoadBytes(t, "create_response.json"), nil) + mockADGateway.EXPECT().CreateDetector(ctx, gatewayRequest).Return(nil, errors.New(string(helperLoadBytes(t, "create_failed_response.json")))) + mockADGateway.EXPECT().StartDetector(ctx, mockDetectorID).Return(nil) + mockADGateway.EXPECT().StopDetector(ctx, mockDetectorID).Return(mapper2.StringToStringPtr("stopped"), nil) + mockADGateway.EXPECT().DeleteDetector(ctx, mockDetectorID).Return(errors.New("failed")) + mockESController := esmockctrl.NewMockController(mockCtrl) + mockESController.EXPECT().GetDistinctValues(ctx, r.Index[0], *r.PartitionField).Return(helperConvertToInterface([]string{"localhost", "localhost"}), nil) + ctrl := New(os.Stdin, mockESController, mockADGateway) + _, err := ctrl.CreateMultiEntityAnomalyDetector(ctx, r, false, false) + assert.EqualError(t, err, "Cannot create anomaly detector with name [testdata-detector] as it's already used by detector [wR_1XXMBs3q1IVz33Sk-]") + }) +} + +func getSearchPayload(name string) entity.SearchRequest { + return entity.SearchRequest{ + Query: entity.SearchQuery{ + Match: entity.Match{ + Name: name, + }, + }, + } +} + +func TestController_StopDetectorByName(t *testing.T) { + t.Run("stop empty detector", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctx := context.Background() + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.StopDetectorByName(ctx, "", false) + assert.Error(t, err) + }) + t.Run("stop detector gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload("detector")).Return( + helperLoadBytes(t, "search_response.json"), nil) + mockADGateway.EXPECT().StopDetector(ctx, "detectorID").Return(nil, errors.New("gateway failed")) + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.StopDetectorByName(ctx, "detector", false) + assert.NoError(t, err) + }) + t.Run("search detector gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload("detector")).Return(nil, errors.New("gateway failed")) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + err := ctrl.StopDetectorByName(ctx, "detector", false) + assert.Error(t, err) + }) + t.Run("stop detector", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload("detector")).Return( + helperLoadBytes(t, "search_response.json"), nil) + mockADGateway.EXPECT().StopDetector(ctx, "detectorID").Return(mapper2.StringToStringPtr("Stopped Detector"), nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.StopDetectorByName(ctx, "detector", false) + assert.NoError(t, err) + }) +} + +func TestController_StartDetectorByName(t *testing.T) { + t.Run("start empty detector", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctx := context.Background() + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.StartDetectorByName(ctx, "", false) + assert.Error(t, err) + }) + t.Run("start detector gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload("detector")).Return( + helperLoadBytes(t, "search_response.json"), nil) + mockADGateway.EXPECT().StartDetector(ctx, "detectorID").Return(errors.New("gateway failed")) + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.StartDetectorByName(ctx, "detector", false) + assert.NoError(t, err) + }) + t.Run("search detector gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload("detector")).Return(nil, errors.New("gateway failed")) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + err := ctrl.StartDetectorByName(ctx, "detector", false) + assert.Error(t, err) + }) + t.Run("start detector", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload("detector")).Return( + helperLoadBytes(t, "search_response.json"), nil) + mockADGateway.EXPECT().StartDetector(ctx, "detectorID").Return(nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.StartDetectorByName(ctx, "detector", false) + assert.NoError(t, err) + }) +} + +func TestController_DeleteDetectorByName(t *testing.T) { + t.Run("invalid detector name", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockESController := esmockctrl.NewMockController(mockCtrl) + ctrl := New(os.Stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetectorByName(ctx, "", false, false) + assert.EqualError(t, err, "name cannot be empty") + }) + t.Run("delete gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload(mockDetectorName)).Return( + helperLoadBytes(t, "search_response.json"), nil) + mockADGateway.EXPECT().DeleteDetector(ctx, "detectorID").Return(errors.New("gateway failed")) + mockESController := esmockctrl.NewMockController(mockCtrl) + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetectorByName(ctx, mockDetectorName, false, false) + assert.NoError(t, err) + }) + t.Run("delete gateway succeeded", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload(mockDetectorName)).Return( + helperLoadBytes(t, "search_response.json"), nil) + mockADGateway.EXPECT().DeleteDetector(ctx, "detectorID").Return(nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetectorByName(ctx, mockDetectorName, false, false) + assert.NoError(t, err) + }) + t.Run("stop gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload(mockDetectorName)).Return( + helperLoadBytes(t, "search_response.json"), nil) + mockADGateway.EXPECT().StopDetector(ctx, "detectorID").Return(nil, errors.New("failed")) + mockESController := esmockctrl.NewMockController(mockCtrl) + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetectorByName(ctx, mockDetectorName, true, false) + assert.NoError(t, err) + }) + t.Run("stop gateway succeeded", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload(mockDetectorName)).Return( + helperLoadBytes(t, "search_response.json"), nil) + mockADGateway.EXPECT().StopDetector(ctx, "detectorID").Return(mapper2.StringToStringPtr("Stopped Detector"), nil) + mockADGateway.EXPECT().DeleteDetector(ctx, "detectorID").Return(nil) + mockESController := esmockctrl.NewMockController(mockCtrl) + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetectorByName(ctx, mockDetectorName, true, false) + assert.NoError(t, err) + }) + + t.Run("cancelled from user", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockESController := esmockctrl.NewMockController(mockCtrl) + var stdin bytes.Buffer + stdin.Write([]byte("no\n")) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload(mockDetectorName)).Return( + helperLoadBytes(t, "search_response.json"), nil) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetectorByName(ctx, mockDetectorName, true, false) + assert.NoError(t, err) + }) + t.Run("agreed by user", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + ctx := context.Background() + mockADGateway := adgateway.NewMockGateway(mockCtrl) + mockESController := esmockctrl.NewMockController(mockCtrl) + var stdin bytes.Buffer + stdin.Write([]byte("yes\n")) + mockADGateway.EXPECT().SearchDetector(ctx, getSearchPayload(mockDetectorName)).Return( + helperLoadBytes(t, "search_response.json"), nil) + mockADGateway.EXPECT().StopDetector(ctx, "detectorID").Return(mapper2.StringToStringPtr("Stopped Detector"), nil) + mockADGateway.EXPECT().DeleteDetector(ctx, "detectorID").Return(nil) + ctrl := New(&stdin, mockESController, mockADGateway) + err := ctrl.DeleteDetectorByName(ctx, mockDetectorName, true, false) + assert.NoError(t, err) + }) +} diff --git a/cli/internal/controller/ad/mocks/mock_ad.go b/cli/internal/controller/ad/mocks/mock_ad.go new file mode 100644 index 00000000..c766e8ca --- /dev/null +++ b/cli/internal/controller/ad/mocks/mock_ad.go @@ -0,0 +1,164 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: esad/internal/controller/ad (interfaces: Controller) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + ad "esad/internal/entity/ad" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockController is a mock of Controller interface +type MockController struct { + ctrl *gomock.Controller + recorder *MockControllerMockRecorder +} + +// MockControllerMockRecorder is the mock recorder for MockController +type MockControllerMockRecorder struct { + mock *MockController +} + +// NewMockController creates a new mock instance +func NewMockController(ctrl *gomock.Controller) *MockController { + mock := &MockController{ctrl: ctrl} + mock.recorder = &MockControllerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockController) EXPECT() *MockControllerMockRecorder { + return m.recorder +} + +// CreateAnomalyDetector mocks base method +func (m *MockController) CreateAnomalyDetector(arg0 context.Context, arg1 ad.CreateDetectorRequest) (*string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateAnomalyDetector", arg0, arg1) + ret0, _ := ret[0].(*string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateAnomalyDetector indicates an expected call of CreateAnomalyDetector +func (mr *MockControllerMockRecorder) CreateAnomalyDetector(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAnomalyDetector", reflect.TypeOf((*MockController)(nil).CreateAnomalyDetector), arg0, arg1) +} + +// CreateMultiEntityAnomalyDetector mocks base method +func (m *MockController) CreateMultiEntityAnomalyDetector(arg0 context.Context, arg1 ad.CreateDetectorRequest, arg2, arg3 bool) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateMultiEntityAnomalyDetector", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateMultiEntityAnomalyDetector indicates an expected call of CreateMultiEntityAnomalyDetector +func (mr *MockControllerMockRecorder) CreateMultiEntityAnomalyDetector(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMultiEntityAnomalyDetector", reflect.TypeOf((*MockController)(nil).CreateMultiEntityAnomalyDetector), arg0, arg1, arg2, arg3) +} + +// DeleteDetector mocks base method +func (m *MockController) DeleteDetector(arg0 context.Context, arg1 string, arg2, arg3 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteDetector", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteDetector indicates an expected call of DeleteDetector +func (mr *MockControllerMockRecorder) DeleteDetector(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDetector", reflect.TypeOf((*MockController)(nil).DeleteDetector), arg0, arg1, arg2, arg3) +} + +// DeleteDetectorByName mocks base method +func (m *MockController) DeleteDetectorByName(arg0 context.Context, arg1 string, arg2, arg3 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteDetectorByName", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteDetectorByName indicates an expected call of DeleteDetectorByName +func (mr *MockControllerMockRecorder) DeleteDetectorByName(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDetectorByName", reflect.TypeOf((*MockController)(nil).DeleteDetectorByName), arg0, arg1, arg2, arg3) +} + +// SearchDetectorByName mocks base method +func (m *MockController) SearchDetectorByName(arg0 context.Context, arg1 string) ([]ad.Detector, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchDetectorByName", arg0, arg1) + ret0, _ := ret[0].([]ad.Detector) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchDetectorByName indicates an expected call of SearchDetectorByName +func (mr *MockControllerMockRecorder) SearchDetectorByName(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchDetectorByName", reflect.TypeOf((*MockController)(nil).SearchDetectorByName), arg0, arg1) +} + +// StartDetector mocks base method +func (m *MockController) StartDetector(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartDetector", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// StartDetector indicates an expected call of StartDetector +func (mr *MockControllerMockRecorder) StartDetector(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartDetector", reflect.TypeOf((*MockController)(nil).StartDetector), arg0, arg1) +} + +// StartDetectorByName mocks base method +func (m *MockController) StartDetectorByName(arg0 context.Context, arg1 string, arg2 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartDetectorByName", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// StartDetectorByName indicates an expected call of StartDetectorByName +func (mr *MockControllerMockRecorder) StartDetectorByName(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartDetectorByName", reflect.TypeOf((*MockController)(nil).StartDetectorByName), arg0, arg1, arg2) +} + +// StopDetector mocks base method +func (m *MockController) StopDetector(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StopDetector", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// StopDetector indicates an expected call of StopDetector +func (mr *MockControllerMockRecorder) StopDetector(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopDetector", reflect.TypeOf((*MockController)(nil).StopDetector), arg0, arg1) +} + +// StopDetectorByName mocks base method +func (m *MockController) StopDetectorByName(arg0 context.Context, arg1 string, arg2 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StopDetectorByName", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// StopDetectorByName indicates an expected call of StopDetectorByName +func (mr *MockControllerMockRecorder) StopDetectorByName(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopDetectorByName", reflect.TypeOf((*MockController)(nil).StopDetectorByName), arg0, arg1, arg2) +} diff --git a/cli/internal/controller/ad/testdata/create_failed_response.json b/cli/internal/controller/ad/testdata/create_failed_response.json new file mode 100644 index 00000000..62a343c8 --- /dev/null +++ b/cli/internal/controller/ad/testdata/create_failed_response.json @@ -0,0 +1,13 @@ +{ + "error": { + "root_cause": [ + { + "type": "illegal_argument_exception", + "reason": "Cannot create anomaly detector with name [testdata-detector] as it's already used by detector [wR_1XXMBs3q1IVz33Sk-]" + } + ], + "type": "illegal_argument_exception", + "reason": "Cannot create anomaly detector with name [testdata-detector] as it's already used by detector [wR_1XXMBs3q1IVz33Sk-]" + }, + "status": 400 +} \ No newline at end of file diff --git a/cli/internal/controller/ad/testdata/create_request.json b/cli/internal/controller/ad/testdata/create_request.json new file mode 100644 index 00000000..63e5d2f2 --- /dev/null +++ b/cli/internal/controller/ad/testdata/create_request.json @@ -0,0 +1,45 @@ +{ + "name": "test-detector", + "description": "Test detector", + "time_field": "timestamp", + "indices": [ + "order*" + ], + "feature_attributes": [ + { + "feature_name": "total_order", + "feature_enabled": true, + "aggregation_query": { + "total_order": { + "sum": { + "field": "value" + } + } + } + } + ], + "filter_query": { + "bool": { + "filter": [ + { + "exists": { + "field": "value", + "boost": 1 + } + } + ] + } + }, + "detection_interval": { + "period": { + "interval": 1, + "unit": "Minutes" + } + }, + "window_delay": { + "period": { + "interval": 1, + "unit": "Minutes" + } + } +} \ No newline at end of file diff --git a/cli/internal/controller/ad/testdata/create_response.json b/cli/internal/controller/ad/testdata/create_response.json new file mode 100644 index 00000000..ecd4162c --- /dev/null +++ b/cli/internal/controller/ad/testdata/create_response.json @@ -0,0 +1,55 @@ +{ + "_id": "m4ccEnIBTXsGi3mvMt9p", + "_version": 1, + "_seq_no": 3, + "_primary_term": 1, + "anomaly_detector": { + "name": "test-detector", + "description": "Test detector", + "time_field": "timestamp", + "indices": [ + "order*" + ], + "filter_query": { + "bool": { + "filter": [ + { + "exists": { + "field": "value", + "boost": 1.0 + } + } + ], + "adjust_pure_negative": true, + "boost": 1.0 + } + }, + "detection_interval": { + "period": { + "interval": 1, + "unit": "Minutes" + } + }, + "window_delay": { + "period": { + "interval": 1, + "unit": "Minutes" + } + }, + "schema_version": 0, + "feature_attributes": [ + { + "feature_id": "mYccEnIBTXsGi3mvMd8_", + "feature_name": "total_order", + "feature_enabled": true, + "aggregation_query": { + "total_order": { + "sum": { + "field": "value" + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/cli/internal/controller/ad/testdata/search_response.json b/cli/internal/controller/ad/testdata/search_response.json new file mode 100644 index 00000000..4f7434e0 --- /dev/null +++ b/cli/internal/controller/ad/testdata/search_response.json @@ -0,0 +1,116 @@ +{ + "took" : 6, + "timed_out" : false, + "_shards" : { + "total" : 1, + "successful" : 1, + "skipped" : 0, + "failed" : 0 + }, + "hits" : { + "total" : { + "value" : 1, + "relation" : "eq" + }, + "max_score" : 0.06453852, + "hits" : [ + { + "_index" : ".opendistro-anomaly-detectors", + "_type" : "_doc", + "_id" : "detectorID", + "_version" : 1, + "_seq_no" : 494, + "_primary_term" : 4, + "_score" : 0.06453852, + "_source" : { + "name" : "detector", + "description" : "Test detector", + "time_field" : "utc_time", + "indices" : [ + "kibana_sample_data_ecommerce*" + ], + "filter_query" : { + "bool" : { + "must" : [ + { + "bool" : { + "filter" : [ + { + "term" : { + "day_of_week" : { + "value" : "Thursday", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + { + "bool" : { + "filter" : [ + { + "term" : { + "currency" : { + "value" : "EUR", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + "detection_interval" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "window_delay" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "schema_version" : 0, + "feature_attributes" : [ + { + "feature_id" : "xVh0bnMBLlLTlH7nzohm", + "feature_name" : "sum_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "sum_total_quantity" : { + "sum" : { + "field" : "total_quantity" + } + } + } + }, + { + "feature_id" : "xlh0bnMBLlLTlH7nzohm", + "feature_name" : "average_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "average_total_quantity" : { + "avg" : { + "field" : "total_quantity" + } + } + } + } + ], + "last_update_time" : 1595286015594 + } + } + ] + } +} diff --git a/cli/internal/controller/es/es.go b/cli/internal/controller/es/es.go new file mode 100644 index 00000000..c21afc3e --- /dev/null +++ b/cli/internal/controller/es/es.go @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package es + +import ( + "context" + "encoding/json" + "esad/internal/entity/es" + "esad/internal/gateway/es" + "fmt" +) + +//go:generate mockgen -destination=mocks/mock_es.go -package=mocks . Controller + +//Controller is an interface for ES Cluster to get distinct values +type Controller interface { + GetDistinctValues(ctx context.Context, index string, field string) ([]interface{}, error) +} + +type controller struct { + gateway es.Gateway +} + +//New returns new instance of Controller +func New(gateway es.Gateway) Controller { + return &controller{ + gateway, + } +} +func (c controller) GetDistinctValues(ctx context.Context, index string, field string) ([]interface{}, error) { + if len(index) == 0 || len(field) == 0 { + return nil, fmt.Errorf("index and field cannot be empty") + } + response, err := c.gateway.SearchDistinctValues(ctx, index, field) + if err != nil { + return nil, err + } + var data elasticsearch.Response + err = json.Unmarshal(response, &data) + if err != nil { + return nil, err + } + + var values []interface{} + for _, bucket := range data.Aggregations.Items.Buckets { + values = append(values, bucket.Key) + } + return values, nil +} diff --git a/cli/internal/controller/es/es_test.go b/cli/internal/controller/es/es_test.go new file mode 100644 index 00000000..14855c42 --- /dev/null +++ b/cli/internal/controller/es/es_test.go @@ -0,0 +1,99 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package es + +import ( + "context" + "errors" + gateway "esad/internal/gateway/es/mocks" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "io/ioutil" + "path/filepath" + "testing" +) + +func helperLoadBytes(t *testing.T, name string) []byte { + path := filepath.Join("testdata", name) // relative path + bytes, err := ioutil.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return bytes +} + +func helperConvertToInterface(input []string) []interface{} { + s := make([]interface{}, len(input)) + for i, v := range input { + s[i] = v + } + return s +} + +func TestController_GetDistinctValues(t *testing.T) { + t.Run("empty index name", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockGateway := gateway.NewMockGateway(mockCtrl) + ctx := context.Background() + ctrl := New(mockGateway) + _, err := ctrl.GetDistinctValues(ctx, "", "f1") + assert.Error(t, err) + }) + t.Run("empty field name", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockGateway := gateway.NewMockGateway(mockCtrl) + ctx := context.Background() + ctrl := New(mockGateway) + _, err := ctrl.GetDistinctValues(ctx, "", "") + assert.Error(t, err) + }) + t.Run("gateway failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockGateway := gateway.NewMockGateway(mockCtrl) + ctx := context.Background() + mockGateway.EXPECT().SearchDistinctValues(ctx, "example", "f1").Return(nil, errors.New("search failed")) + ctrl := New(mockGateway) + _, err := ctrl.GetDistinctValues(ctx, "example", "f1") + assert.Error(t, err) + }) + t.Run("gateway response failed", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockGateway := gateway.NewMockGateway(mockCtrl) + ctx := context.Background() + mockGateway.EXPECT().SearchDistinctValues(ctx, "example", "f1").Return([]byte("No response"), nil) + ctrl := New(mockGateway) + _, err := ctrl.GetDistinctValues(ctx, "example", "f1") + assert.Error(t, err) + }) + t.Run("get distinct success", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockGateway := gateway.NewMockGateway(mockCtrl) + ctx := context.Background() + expectedResult := helperConvertToInterface([]string{"Packaged Foods", "Dairy", "Meat and Seafood"}) + mockGateway.EXPECT().SearchDistinctValues(ctx, "example", "f1").Return(helperLoadBytes(t, "search_result.json"), nil) + ctrl := New(mockGateway) + result, err := ctrl.GetDistinctValues(ctx, "example", "f1") + assert.NoError(t, err) + assert.EqualValues(t, expectedResult, result) + + }) +} diff --git a/cli/internal/controller/es/mocks/mock_es.go b/cli/internal/controller/es/mocks/mock_es.go new file mode 100644 index 00000000..262e265d --- /dev/null +++ b/cli/internal/controller/es/mocks/mock_es.go @@ -0,0 +1,49 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: esad/internal/controller/es (interfaces: Controller) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockController is a mock of Controller interface +type MockController struct { + ctrl *gomock.Controller + recorder *MockControllerMockRecorder +} + +// MockControllerMockRecorder is the mock recorder for MockController +type MockControllerMockRecorder struct { + mock *MockController +} + +// NewMockController creates a new mock instance +func NewMockController(ctrl *gomock.Controller) *MockController { + mock := &MockController{ctrl: ctrl} + mock.recorder = &MockControllerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockController) EXPECT() *MockControllerMockRecorder { + return m.recorder +} + +// GetDistinctValues mocks base method +func (m *MockController) GetDistinctValues(arg0 context.Context, arg1, arg2 string) ([]interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDistinctValues", arg0, arg1, arg2) + ret0, _ := ret[0].([]interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetDistinctValues indicates an expected call of GetDistinctValues +func (mr *MockControllerMockRecorder) GetDistinctValues(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDistinctValues", reflect.TypeOf((*MockController)(nil).GetDistinctValues), arg0, arg1, arg2) +} diff --git a/cli/internal/controller/es/testdata/search_result.json b/cli/internal/controller/es/testdata/search_result.json new file mode 100644 index 00000000..7b1e47f2 --- /dev/null +++ b/cli/internal/controller/es/testdata/search_result.json @@ -0,0 +1,35 @@ +{ + "took": 80, + "timed_out": false, + "_shards": { + "total": 5, + "successful": 5, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": 14, + "max_score": 0, + "hits": [] + }, + "aggregations": { + "items": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "Packaged Foods", + "doc_count": 4 + }, + { + "key": "Dairy", + "doc_count": 3 + }, + { + "key": "Meat and Seafood", + "doc_count": 2 + } + ] + } + } +} \ No newline at end of file diff --git a/cli/internal/entity/ad/ad.go b/cli/internal/entity/ad/ad.go new file mode 100644 index 00000000..30f54b53 --- /dev/null +++ b/cli/internal/entity/ad/ad.go @@ -0,0 +1,145 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package ad + +import ( + "encoding/json" +) + +//Feature structure for detector features +type Feature struct { + Name string `json:"feature_name"` + Enabled bool `json:"feature_enabled"` + AggregationQuery json.RawMessage `json:"aggregation_query"` +} + +//Period represents time interval +type Period struct { + Duration int32 `json:"interval"` + Unit string `json:"unit"` +} + +//Interval represent unit of time +type Interval struct { + Period Period `json:"period"` +} + +//CreateDetector represents Detector creation request +type CreateDetector struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + TimeField string `json:"time_field"` + Index []string `json:"indices"` + Features []Feature `json:"feature_attributes"` + Filter json.RawMessage `json:"filter_query,omitempty"` + Interval Interval `json:"detection_interval"` + Delay Interval `json:"window_delay"` +} + +//FeatureRequest represents feature request +type FeatureRequest struct { + AggregationType []string `json:"aggregation_type"` + Enabled bool `json:"enabled"` + Field []string `json:"field"` +} + +//CreateDetectorRequest represents request for AD +type CreateDetectorRequest struct { + Name string `json:"name"` + Description string `json:"description"` + TimeField string `json:"time_field"` + Index []string `json:"index"` + Features []FeatureRequest `json:"features"` + Filter json.RawMessage `json:"filter,omitempty"` + Interval string `json:"interval"` + Delay string `json:"window_delay"` + Start bool `json:"start"` + PartitionField *string `json:"partition_field"` +} + +//Bool type for must query +type Bool struct { + Must []json.RawMessage `json:"must"` +} + +//Query type to represent query +type Query struct { + Bool Bool `json:"bool"` +} + +//Detector type to map name to ID +type Detector struct { + Name string + ID string +} + +//CreateFailedError structure if create failed +type CreateFailedError struct { + Type string `json:"type"` + Reason string `json:"reason"` +} + +//CreateError Error type in Create Response +type CreateError struct { + Error CreateFailedError `json:"error"` + Status int32 `json:"status"` +} + +//Profile represents profile in config +type Profile struct { + Endpoint string `mapstructure:"endpoint"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Name string `mapstructure:"name"` +} + +//Configuration represents configuration in config file +type Configuration struct { + Profiles []Profile `mapstructure:"profiles"` +} + +//Match specifies name +type Match struct { + Name string `json:"name"` +} + +//SearchQuery contains match names +type SearchQuery struct { + Match Match `json:"match"` +} + +//SearchRequest represents structure for search detectors +type SearchRequest struct { + Query SearchQuery `json:"query"` +} + +//Source contains detectors metadata +type Source struct { + Name string `json:"name"` +} + +//Hit contains search results +type Hit struct { + ID string `json:"_id"` + Source Source `json:"_source"` +} + +//Container represents structure for search response +type Container struct { + Hits []Hit `json:"hits"` +} + +//SearchResponse represents structure for search response +type SearchResponse struct { + Hits Container `json:"hits"` +} diff --git a/cli/internal/entity/ad/ad_test.go b/cli/internal/entity/ad/ad_test.go new file mode 100644 index 00000000..12e2f709 --- /dev/null +++ b/cli/internal/entity/ad/ad_test.go @@ -0,0 +1,176 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package ad + +import ( + "encoding/json" + "esad/internal/mapper" + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func getRawFilter() []byte { + return []byte(`{ + "bool": { + "filter": [{ + "exists": { + "field": "value", + "boost": 1 + } + }], + "adjust_pure_negative": true, + "boost": 1 + } + }`) +} + +func getRawFeatureAggregation() []byte { + return []byte(`{ + "sum_value": { + "sum": { + "field": "value" + } + } + }`) +} + +func getCreateAnomalyDetectorData() string { + return strings.ReplaceAll(`{ + "name": "testdata-detector", + "description": "Test detector", + "time_field": "timestamp", + "indices": ["order*"], + "feature_attributes": [{"feature_name": "sum_value","feature_enabled": true,"aggregation_query": {"sum_value": {"sum": {"field": "value"}}}}], + "filter_query": {"bool": {"filter": [{"exists": {"field": "value","boost": 1}}],"adjust_pure_negative": true,"boost": 1}}, + "detection_interval": {"period": {"interval": 1,"unit": "Minutes"} + }, + "window_delay": {"period": {"interval": 1,"unit": "Minutes"}}}`, "\n\t ", "") +} +func getCreateDetector() CreateDetector { + return CreateDetector{ + Name: "testdata-detector", + Description: "Testdetector", + TimeField: "timestamp", + Index: []string{"order*"}, + Features: []Feature{ + { + Name: "sum_value", + Enabled: true, + AggregationQuery: getRawFeatureAggregation(), + }, + }, + Filter: getRawFilter(), + Interval: Interval{ + Period: Period{ + Duration: 1, + Unit: "Minutes", + }, + }, + Delay: Interval{ + Period: Period{ + Duration: 1, + Unit: "Minutes", + }, + }, + } +} + +func getCreateDetectorRequest() CreateDetectorRequest { + return CreateDetectorRequest{ + Name: "testdata-detector", + Description: "Test detector", + TimeField: "timestamp", + Index: []string{"order*"}, + Features: []FeatureRequest{{ + AggregationType: []string{"sum"}, + Enabled: true, + Field: []string{"value"}, + }}, + Filter: getRawFilter(), + Interval: "10m", + Delay: "5m", + Start: true, + PartitionField: mapper.StringToStringPtr("ip"), + } +} +func getCreateDetectorRequestJSON() string { + return `{ + "name": "testdata-detector", + "description": "Test detector", + "time_field": "timestamp", + "index": ["order*"], + "features": [{ + "aggregation_type": ["sum"], + "enabled": true, + "field":["value"] + }], + "filter": { + "bool": { + "filter": [{ + "exists": { + "field": "value", + "boost": 1 + } + }], + "adjust_pure_negative": true, + "boost": 1 + } + }, + "interval": "10m", + "window_delay": "5m", + "start": true, + "partition_field": "ip" + }` +} + +func TestCreateDetectorRequestUnMarshalling(t *testing.T) { + t.Run("deserialization success", func(t *testing.T) { + test := getCreateDetectorRequestJSON() + actual := CreateDetectorRequest{} + _ = json.Unmarshal([]byte(test), &actual) + expected := getCreateDetectorRequest() + assert.EqualValues(t, expected, actual) + }) +} + +func TestCreateDetectorMarshalling(t *testing.T) { + t.Run("serialization success", func(t *testing.T) { + test := getCreateDetector() + actual, _ := json.Marshal(test) + expected := getCreateAnomalyDetectorData() + removeNewLine := strings.ReplaceAll(expected, "\n", "") + removeTabs := strings.ReplaceAll(removeNewLine, "\n", "") + assert.EqualValues(t, strings.ReplaceAll(removeTabs, " ", ""), string(actual)) + }) +} + +func TestSearchRequestMarshall(t *testing.T) { + t.Run("serialization for search", func(t *testing.T) { + data := getSearchRequest() + actual, err := json.Marshal(data) + assert.Nil(t, err) + assert.EqualValues(t, getSearchRequestJSON(), string(actual)) + }) +} + +func getSearchRequestJSON() string { + return `{"query":{"match":{"name":"test-d"}}}` +} +func getSearchRequest() interface{} { + return SearchRequest{ + Query: SearchQuery{ + Match: Match{Name: "test-d"}, + }, + } +} diff --git a/cli/internal/entity/es/es.go b/cli/internal/entity/es/es.go new file mode 100644 index 00000000..921ccdd3 --- /dev/null +++ b/cli/internal/entity/es/es.go @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package elasticsearch + +//Terms contains fields +type Terms struct { + Field string `json:"field"` +} + +//DistinctGroups contains terms +type DistinctGroups struct { + Term Terms `json:"terms"` +} + +//Aggregate contains list of items +type Aggregate struct { + Group DistinctGroups `json:"items"` +} + +//SearchRequest structure for request +type SearchRequest struct { + Agg Aggregate `json:"aggs"` + Size int32 `json:"size"` +} + +//Bucket represents bucket used by ES for aggregations +type Bucket struct { + Key interface{} `json:"key"` + DocCount int64 `json:"doc_count"` +} + +//Items contains buckets defined by es response +type Items struct { + Buckets []Bucket `json:"buckets"` +} + +//Aggregations contains items defined by es response +type Aggregations struct { + Items Items `json:"items"` +} + +//Response response defined by es response +type Response struct { + Aggregations Aggregations `json:"aggregations"` +} diff --git a/cli/internal/gateway/ad/ad.go b/cli/internal/gateway/ad/ad.go new file mode 100644 index 00000000..e230addc --- /dev/null +++ b/cli/internal/gateway/ad/ad.go @@ -0,0 +1,176 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package ad + +import ( + "context" + "esad/internal/client" + gw "esad/internal/gateway" + mapper2 "esad/internal/mapper" + "fmt" + "net/http" + "net/url" +) + +const ( + baseURL = "_opendistro/_anomaly_detection/detectors" + startURLTemplate = baseURL + "/%s/" + "_start" + stopURLTemplate = baseURL + "/%s/" + "_stop" + searchURLTemplate = baseURL + "/_search" + deleteURLTemplate = baseURL + "/%s" +) + +//go:generate mockgen -destination=mocks/mock_ad.go -package=mocks . Gateway + +// Gateway interface to AD Plugin +type Gateway interface { + CreateDetector(context.Context, interface{}) ([]byte, error) + StartDetector(context.Context, string) error + StopDetector(context.Context, string) (*string, error) + DeleteDetector(context.Context, string) error + SearchDetector(context.Context, interface{}) ([]byte, error) +} + +type gateway struct { + gw.HTTPGateway +} + +//New creates new Gateway instance +func New(c *client.Client, u *client.UserConfig) Gateway { + return &gateway{*gw.NewHTTPGateway(c, u)} +} + +func (g *gateway) buildCreateURL() (*url.URL, error) { + endpoint, err := gw.GetValidEndpoint(g.UserConfig) + if err != nil { + return nil, err + } + endpoint.Path = baseURL + return endpoint, nil +} + +func (g *gateway) CreateDetector(ctx context.Context, payload interface{}) ([]byte, error) { + createURL, err := g.buildCreateURL() + if err != nil { + return nil, err + } + detectorRequest, err := g.BuildRequest(ctx, http.MethodPost, payload, createURL.String(), gw.GetHeaders()) + if err != nil { + return nil, err + } + response, err := g.Call(detectorRequest, http.StatusCreated) + if err != nil { + return nil, err + } + return response, nil +} + +func (g *gateway) buildStartURL(ID string) (*url.URL, error) { + endpoint, err := gw.GetValidEndpoint(g.UserConfig) + if err != nil { + return nil, err + } + endpoint.Path = fmt.Sprintf(startURLTemplate, ID) + return endpoint, nil +} + +func (g *gateway) StartDetector(ctx context.Context, ID string) error { + startURL, err := g.buildStartURL(ID) + if err != nil { + return err + } + detectorRequest, err := g.BuildRequest(ctx, http.MethodPost, "", startURL.String(), gw.GetHeaders()) + if err != nil { + return err + } + _, err = g.Call(detectorRequest, http.StatusOK) + if err != nil { + return err + } + return nil +} + +func (g *gateway) buildStopURL(ID string) (*url.URL, error) { + endpoint, err := gw.GetValidEndpoint(g.UserConfig) + if err != nil { + return nil, err + } + endpoint.Path = fmt.Sprintf(stopURLTemplate, ID) + return endpoint, nil +} + +func (g *gateway) StopDetector(ctx context.Context, ID string) (*string, error) { + stopURL, err := g.buildStopURL(ID) + if err != nil { + return nil, err + } + detectorRequest, err := g.BuildRequest(ctx, http.MethodPost, "", stopURL.String(), gw.GetHeaders()) + if err != nil { + return nil, err + } + res, err := g.Call(detectorRequest, http.StatusOK) + if err != nil { + return nil, err + } + return mapper2.StringToStringPtr(fmt.Sprintf("%s", res)), nil +} + +func (g *gateway) buildSearchURL() (*url.URL, error) { + endpoint, err := gw.GetValidEndpoint(g.UserConfig) + if err != nil { + return nil, err + } + endpoint.Path = searchURLTemplate + return endpoint, nil +} + +func (g *gateway) SearchDetector(ctx context.Context, payload interface{}) ([]byte, error) { + searchURL, err := g.buildSearchURL() + if err != nil { + return nil, err + } + searchRequest, err := g.BuildRequest(ctx, http.MethodPost, payload, searchURL.String(), gw.GetHeaders()) + if err != nil { + return nil, err + } + response, err := g.Call(searchRequest, http.StatusOK) + if err != nil { + return nil, err + } + return response, nil +} + +func (g *gateway) buildDeleteURL(ID string) (*url.URL, error) { + endpoint, err := gw.GetValidEndpoint(g.UserConfig) + if err != nil { + return nil, err + } + endpoint.Path = fmt.Sprintf(deleteURLTemplate, ID) + return endpoint, nil +} + +func (g *gateway) DeleteDetector(ctx context.Context, ID string) error { + deleteURL, err := g.buildDeleteURL(ID) + if err != nil { + return err + } + detectorRequest, err := g.BuildRequest(ctx, http.MethodDelete, "", deleteURL.String(), gw.GetHeaders()) + if err != nil { + return err + } + _, err = g.Call(detectorRequest, http.StatusOK) + if err != nil { + return err + } + return nil +} diff --git a/cli/internal/gateway/ad/ad_test.go b/cli/internal/gateway/ad/ad_test.go new file mode 100644 index 00000000..9caa9f7f --- /dev/null +++ b/cli/internal/gateway/ad/ad_test.go @@ -0,0 +1,290 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package ad + +import ( + "bytes" + "context" + "encoding/json" + "esad/internal/client" + "esad/internal/client/mocks" + "esad/internal/entity/ad" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "path/filepath" + "testing" +) + +func helperLoadBytes(t *testing.T, name string) []byte { + path := filepath.Join("testdata", name) // relative path + bytes, err := ioutil.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return bytes +} + +func getTestClient(t *testing.T, response string, code int, method string, action string) *client.Client { + testClient := mocks.NewTestClient(func(req *http.Request) *http.Response { + // Test request parameters + assert.Equal(t, req.URL.String(), "http://localhost:9200/_opendistro/_anomaly_detection/detectors/id"+action) + assert.EqualValues(t, req.Method, method) + assert.EqualValues(t, len(req.Header), 2) + return &http.Response{ + StatusCode: code, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewBufferString(response)), + // Must be set to non-nil value or it panics + Header: make(http.Header), + } + }) + return testClient +} + +func TestGateway_StartDetector(t *testing.T) { + ctx := context.Background() + t.Run("connection failed", func(t *testing.T) { + testClient := getTestClient(t, `connection failed`, 400, http.MethodPost, "/_start") + + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + err := testGateway.StartDetector(ctx, "id") + assert.EqualError(t, err, "connection failed") + }) + t.Run("started successfully", func(t *testing.T) { + testClient := getTestClient(t, `{ + "_id" : "id", + "_version" : 1, + "_seq_no" : 6, + "_primary_term" : 1 + }`, 200, http.MethodPost, "/_start") + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + err := testGateway.StartDetector(ctx, "id") + assert.NoError(t, err) + }) +} +func TestGateway_StopDetector(t *testing.T) { + ctx := context.Background() + t.Run("connection failed", func(t *testing.T) { + testClient := getTestClient(t, `connection failed`, 400, http.MethodPost, "/_stop") + + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + _, err := testGateway.StopDetector(ctx, "id") + assert.EqualError(t, err, "connection failed") + }) + t.Run("stop successfully", func(t *testing.T) { + testClient := getTestClient(t, `Stopped detector: id`, 200, http.MethodPost, "/_stop") + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + res, err := testGateway.StopDetector(ctx, "id") + assert.NoError(t, err) + assert.EqualValues(t, *res, "Stopped detector: id") + }) +} + +func TestGateway_DeleteDetector(t *testing.T) { + ctx := context.Background() + t.Run("connection failed", func(t *testing.T) { + testClient := getTestClient(t, `connection failed`, 400, http.MethodDelete, "") + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + err := testGateway.DeleteDetector(ctx, "id") + assert.EqualError(t, err, "connection failed") + }) + t.Run("delete success", func(t *testing.T) { + testClient := getTestClient(t, ` + { + "_index" : ".opendistro-anomaly-detectors", + "_type" : "_doc", + "_id" : "id", + "_version" : 2, + "result" : "deleted", + "forced_refresh" : true, + "_shards" : { + "total" : 2, + "successful" : 2, + "failed" : 0 + }, + "_seq_no" : 6, + "_primary_term" : 1 + }`, 200, http.MethodDelete, "") + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + err := testGateway.DeleteDetector(ctx, "id") + assert.NoError(t, err) + }) +} + +func TestGateway_SearchDetector(t *testing.T) { + responseData, _ := json.Marshal(helperLoadBytes(t, "search_result.json")) + ctx := context.Background() + t.Run("search succeeded", func(t *testing.T) { + + testClient := getSearchClient(t, responseData, 200) + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + response, err := testGateway.SearchDetector(ctx, ad.SearchRequest{ + Query: ad.SearchQuery{ + Match: ad.Match{ + Name: "detector-name", + }, + }}) + assert.NoError(t, err) + assert.EqualValues(t, response, responseData) + }) + t.Run("search failed due to 404", func(t *testing.T) { + + testClient := getSearchClient(t, []byte("No connection found"), 400) + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + _, err := testGateway.SearchDetector(ctx, ad.SearchRequest{ + Query: ad.SearchQuery{ + Match: ad.Match{ + Name: "detector-name", + }, + }}) + assert.EqualError(t, err, "No connection found") + }) +} + +func TestGateway_CreateDetector(t *testing.T) { + responseData, _ := json.Marshal(helperLoadBytes(t, "create_result.json")) + ctx := context.Background() + t.Run("create succeeded", func(t *testing.T) { + + testClient := getCreateClient(t, responseData, 201) + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + response, err := testGateway.CreateDetector(ctx, getCreateDetector()) + assert.NoError(t, err) + assert.EqualValues(t, response, responseData) + }) + + t.Run("create failed due to 400", func(t *testing.T) { + + testClient := getCreateClient(t, []byte("No connection found"), 400) + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + _, err := testGateway.CreateDetector(ctx, getCreateDetector()) + assert.EqualError(t, err, "No connection found") + }) +} + +func getSearchClient(t *testing.T, responseData []byte, code int) *client.Client { + testClient := mocks.NewTestClient(func(req *http.Request) *http.Response { + // Test request parameters + assert.Equal(t, req.URL.String(), "http://localhost:9200/_opendistro/_anomaly_detection/detectors/_search") + assert.EqualValues(t, req.Method, http.MethodPost) + resBytes, _ := ioutil.ReadAll(req.Body) + var body ad.SearchRequest + err := json.Unmarshal(resBytes, &body) + assert.NoError(t, err) + assert.EqualValues(t, body.Query.Match.Name, "detector-name") + assert.EqualValues(t, len(req.Header), 2) + return &http.Response{ + StatusCode: code, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewBufferString(string(responseData))), + // Must be set to non-nil value or it panics + Header: make(http.Header), + } + }) + return testClient +} + +func getRawFeatureAggregation() []byte { + return []byte(`{"sum_value":{"sum":{"field":"value"}}}`) +} + +func getCreateDetector() ad.CreateDetector { + return ad.CreateDetector{ + Name: "testdata-detector", + Description: "Test detector", + TimeField: "timestamp", + Index: []string{"order*"}, + Features: []ad.Feature{ + { + Name: "sum_value", + Enabled: true, + AggregationQuery: getRawFeatureAggregation(), + }, + }, + Filter: []byte("{}"), + Interval: ad.Interval{ + Period: ad.Period{ + Duration: 1, + Unit: "Minutes", + }, + }, + Delay: ad.Interval{ + Period: ad.Period{ + Duration: 1, + Unit: "Minutes", + }, + }, + } +} + +func getCreateClient(t *testing.T, responseData []byte, code int) *client.Client { + return mocks.NewTestClient(func(req *http.Request) *http.Response { + // Test request parameters + assert.Equal(t, req.URL.String(), "http://localhost:9200/_opendistro/_anomaly_detection/detectors") + assert.EqualValues(t, req.Method, http.MethodPost) + resBytes, _ := ioutil.ReadAll(req.Body) + var body ad.CreateDetector + err := json.Unmarshal(resBytes, &body) + assert.NoError(t, err) + assert.Equal(t, getCreateDetector(), body) + assert.EqualValues(t, 2, len(req.Header)) + return &http.Response{ + StatusCode: code, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewBufferString(string(responseData))), + // Must be set to non-nil value or it panics + Header: make(http.Header), + } + }) +} diff --git a/cli/internal/gateway/ad/mocks/mock_ad.go b/cli/internal/gateway/ad/mocks/mock_ad.go new file mode 100644 index 00000000..23159470 --- /dev/null +++ b/cli/internal/gateway/ad/mocks/mock_ad.go @@ -0,0 +1,107 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: esad/internal/gateway/ad (interfaces: Gateway) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockGateway is a mock of Gateway interface +type MockGateway struct { + ctrl *gomock.Controller + recorder *MockGatewayMockRecorder +} + +// MockGatewayMockRecorder is the mock recorder for MockGateway +type MockGatewayMockRecorder struct { + mock *MockGateway +} + +// NewMockGateway creates a new mock instance +func NewMockGateway(ctrl *gomock.Controller) *MockGateway { + mock := &MockGateway{ctrl: ctrl} + mock.recorder = &MockGatewayMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockGateway) EXPECT() *MockGatewayMockRecorder { + return m.recorder +} + +// CreateDetector mocks base method +func (m *MockGateway) CreateDetector(arg0 context.Context, arg1 interface{}) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateDetector", arg0, arg1) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateDetector indicates an expected call of CreateDetector +func (mr *MockGatewayMockRecorder) CreateDetector(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDetector", reflect.TypeOf((*MockGateway)(nil).CreateDetector), arg0, arg1) +} + +// DeleteDetector mocks base method +func (m *MockGateway) DeleteDetector(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteDetector", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteDetector indicates an expected call of DeleteDetector +func (mr *MockGatewayMockRecorder) DeleteDetector(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDetector", reflect.TypeOf((*MockGateway)(nil).DeleteDetector), arg0, arg1) +} + +// SearchDetector mocks base method +func (m *MockGateway) SearchDetector(arg0 context.Context, arg1 interface{}) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchDetector", arg0, arg1) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchDetector indicates an expected call of SearchDetector +func (mr *MockGatewayMockRecorder) SearchDetector(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchDetector", reflect.TypeOf((*MockGateway)(nil).SearchDetector), arg0, arg1) +} + +// StartDetector mocks base method +func (m *MockGateway) StartDetector(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StartDetector", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// StartDetector indicates an expected call of StartDetector +func (mr *MockGatewayMockRecorder) StartDetector(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartDetector", reflect.TypeOf((*MockGateway)(nil).StartDetector), arg0, arg1) +} + +// StopDetector mocks base method +func (m *MockGateway) StopDetector(arg0 context.Context, arg1 string) (*string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StopDetector", arg0, arg1) + ret0, _ := ret[0].(*string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StopDetector indicates an expected call of StopDetector +func (mr *MockGatewayMockRecorder) StopDetector(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopDetector", reflect.TypeOf((*MockGateway)(nil).StopDetector), arg0, arg1) +} diff --git a/cli/internal/gateway/ad/testdata/create_result.json b/cli/internal/gateway/ad/testdata/create_result.json new file mode 100644 index 00000000..53bdcc33 --- /dev/null +++ b/cli/internal/gateway/ad/testdata/create_result.json @@ -0,0 +1,55 @@ +{ + "_id" : "m4ccEnIBTXsGi3mvMt9p", + "_version" : 1, + "_seq_no" : 3, + "_primary_term" : 1, + "anomaly_detector" : { + "name" : "test-detector", + "description" : "Test detector", + "time_field" : "timestamp", + "indices" : [ + "order*" + ], + "filter_query" : { + "bool" : { + "filter" : [ + { + "exists" : { + "field" : "value", + "boost" : 1.0 + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + "detection_interval" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "window_delay" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "schema_version" : 0, + "feature_attributes" : [ + { + "feature_id" : "mYccEnIBTXsGi3mvMd8_", + "feature_name" : "total_order", + "feature_enabled" : true, + "aggregation_query" : { + "total_order" : { + "sum" : { + "field" : "value" + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/cli/internal/gateway/ad/testdata/search_result.json b/cli/internal/gateway/ad/testdata/search_result.json new file mode 100644 index 00000000..4c2ded31 --- /dev/null +++ b/cli/internal/gateway/ad/testdata/search_result.json @@ -0,0 +1,78 @@ +{ + "took": 13, + "timed_out": false, + "_shards": { + "total": 5, + "successful": 5, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 994, + "relation": "eq" + }, + "max_score": 3.5410638, + "hits": [ + { + "_index": ".opendistro-anomaly-detectors", + "_type": "_doc", + "_id": "m4ccEnIBTXsGi3mvMt9p", + "_version": 2, + "_seq_no": 221, + "_primary_term": 1, + "_score": 3.5410638, + "_source": { + "name": "test-detector", + "description": "Test detector", + "time_field": "timestamp", + "indices": [ + "order*" + ], + "filter_query": { + "bool": { + "filter": [ + { + "exists": { + "field": "value", + "boost": 1 + } + } + ], + "adjust_pure_negative": true, + "boost": 1 + } + }, + "detection_interval": { + "period": { + "interval": 10, + "unit": "Minutes" + } + }, + "window_delay": { + "period": { + "interval": 1, + "unit": "Minutes" + } + }, + "schema_version": 0, + "feature_attributes": [ + { + "feature_id": "xxokEnIBcpeWMD987A1X", + "feature_name": "total_order", + "feature_enabled": true, + "aggregation_query": { + "total_order": { + "sum": { + "field": "value" + } + } + } + } + ], + "last_update_time": 1589442309241 + } + } + ] + } +} \ No newline at end of file diff --git a/cli/internal/gateway/es/es.go b/cli/internal/gateway/es/es.go new file mode 100644 index 00000000..91c1724d --- /dev/null +++ b/cli/internal/gateway/es/es.go @@ -0,0 +1,81 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package es + +import ( + "context" + "esad/internal/client" + elasticsearch "esad/internal/entity/es" + gw "esad/internal/gateway" + "fmt" + "net/http" + "net/url" +) + +const search = "_search" + +//go:generate mockgen -destination=mocks/mock_es.go -package=mocks . Gateway + +//Gateway interface to call ES +type Gateway interface { + SearchDistinctValues(ctx context.Context, index string, field string) ([]byte, error) +} + +type gateway struct { + gw.HTTPGateway +} + +// New returns new Gateway instance +func New(c *client.Client, u *client.UserConfig) Gateway { + return &gateway{ + *gw.NewHTTPGateway(c, u), + } +} +func buildPayload(field string) *elasticsearch.SearchRequest { + return &elasticsearch.SearchRequest{ + Size: 0, // This will skip data in the response + Agg: elasticsearch.Aggregate{ + Group: elasticsearch.DistinctGroups{ + Term: elasticsearch.Terms{ + Field: field, + }, + }, + }, + } +} + +func (g *gateway) buildSearchURL(index string) (*url.URL, error) { + endpoint, err := gw.GetValidEndpoint(g.UserConfig) + if err != nil { + return nil, err + } + endpoint.Path = fmt.Sprintf("%s/%s", index, search) + return endpoint, nil +} + +//SearchDistinctValues gets distinct values on index for given field +func (g *gateway) SearchDistinctValues(ctx context.Context, index string, field string) ([]byte, error) { + searchURL, err := g.buildSearchURL(index) + if err != nil { + return nil, err + } + searchRequest, err := g.BuildRequest(ctx, http.MethodGet, buildPayload(field), searchURL.String(), gw.GetHeaders()) + if err != nil { + return nil, err + } + response, err := g.Call(searchRequest, http.StatusOK) + if err != nil { + return nil, err + } + return response, nil +} diff --git a/cli/internal/gateway/es/es_test.go b/cli/internal/gateway/es/es_test.go new file mode 100644 index 00000000..4164839e --- /dev/null +++ b/cli/internal/gateway/es/es_test.go @@ -0,0 +1,95 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package es + +import ( + "bytes" + "context" + "encoding/json" + "esad/internal/client" + "esad/internal/client/mocks" + elasticsearch "esad/internal/entity/es" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "path/filepath" + "testing" +) + +func helperLoadBytes(t *testing.T, name string) []byte { + path := filepath.Join("testdata", name) // relative path + bytes, err := ioutil.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return bytes +} + +func getTestClient(t *testing.T, responseData string, code int) *client.Client { + return mocks.NewTestClient(func(req *http.Request) *http.Response { + // Test request parameters + assert.Equal(t, req.URL.String(), "http://localhost:9200/test_index/_search") + resBytes, _ := ioutil.ReadAll(req.Body) + var body elasticsearch.SearchRequest + err := json.Unmarshal(resBytes, &body) + assert.NoError(t, err) + assert.EqualValues(t, body.Size, 0) + assert.EqualValues(t, body.Agg.Group.Term.Field, "day_of_week") + assert.EqualValues(t, len(req.Header), 2) + return &http.Response{ + StatusCode: code, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewBufferString(responseData)), + // Must be set to non-nil value or it panics + Header: make(http.Header), + } + }) +} + +func TestGateway_SearchDistinctValues(t *testing.T) { + responseData, _ := json.Marshal(helperLoadBytes(t, "search_result.json")) + ctx := context.Background() + t.Run("search succeeded", func(t *testing.T) { + + testClient := getTestClient(t, string(responseData), 200) + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + actual, err := testGateway.SearchDistinctValues(ctx, "test_index", "day_of_week") + assert.NoError(t, err) + assert.EqualValues(t, actual, responseData) + }) + t.Run("search failed due to 404", func(t *testing.T) { + testClient := getTestClient(t, "No connection found", 400) + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "admin", + Password: "admin", + }) + _, err := testGateway.SearchDistinctValues(ctx, "test_index", "day_of_week") + assert.EqualError(t, err, "No connection found") + }) + t.Run("search failed due to bad user config", func(t *testing.T) { + + testClient := getTestClient(t, "No connection found", 400) + testGateway := New(testClient, &client.UserConfig{ + Endpoint: "http://localhost:9200", + Username: "", + Password: "admin", + }) + _, err := testGateway.SearchDistinctValues(ctx, "test_index", "day_of_week") + assert.EqualError(t, err, "user name and password cannot be empty") + }) +} diff --git a/cli/internal/gateway/es/mocks/mock_es.go b/cli/internal/gateway/es/mocks/mock_es.go new file mode 100644 index 00000000..7160f51d --- /dev/null +++ b/cli/internal/gateway/es/mocks/mock_es.go @@ -0,0 +1,49 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: esad/internal/gateway/es (interfaces: Gateway) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockGateway is a mock of Gateway interface +type MockGateway struct { + ctrl *gomock.Controller + recorder *MockGatewayMockRecorder +} + +// MockGatewayMockRecorder is the mock recorder for MockGateway +type MockGatewayMockRecorder struct { + mock *MockGateway +} + +// NewMockGateway creates a new mock instance +func NewMockGateway(ctrl *gomock.Controller) *MockGateway { + mock := &MockGateway{ctrl: ctrl} + mock.recorder = &MockGatewayMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockGateway) EXPECT() *MockGatewayMockRecorder { + return m.recorder +} + +// SearchDistinctValues mocks base method +func (m *MockGateway) SearchDistinctValues(arg0 context.Context, arg1, arg2 string) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchDistinctValues", arg0, arg1, arg2) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchDistinctValues indicates an expected call of SearchDistinctValues +func (mr *MockGatewayMockRecorder) SearchDistinctValues(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchDistinctValues", reflect.TypeOf((*MockGateway)(nil).SearchDistinctValues), arg0, arg1, arg2) +} diff --git a/cli/internal/gateway/es/testdata/search_result.json b/cli/internal/gateway/es/testdata/search_result.json new file mode 100644 index 00000000..7b1e47f2 --- /dev/null +++ b/cli/internal/gateway/es/testdata/search_result.json @@ -0,0 +1,35 @@ +{ + "took": 80, + "timed_out": false, + "_shards": { + "total": 5, + "successful": 5, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": 14, + "max_score": 0, + "hits": [] + }, + "aggregations": { + "items": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "Packaged Foods", + "doc_count": 4 + }, + { + "key": "Dairy", + "doc_count": 3 + }, + { + "key": "Meat and Seafood", + "doc_count": 2 + } + ] + } + } +} \ No newline at end of file diff --git a/cli/internal/gateway/gateway.go b/cli/internal/gateway/gateway.go new file mode 100644 index 00000000..aa36ffcf --- /dev/null +++ b/cli/internal/gateway/gateway.go @@ -0,0 +1,103 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package gateway + +import ( + "bytes" + "context" + "encoding/json" + "esad/internal/client" + "fmt" + "github.com/hashicorp/go-retryablehttp" + "io/ioutil" + "net/url" +) + +//HTTPGateway type for gateway client +type HTTPGateway struct { + Client *client.Client + UserConfig *client.UserConfig +} + +//GetHeaders returns common headers +func GetHeaders() map[string]string { + return map[string]string{ + "Content-Type": "application/json", + } +} + +//NewHTTPGateway creats new HTTPGateway instance +func NewHTTPGateway(c *client.Client, u *client.UserConfig) *HTTPGateway { + return &HTTPGateway{ + Client: c, + UserConfig: u, + } +} + +//Call calls request using http +func (g *HTTPGateway) Call(req *retryablehttp.Request, statusCode int) ([]byte, error) { + + res, err := g.Client.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer func() { + err := res.Body.Close() + if err != nil { + return + } + }() + resBytes, _ := ioutil.ReadAll(res.Body) + if res.StatusCode != statusCode { + return nil, fmt.Errorf("%s", string(resBytes)) + } + return resBytes, nil + +} + +//BuildRequest builds request based on method and appends payload for given url with headers +func (g *HTTPGateway) BuildRequest(ctx context.Context, method string, payload interface{}, url string, headers map[string]string) (*retryablehttp.Request, error) { + reqBytes, _ := json.Marshal(payload) + reqReader := bytes.NewReader(reqBytes) + r, err := retryablehttp.NewRequest(method, url, reqReader) + if err != nil { + return nil, err + } + req := r.WithContext(ctx) + if len(g.UserConfig.Username) == 0 || len(g.UserConfig.Password) == 0 { + return nil, fmt.Errorf("user name and password cannot be empty") + } + req.SetBasicAuth(g.UserConfig.Username, g.UserConfig.Password) + if len(headers) == 0 { + return req, nil + } + for key, value := range headers { + req.Header.Set(key, value) + } + return req, nil +} + +//GetValidEndpoint get url based on user config +func GetValidEndpoint(userConfig *client.UserConfig) (*url.URL, error) { + if len(userConfig.Endpoint) == 0 { + return &url.URL{ + Scheme: "https", + Host: "localhost:9200", + }, nil + } + u, err := url.Parse(userConfig.Endpoint) + if err != nil { + return nil, fmt.Errorf("invalid endpoint: %v due to %v", userConfig.Endpoint, err) + } + return u, nil +} diff --git a/cli/internal/handler/ad/ad.go b/cli/internal/handler/ad/ad.go new file mode 100644 index 00000000..6fac3cff --- /dev/null +++ b/cli/internal/handler/ad/ad.go @@ -0,0 +1,194 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package ad + +import ( + "context" + "encoding/json" + controller "esad/internal/controller/ad" + entity "esad/internal/entity/ad" + mapper2 "esad/internal/mapper" + "fmt" + "io/ioutil" + "os" +) + +//Handler is facade for controller +type Handler struct { + controller.Controller +} + +// New returns new Handler instance +func New(controller controller.Controller) *Handler { + return &Handler{ + controller, + } +} + +//CreateAnomalyDetector creates detector based on file configurations +func CreateAnomalyDetector(h *Handler, fileName string, interactive bool) error { + return h.CreateAnomalyDetector(fileName, interactive) +} + +//GenerateAnomalyDetector generate sample detector to provide skeleton for users +func GenerateAnomalyDetector() ([]byte, error) { + return json.MarshalIndent(entity.CreateDetectorRequest{ + Name: "Detector Name", + Description: "A brief description", + TimeField: "", + Index: []string{}, + Features: []entity.FeatureRequest{ + { + AggregationType: []string{"count"}, + Enabled: false, + Field: []string{}, + }, + }, + Filter: []byte("{}"), + Interval: "10m", + Delay: "1m", + Start: false, + PartitionField: mapper2.StringToStringPtr(""), + }, "", " ") +} + +//CreateAnomalyDetector creates detector based on file configurations +func (h *Handler) CreateAnomalyDetector(fileName string, interactive bool) error { + if len(fileName) < 1 { + return fmt.Errorf("file name cannot be empty") + } + + jsonFile, err := os.Open(fileName) + if err != nil { + return fmt.Errorf("failed to open file %s due to %v", fileName, err) + } + defer func() { + err := jsonFile.Close() + if err != nil { + fmt.Println("failed close json file due to ", err) + } + }() + byteValue, _ := ioutil.ReadAll(jsonFile) + var request entity.CreateDetectorRequest + err = json.Unmarshal(byteValue, &request) + if err != nil { + return fmt.Errorf("file %s cannot be accepted due to %v", fileName, err) + } + ctx := context.Background() + names, err := h.CreateMultiEntityAnomalyDetector(ctx, request, interactive, true) + if err != nil { + return err + } + if len(names) > 0 { + fmt.Printf("Successfully created %d detector(s)", len(names)) + fmt.Println() + return nil + } + return err +} + +//DeleteAnomalyDetectorByID deletes detector based on detectorId +func DeleteAnomalyDetectorByID(h *Handler, detectorID string, force bool) error { + return h.DeleteAnomalyDetectorByID(detectorID, force) +} + +//DeleteAnomalyDetectorByID deletes detector based on detectorId +func (h *Handler) DeleteAnomalyDetectorByID(detectorID string, force bool) error { + + ctx := context.Background() + err := h.DeleteDetector(ctx, detectorID, true, force) + if err != nil { + return err + } + return err +} + +//DeleteAnomalyDetectorByNamePattern deletes detector based on detectorName +func DeleteAnomalyDetectorByNamePattern(h *Handler, detectorName string, force bool) error { + return h.DeleteAnomalyDetectorByNamePattern(detectorName, force) +} + +//DeleteAnomalyDetectorByNamePattern deletes detector based on detectorName +func (h *Handler) DeleteAnomalyDetectorByNamePattern(detectorName string, force bool) error { + + ctx := context.Background() + err := h.DeleteDetectorByName(ctx, detectorName, force, true) + if err != nil { + return err + } + return err +} + +//StartAnomalyDetectorByID starts detector based on detector id +func StartAnomalyDetectorByID(h *Handler, detector string) error { + return h.StartAnomalyDetectorByID(detector) +} + +//StartAnomalyDetectorByID starts detector based on detector id +func (h *Handler) StartAnomalyDetectorByID(detector string) error { + + ctx := context.Background() + err := h.StartDetector(ctx, detector) + if err != nil { + return err + } + return nil +} + +// StartAnomalyDetectorByNamePattern starts detector based on detector name pattern +func StartAnomalyDetectorByNamePattern(h *Handler, detector string) error { + return h.StartAnomalyDetectorByNamePattern(detector) +} + +// StartAnomalyDetectorByNamePattern starts detector based on detector name pattern +func (h *Handler) StartAnomalyDetectorByNamePattern(detector string) error { + + ctx := context.Background() + err := h.StartDetectorByName(ctx, detector, true) + if err != nil { + return err + } + return nil +} + +// StopAnomalyDetectorByNamePattern stops detector based on detector name pattern +func StopAnomalyDetectorByNamePattern(h *Handler, detector string) error { + return h.StopAnomalyDetectorByNamePattern(detector) +} + +// StopAnomalyDetectorByNamePattern stops detector based on detector name pattern +func (h *Handler) StopAnomalyDetectorByNamePattern(detector string) error { + + ctx := context.Background() + err := h.StopDetectorByName(ctx, detector, true) + if err != nil { + return err + } + return nil +} + +// StopAnomalyDetectorByID stops detector based on detector id +func StopAnomalyDetectorByID(h *Handler, detector string) error { + return h.StopAnomalyDetectorByID(detector) +} + +// StopAnomalyDetectorByID stops detector based on detector id +func (h *Handler) StopAnomalyDetectorByID(detector string) error { + + ctx := context.Background() + err := h.StopDetector(ctx, detector) + if err != nil { + return err + } + return nil +} diff --git a/cli/internal/handler/ad/ad_test.go b/cli/internal/handler/ad/ad_test.go new file mode 100644 index 00000000..34dfdd81 --- /dev/null +++ b/cli/internal/handler/ad/ad_test.go @@ -0,0 +1,218 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package ad + +import ( + "context" + "encoding/json" + "errors" + "esad/internal/controller/ad/mocks" + "esad/internal/entity/ad" + "esad/internal/mapper" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "testing" +) + +func getRawFilter() []byte { + return []byte(`{ + "bool": { + "filter": { + "term": { + "currency": "EUR" + } + }} + }`) +} + +func getCreateDetectorRequest() ad.CreateDetectorRequest { + return ad.CreateDetectorRequest{ + Name: "test-detector-ecommerce0", + Description: "Test detector", + TimeField: "utc_time", + Index: []string{"kibana_sample_data_ecommerce*"}, + Features: []ad.FeatureRequest{{ + AggregationType: []string{"sum", "average"}, + Enabled: true, + Field: []string{"total_quantity"}, + }}, + Filter: getRawFilter(), + Interval: "1m", + Delay: "1m", + Start: true, + PartitionField: mapper.StringToStringPtr("day_of_week"), + } +} +func TestHandler_CreateAnomalyDetector(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + t.Run("test create success", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().CreateMultiEntityAnomalyDetector(ctx, getCreateDetectorRequest(), false, true).Return([]string{"test-detector-ecommerce0-one"}, nil) + instance := New(mockedController) + err := CreateAnomalyDetector(instance, "testdata/create.json", false) + assert.NoError(t, err) + }) + t.Run("test create failure", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().CreateMultiEntityAnomalyDetector(ctx, getCreateDetectorRequest(), false, true).Return(nil, errors.New("failed to create")) + instance := New(mockedController) + err := CreateAnomalyDetector(instance, "testdata/create.json", false) + assert.EqualError(t, err, "failed to create") + }) + t.Run("test create failure due to invalid file", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + instance := New(mockedController) + err := CreateAnomalyDetector(instance, "testdata/create1.json", false) + assert.EqualError(t, err, "failed to open file testdata/create1.json due to open testdata/create1.json: no such file or directory") + }) + t.Run("test create failure due to empty file", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + instance := New(mockedController) + err := CreateAnomalyDetector(instance, "", false) + assert.EqualError(t, err, "file name cannot be empty") + }) + t.Run("test create failure due to invalid file", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + instance := New(mockedController) + err := CreateAnomalyDetector(instance, "testdata/invalid.txt", false) + assert.EqualError(t, err, "file testdata/invalid.txt cannot be accepted due to invalid character 'i' looking for beginning of value") + }) +} +func TestHandler_DeleteAnomalyDetector(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + t.Run("test delete success", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().DeleteDetectorByName(ctx, "detector", false, true).Return(nil) + instance := New(mockedController) + err := DeleteAnomalyDetectorByNamePattern(instance, "detector", false) + assert.NoError(t, err) + }) + t.Run("test delete failure", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().DeleteDetectorByName(ctx, "detector", false, true).Return(errors.New("failed to delete")) + instance := New(mockedController) + err := DeleteAnomalyDetectorByNamePattern(instance, "detector", false) + assert.EqualError(t, err, "failed to delete") + }) +} + +func TestHandler_StartAnomalyDetector(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + t.Run("test start success", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().StartDetectorByName(ctx, "detector", true).Return(nil) + instance := New(mockedController) + err := StartAnomalyDetectorByNamePattern(instance, "detector") + assert.NoError(t, err) + }) + t.Run("test start failure", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().StartDetectorByName(ctx, "detector", true).Return(errors.New("failed to start")) + instance := New(mockedController) + err := instance.StartAnomalyDetectorByNamePattern("detector") + assert.EqualError(t, err, "failed to start") + }) +} + +func TestHandler_StopAnomalyDetector(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + t.Run("test stop success", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().StopDetectorByName(ctx, "detector", true).Return(nil) + instance := New(mockedController) + err := StopAnomalyDetectorByNamePattern(instance, "detector") + assert.NoError(t, err) + }) + t.Run("test stop failure", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().StopDetectorByName(ctx, "detector", true).Return(errors.New("failed to stop")) + instance := New(mockedController) + err := instance.StopAnomalyDetectorByNamePattern("detector") + assert.EqualError(t, err, "failed to stop") + }) +} +func TestHandler_StartAnomalyDetectorByID(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + t.Run("test start success", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().StartDetector(ctx, "detector").Return(nil) + instance := New(mockedController) + err := StartAnomalyDetectorByID(instance, "detector") + assert.NoError(t, err) + }) + t.Run("test start failure", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().StartDetector(ctx, "detector").Return(errors.New("failed to start")) + instance := New(mockedController) + err := instance.StartAnomalyDetectorByID("detector") + assert.EqualError(t, err, "failed to start") + }) +} + +func TestHandler_StopAnomalyDetectorByID(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + t.Run("test stop success", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().StopDetector(ctx, "detector").Return(nil) + instance := New(mockedController) + err := StopAnomalyDetectorByID(instance, "detector") + assert.NoError(t, err) + }) + t.Run("test stop failure", func(t *testing.T) { + mockedController := mocks.NewMockController(mockCtrl) + mockedController.EXPECT().StopDetector(ctx, "detector").Return(errors.New("failed to stop")) + instance := New(mockedController) + err := instance.StopAnomalyDetectorByID("detector") + assert.EqualError(t, err, "failed to stop") + }) +} + +func TestGenerateAnomalyDetector(t *testing.T) { + t.Run("test generate success", func(t *testing.T) { + expected := ad.CreateDetectorRequest{ + Name: "Detector Name", + Description: "A brief description", + TimeField: "", + Index: []string{}, + Features: []ad.FeatureRequest{ + { + AggregationType: []string{"count"}, + Enabled: false, + Field: []string{}, + }, + }, + Filter: []byte("{}"), + Interval: "10m", + Delay: "1m", + Start: false, + PartitionField: mapper.StringToStringPtr(""), + } + res, err := GenerateAnomalyDetector() + assert.NoError(t, err) + var actual ad.CreateDetectorRequest + assert.NoError(t, json.Unmarshal(res, &actual)) + assert.EqualValues(t, expected, actual) + }) +} diff --git a/cli/internal/handler/ad/testdata/create.json b/cli/internal/handler/ad/testdata/create.json new file mode 100644 index 00000000..808a18e3 --- /dev/null +++ b/cli/internal/handler/ad/testdata/create.json @@ -0,0 +1,23 @@ +{ + "name": "test-detector-ecommerce0", + "description": "Test detector", + "time_field": "utc_time", + "index": ["kibana_sample_data_ecommerce*"], + "features": [{ + "aggregation_type": ["sum", "average"], + "enabled": true, + "field":["total_quantity"] + }], + "filter": { + "bool": { + "filter": { + "term": { + "currency": "EUR" + } + }} + }, + "interval": "1m", + "window_delay": "1m", + "start": true, + "partition_field": "day_of_week" +} \ No newline at end of file diff --git a/cli/internal/handler/ad/testdata/invalid.txt b/cli/internal/handler/ad/testdata/invalid.txt new file mode 100644 index 00000000..2aa3af9e --- /dev/null +++ b/cli/internal/handler/ad/testdata/invalid.txt @@ -0,0 +1 @@ +invalid content \ No newline at end of file diff --git a/cli/internal/mapper/ad/ad.go b/cli/internal/mapper/ad/ad.go new file mode 100644 index 00000000..2167ae54 --- /dev/null +++ b/cli/internal/mapper/ad/ad.go @@ -0,0 +1,186 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package ad + +import ( + "encoding/json" + "esad/internal/entity/ad" + "esad/internal/mapper" + "fmt" + "regexp" + "strconv" + "strings" +) + +const ( + featureCountLimit = 5 + minutesKey = "m" + minutes = "Minutes" +) + +func getFeatureAggregationQuery(name string, agg string, field string) ([]byte, error) { + + userTypeToESType := make(map[string]string) + userTypeToESType["average"] = "avg" + userTypeToESType["count"] = "value_count" + userTypeToESType["sum"] = "sum" + userTypeToESType["min"] = "min" + userTypeToESType["max"] = "max" + val, ok := userTypeToESType[strings.ToLower(agg)] + if !ok { + var allowedTypes []string + for key := range userTypeToESType { + allowedTypes = append(allowedTypes, key) + } + return nil, fmt.Errorf("invlaid aggeration type: '%s', only allowed types are: %s ", agg, strings.Join(allowedTypes, ",")) + } + agg = val + return []byte(fmt.Sprintf(`{ + "%s": { + "%s": { + "field": "%s" + } + } + }`, name, agg, field)), nil +} +func mapToFeature(r ad.FeatureRequest) ([]ad.Feature, error) { + var features []ad.Feature + for _, t := range r.AggregationType { + for _, f := range r.Field { + name := fmt.Sprintf("%s_%s", t, f) + query, err := getFeatureAggregationQuery(name, t, f) + if err != nil { + return nil, err + } + features = append(features, ad.Feature{ + Name: name, + Enabled: r.Enabled, + AggregationQuery: query, + }) + } + } + return features, nil +} + +func getUnit(request string) (*string, error) { + + //extract last character + unit := strings.ToLower(request[len(request)-1:]) + if unit != minutesKey { + return nil, fmt.Errorf("invlaid unit: '%v' in %v, only %s (%s) is supported", unit, request, minutesKey, minutes) + } + return mapper.StringToStringPtr(minutes), nil +} + +func getDuration(request string) (*int32, error) { + //extract last but one character + duration, err := strconv.Atoi(request[:len(request)-1]) + if err != nil { + return nil, fmt.Errorf("invlaid duration: %v, due to {%v}", request, err) + } + if duration < 0 { + return nil, fmt.Errorf("duration must be positive integer") + } + return mapper.IntToInt32Ptr(duration) +} + +func mapToInterval(request string) (*ad.Interval, error) { + if len(request) < 2 { + return nil, fmt.Errorf("invalid format: %s", request) + } + duration, err := getDuration(request) + if err != nil { + return nil, err + } + unit, err := getUnit(request) + if err != nil { + return nil, err + } + return &ad.Interval{ + Period: ad.Period{ + Duration: mapper.Int32PtrToInt32(duration), + Unit: mapper.StringPtrToString(unit), + }, + }, nil +} + +//MapToCreateDetector maps to CreateDetector +func MapToCreateDetector(request ad.CreateDetectorRequest) (*ad.CreateDetector, error) { + + var features []ad.Feature + err := validateFeatureLimit(request.Features) + if err != nil { + return nil, err + } + for _, f := range request.Features { + ftr, err := mapToFeature(f) + if err != nil { + return nil, err + } + features = append(features, ftr...) + } + + interval, err := mapToInterval(request.Interval) + if err != nil { + return nil, err + } + delay, err := mapToInterval(request.Delay) + if err != nil { + return nil, err + } + return &ad.CreateDetector{ + Name: request.Name, + Description: request.Description, + TimeField: request.TimeField, + Index: request.Index, + Features: features, + Filter: request.Filter, + Interval: *interval, + Delay: *delay, + }, nil +} + +func validateFeatureLimit(features []ad.FeatureRequest) error { + featureCount := 0 + for _, f := range features { + featureCount += len(f.AggregationType) * len(f.Field) + } + if featureCount == 0 || featureCount > featureCountLimit { + return fmt.Errorf("trying to create %d feautres, only upto %d features are allowed", featureCount, featureCountLimit) + } + return nil +} + +//MapToDetectors maps response to detectors +func MapToDetectors(searchResponse []byte, name string) ([]ad.Detector, error) { + var data ad.SearchResponse + err := json.Unmarshal(searchResponse, &data) + if err != nil { + return nil, err + } + var result []ad.Detector + processedNameAnyCharacter := strings.ReplaceAll(name, "*", "(.*)") + processedName := strings.ReplaceAll(processedNameAnyCharacter, "+", "(.+)") + + r, _ := regexp.Compile(fmt.Sprintf("^%s$", processedName)) + for _, detector := range data.Hits.Hits { + if !r.MatchString(detector.Source.Name) { + continue + } + result = append(result, ad.Detector{ + Name: detector.Source.Name, + ID: detector.ID, + }) + } + return result, nil +} diff --git a/cli/internal/mapper/ad/ad_test.go b/cli/internal/mapper/ad/ad_test.go new file mode 100644 index 00000000..c7d1dd4f --- /dev/null +++ b/cli/internal/mapper/ad/ad_test.go @@ -0,0 +1,197 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package ad + +import ( + "esad/internal/entity/ad" + "esad/internal/mapper" + "github.com/stretchr/testify/assert" + "io/ioutil" + "path/filepath" + "testing" +) + +func helperLoadBytes(t *testing.T, name string) []byte { + path := filepath.Join("testdata", name) // relative path + contents, err := ioutil.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return contents +} + +func getRawFilter() []byte { + return []byte(`{ + "bool": { + "filter": [{ + "exists": { + "field": "value", + "boost": 1 + } + }], + "adjust_pure_negative": true, + "boost": 1 + } +}`) +} + +func getRawFeatureAggregation() []byte { + return []byte(`{ + "sum_order": { + "sum": { + "field": "order" + } + } + }`) +} + +func getCreateDetector() ad.CreateDetector { + return ad.CreateDetector{ + Name: "testdata-detector", + Description: "Test detector", + TimeField: "timestamp", + Index: []string{"order*"}, + Features: []ad.Feature{ + { + Name: "sum_order", + Enabled: true, + AggregationQuery: getRawFeatureAggregation(), + }, + }, + Filter: getRawFilter(), + Interval: ad.Interval{ + Period: ad.Period{ + Duration: 1, + Unit: "Minutes", + }, + }, + Delay: ad.Interval{ + Period: ad.Period{ + Duration: 1, + Unit: "Minutes", + }, + }, + } +} + +func getCreateDetectorRequest(interval string, delay string) ad.CreateDetectorRequest { + return ad.CreateDetectorRequest{ + Name: "testdata-detector", + Description: "Test detector", + TimeField: "timestamp", + Index: []string{"order*"}, + Features: []ad.FeatureRequest{{ + AggregationType: []string{"sum"}, + Enabled: true, + Field: []string{"order"}, + }}, + Filter: getRawFilter(), + Interval: interval, + Delay: delay, + Start: true, + PartitionField: mapper.StringToStringPtr("ip"), + } +} +func TestMapToCreateDetector(t *testing.T) { + t.Run("Success: Valid Input", func(t *testing.T) { + r := getCreateDetectorRequest("1m", "1m") + actual, err := MapToCreateDetector(r) + expected := getCreateDetector() + assert.NoError(t, err) + assert.EqualValues(t, expected, *actual) + }) + t.Run("Failure: interval val", func(t *testing.T) { + r := getCreateDetectorRequest("m1", "1m") + _, err := MapToCreateDetector(r) + assert.Error(t, err) + }) + t.Run("Failure: interval unit", func(t *testing.T) { + r := getCreateDetectorRequest("1", "1m") + _, err := MapToCreateDetector(r) + assert.Error(t, err) + }) + t.Run("Failure: interval wrong unit", func(t *testing.T) { + r := getCreateDetectorRequest("1y", "1m") + _, err := MapToCreateDetector(r) + assert.Error(t, err) + }) + t.Run("Failure: window delay val", func(t *testing.T) { + r := getCreateDetectorRequest("1m", "m1") + _, err := MapToCreateDetector(r) + assert.Error(t, err) + }) + t.Run("Failure: window delay unit", func(t *testing.T) { + r := getCreateDetectorRequest("1m", "1") + _, err := MapToCreateDetector(r) + assert.Error(t, err) + }) + t.Run("Failure: window delay wrong unit", func(t *testing.T) { + r := getCreateDetectorRequest("1m", "1y") + _, err := MapToCreateDetector(r) + assert.Error(t, err) + }) +} + +func TestMapToDetectors(t *testing.T) { + t.Run("filter detectors", func(t *testing.T) { + actual, err := MapToDetectors(helperLoadBytes(t, "search_response.json"), "test-detector-ecommerce0-T*") + expected := []ad.Detector{ + { + Name: "test-detector-ecommerce0-Tuesday", + ID: "6lh0bnMBLlLTlH7nz4iE", + }, + { + Name: "test-detector-ecommerce0-Thursday", + ID: "ylh0bnMBLlLTlH7nzohq", + }, + } + assert.Nil(t, err) + assert.ElementsMatch(t, expected, actual) + }) + t.Run("filter detectors for any ", func(t *testing.T) { + actual, err := MapToDetectors(helperLoadBytes(t, "search_response.json"), "test-detector-ecommerce0-T*") + expected := []ad.Detector{ + { + Name: "test-detector-ecommerce0-Tuesday", + ID: "6lh0bnMBLlLTlH7nz4iE", + }, + { + Name: "test-detector-ecommerce0-Thursday", + ID: "ylh0bnMBLlLTlH7nzohq", + }, + } + assert.Nil(t, err) + assert.ElementsMatch(t, expected, actual) + }) + t.Run("filter detectors for at least", func(t *testing.T) { + actual, err := MapToDetectors(helperLoadBytes(t, "search_response.json"), "test-detector-ecommerce0-Tuesday+") + assert.Nil(t, err) + assert.ElementsMatch(t, []ad.Detector{}, actual) + }) + t.Run("filter detectors for exact", func(t *testing.T) { + actual, err := MapToDetectors(helperLoadBytes(t, "search_response.json"), "test-detector-ecommerce0-Tuesday") + expected := []ad.Detector{ + { + Name: "test-detector-ecommerce0-Tuesday", + ID: "6lh0bnMBLlLTlH7nz4iE", + }, + } + assert.Nil(t, err) + assert.ElementsMatch(t, expected, actual) + }) + t.Run("filter detectors for no match", func(t *testing.T) { + actual, err := MapToDetectors(helperLoadBytes(t, "search_response.json"), "test-detector-ecommerce0-Tuesda") + assert.Nil(t, err) + assert.ElementsMatch(t, []ad.Detector{}, actual) + }) +} diff --git a/cli/internal/mapper/ad/testdata/search_response.json b/cli/internal/mapper/ad/testdata/search_response.json new file mode 100644 index 00000000..27c271eb --- /dev/null +++ b/cli/internal/mapper/ad/testdata/search_response.json @@ -0,0 +1,698 @@ +{ + "took" : 6, + "timed_out" : false, + "_shards" : { + "total" : 1, + "successful" : 1, + "skipped" : 0, + "failed" : 0 + }, + "hits" : { + "total" : { + "value" : 7, + "relation" : "eq" + }, + "max_score" : 0.06453852, + "hits" : [ + { + "_index" : ".opendistro-anomaly-detectors", + "_type" : "_doc", + "_id" : "ylh0bnMBLlLTlH7nzohq", + "_version" : 1, + "_seq_no" : 494, + "_primary_term" : 4, + "_score" : 0.06453852, + "_source" : { + "name" : "test-detector-ecommerce0-Thursday", + "description" : "Test detector", + "time_field" : "utc_time", + "indices" : [ + "kibana_sample_data_ecommerce*" + ], + "filter_query" : { + "bool" : { + "must" : [ + { + "bool" : { + "filter" : [ + { + "term" : { + "day_of_week" : { + "value" : "Thursday", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + { + "bool" : { + "filter" : [ + { + "term" : { + "currency" : { + "value" : "EUR", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + "detection_interval" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "window_delay" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "schema_version" : 0, + "feature_attributes" : [ + { + "feature_id" : "xVh0bnMBLlLTlH7nzohm", + "feature_name" : "sum_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "sum_total_quantity" : { + "sum" : { + "field" : "total_quantity" + } + } + } + }, + { + "feature_id" : "xlh0bnMBLlLTlH7nzohm", + "feature_name" : "average_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "average_total_quantity" : { + "avg" : { + "field" : "total_quantity" + } + } + } + } + ], + "last_update_time" : 1595286015594 + } + }, + { + "_index" : ".opendistro-anomaly-detectors", + "_type" : "_doc", + "_id" : "0lh0bnMBLlLTlH7nzoi2", + "_version" : 1, + "_seq_no" : 495, + "_primary_term" : 4, + "_score" : 0.06453852, + "_source" : { + "name" : "test-detector-ecommerce0-Friday", + "description" : "Test detector", + "time_field" : "utc_time", + "indices" : [ + "kibana_sample_data_ecommerce*" + ], + "filter_query" : { + "bool" : { + "must" : [ + { + "bool" : { + "filter" : [ + { + "term" : { + "day_of_week" : { + "value" : "Friday", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + { + "bool" : { + "filter" : [ + { + "term" : { + "currency" : { + "value" : "EUR", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + "detection_interval" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "window_delay" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "schema_version" : 0, + "feature_attributes" : [ + { + "feature_id" : "zVh0bnMBLlLTlH7nzoix", + "feature_name" : "sum_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "sum_total_quantity" : { + "sum" : { + "field" : "total_quantity" + } + } + } + }, + { + "feature_id" : "zlh0bnMBLlLTlH7nzoiy", + "feature_name" : "average_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "average_total_quantity" : { + "avg" : { + "field" : "total_quantity" + } + } + } + } + ], + "last_update_time" : 1595286015669 + } + }, + { + "_index" : ".opendistro-anomaly-detectors", + "_type" : "_doc", + "_id" : "2lh0bnMBLlLTlH7nzoj4", + "_version" : 1, + "_seq_no" : 496, + "_primary_term" : 4, + "_score" : 0.06453852, + "_source" : { + "name" : "test-detector-ecommerce0-Saturday", + "description" : "Test detector", + "time_field" : "utc_time", + "indices" : [ + "kibana_sample_data_ecommerce*" + ], + "filter_query" : { + "bool" : { + "must" : [ + { + "bool" : { + "filter" : [ + { + "term" : { + "day_of_week" : { + "value" : "Saturday", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + { + "bool" : { + "filter" : [ + { + "term" : { + "currency" : { + "value" : "EUR", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + "detection_interval" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "window_delay" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "schema_version" : 0, + "feature_attributes" : [ + { + "feature_id" : "1Vh0bnMBLlLTlH7nzojz", + "feature_name" : "sum_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "sum_total_quantity" : { + "sum" : { + "field" : "total_quantity" + } + } + } + }, + { + "feature_id" : "1lh0bnMBLlLTlH7nzojz", + "feature_name" : "average_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "average_total_quantity" : { + "avg" : { + "field" : "total_quantity" + } + } + } + } + ], + "last_update_time" : 1595286015735 + } + }, + { + "_index" : ".opendistro-anomaly-detectors", + "_type" : "_doc", + "_id" : "4lh0bnMBLlLTlH7nz4g8", + "_version" : 1, + "_seq_no" : 497, + "_primary_term" : 4, + "_score" : 0.06453852, + "_source" : { + "name" : "test-detector-ecommerce0-Sunday", + "description" : "Test detector", + "time_field" : "utc_time", + "indices" : [ + "kibana_sample_data_ecommerce*" + ], + "filter_query" : { + "bool" : { + "must" : [ + { + "bool" : { + "filter" : [ + { + "term" : { + "day_of_week" : { + "value" : "Sunday", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + { + "bool" : { + "filter" : [ + { + "term" : { + "currency" : { + "value" : "EUR", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + "detection_interval" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "window_delay" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "schema_version" : 0, + "feature_attributes" : [ + { + "feature_id" : "3Vh0bnMBLlLTlH7nz4g4", + "feature_name" : "sum_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "sum_total_quantity" : { + "sum" : { + "field" : "total_quantity" + } + } + } + }, + { + "feature_id" : "3lh0bnMBLlLTlH7nz4g4", + "feature_name" : "average_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "average_total_quantity" : { + "avg" : { + "field" : "total_quantity" + } + } + } + } + ], + "last_update_time" : 1595286015803 + } + }, + { + "_index" : ".opendistro-anomaly-detectors", + "_type" : "_doc", + "_id" : "6lh0bnMBLlLTlH7nz4iE", + "_version" : 1, + "_seq_no" : 498, + "_primary_term" : 4, + "_score" : 0.06453852, + "_source" : { + "name" : "test-detector-ecommerce0-Tuesday", + "description" : "Test detector", + "time_field" : "utc_time", + "indices" : [ + "kibana_sample_data_ecommerce*" + ], + "filter_query" : { + "bool" : { + "must" : [ + { + "bool" : { + "filter" : [ + { + "term" : { + "day_of_week" : { + "value" : "Tuesday", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + { + "bool" : { + "filter" : [ + { + "term" : { + "currency" : { + "value" : "EUR", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + "detection_interval" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "window_delay" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "schema_version" : 0, + "feature_attributes" : [ + { + "feature_id" : "5Vh0bnMBLlLTlH7nz4iA", + "feature_name" : "sum_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "sum_total_quantity" : { + "sum" : { + "field" : "total_quantity" + } + } + } + }, + { + "feature_id" : "5lh0bnMBLlLTlH7nz4iA", + "feature_name" : "average_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "average_total_quantity" : { + "avg" : { + "field" : "total_quantity" + } + } + } + } + ], + "last_update_time" : 1595286015876 + } + }, + { + "_index" : ".opendistro-anomaly-detectors", + "_type" : "_doc", + "_id" : "8lh0bnMBLlLTlH7nz4jT", + "_version" : 1, + "_seq_no" : 499, + "_primary_term" : 4, + "_score" : 0.06453852, + "_source" : { + "name" : "test-detector-ecommerce0-Wednesday", + "description" : "Test detector", + "time_field" : "utc_time", + "indices" : [ + "kibana_sample_data_ecommerce*" + ], + "filter_query" : { + "bool" : { + "must" : [ + { + "bool" : { + "filter" : [ + { + "term" : { + "day_of_week" : { + "value" : "Wednesday", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + { + "bool" : { + "filter" : [ + { + "term" : { + "currency" : { + "value" : "EUR", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + "detection_interval" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "window_delay" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "schema_version" : 0, + "feature_attributes" : [ + { + "feature_id" : "7Vh0bnMBLlLTlH7nz4jP", + "feature_name" : "sum_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "sum_total_quantity" : { + "sum" : { + "field" : "total_quantity" + } + } + } + }, + { + "feature_id" : "7lh0bnMBLlLTlH7nz4jQ", + "feature_name" : "average_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "average_total_quantity" : { + "avg" : { + "field" : "total_quantity" + } + } + } + } + ], + "last_update_time" : 1595286015955 + } + }, + { + "_index" : ".opendistro-anomaly-detectors", + "_type" : "_doc", + "_id" : "-lh0bnMBLlLTlH7n0IgT", + "_version" : 1, + "_seq_no" : 500, + "_primary_term" : 4, + "_score" : 0.06453852, + "_source" : { + "name" : "test-detector-ecommerce0-Monday", + "description" : "Test detector", + "time_field" : "utc_time", + "indices" : [ + "kibana_sample_data_ecommerce*" + ], + "filter_query" : { + "bool" : { + "must" : [ + { + "bool" : { + "filter" : [ + { + "term" : { + "day_of_week" : { + "value" : "Monday", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + { + "bool" : { + "filter" : [ + { + "term" : { + "currency" : { + "value" : "EUR", + "boost" : 1.0 + } + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + } + ], + "adjust_pure_negative" : true, + "boost" : 1.0 + } + }, + "detection_interval" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "window_delay" : { + "period" : { + "interval" : 1, + "unit" : "Minutes" + } + }, + "schema_version" : 0, + "feature_attributes" : [ + { + "feature_id" : "9Vh0bnMBLlLTlH7n0IgP", + "feature_name" : "sum_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "sum_total_quantity" : { + "sum" : { + "field" : "total_quantity" + } + } + } + }, + { + "feature_id" : "9lh0bnMBLlLTlH7n0IgP", + "feature_name" : "average_total_quantity", + "feature_enabled" : true, + "aggregation_query" : { + "average_total_quantity" : { + "avg" : { + "field" : "total_quantity" + } + } + } + } + ], + "last_update_time" : 1595286016019 + } + } + ] + } +} diff --git a/cli/internal/mapper/mapper.go b/cli/internal/mapper/mapper.go new file mode 100644 index 00000000..a88c82b4 --- /dev/null +++ b/cli/internal/mapper/mapper.go @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +// Package mapper provides a collection of simple mapper functions. +package mapper + +import ( + "fmt" + "math" +) + +// IntToInt32 maps an int to an int32. +func IntToInt32(r int) (int32, error) { + if r < math.MinInt32 || r > math.MaxInt32 { + return 0, fmt.Errorf("integer overflow, cannot map %d to int32", r) + } + return int32(r), nil +} + +// IntToInt32Ptr maps an int to an *int32. +func IntToInt32Ptr(r int) (*int32, error) { + rr, err := IntToInt32(r) + return &rr, err +} + +// Int32PtrToInt32 maps an *int32 to an int32, +// defaulting to 0 if the pointer is nil. +func Int32PtrToInt32(r *int32) int32 { + if r == nil { + return 0 + } + return *r +} + +// StringToStringPtr maps a string to a *string. +func StringToStringPtr(r string) *string { + return &r +} + +// StringPtrToString maps a *string to a string, +// defaulting to "" if the pointer is nil. +func StringPtrToString(r *string) string { + if r == nil { + return "" + } + return *r +} diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 00000000..5ac6a4ff --- /dev/null +++ b/cli/main.go @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +// ESAD is a command lint tool for AD Plugin +package main + +import ( + "esad/cmd" + "os" +) + +func main() { + if err := cmd.Execute(); err != nil { + // By default every command should handle their error message + os.Exit(1) + } +} diff --git a/cli/pkg/version.go b/cli/pkg/version.go new file mode 100644 index 00000000..e62fe012 --- /dev/null +++ b/cli/pkg/version.go @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * http://www.apache.org/licenses/LICENSE-2.0 + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package pkg + +// VERSION represents the version of CLI +var VERSION = "0.1"