Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

AD CLI #196

Merged
merged 23 commits into from
Aug 5, 2020
Merged

AD CLI #196

merged 23 commits into from
Aug 5, 2020

Conversation

VijayanB
Copy link
Member

Merged esad from private repo to anomaly detection plugin.

Issue #, if available:

Description of changes:

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Merged esad from private repo to anomaly detection plugin.
@ylwu-amzn
Copy link
Contributor

Do we plan to use same repo for AD CLI tool?

@VijayanB
Copy link
Member Author

Do we plan to use same repo for AD CLI tool?

Yeah. Recently, SQL CLI is also merged with sql repo.

@VijayanB VijayanB self-assigned this Jul 27, 2020
@VijayanB VijayanB added the feature new feature label Jul 28, 2020
Added build and publish artifacts
Copy link
Contributor

@wnbts wnbts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: why is the go programming language used for cli? if the programming language needs to be different from the rest of the project code, why not python for the equivalent implementation could be simpler?

cli/README.md Outdated
@@ -0,0 +1,97 @@
![Test Workflow](https://github.com/VijayanB/esad/workflows/Build%20and%20Test%20Anomaly%20detection%20commandline%20tool/badge.svg)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor. is this expected?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing it. I removed it for now.

cli/README.md Outdated
# 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.
his 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor. his should be This

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing it. I updated it.

cli/README.md Outdated

## Basic Commands

AN ESAD CLI has following structure
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor. AN should be An

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing it. I updated it in next commit.

}

func createDetectors(fileNames []string, status bool) error {
h, err := getCommandHandler()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion. for readability, refrain from using single letters for variable names.

Copy link
Member Author

@VijayanB VijayanB Jul 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i updated it. Nevertheless, accordingly to go style, (https://github.com/golang/go/wiki/CodeReviewComments#variable-names) it is preferred to use short variable name if it is not used below a page down .

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.`,
//Run: func(cmd *cobra.Command, args []string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor. unused code can be deleted in multiple places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deleted the commented code. Thanks for pointing out.

}
}

func validProfileName(name string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor. the name should be existingprofilename as existing name is not valid for creation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i updated to isProfileExists since it returns bool. But i renamed variable names like you suggested in other place. Can you review it?

}

var safeProfiles []entity.Profile
profiles := getProfiles()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor. retrieving profiles is repeated and can use refactoring.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic to get profiles (load file, deserialization ) is a single method, just that it is being used in multiple use cases. In this particular case, i had split logic to delete actual profile and verify whether the profile is valid or not . This will make logic easy to maintain than having both use cases entangled.

profiles := getProfiles()
for _, p := range profiles {
safe := true
for _, name := range validProfiles {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question. does go have libraries for data structures other than array?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately no. It has map and slice(array). Usually if there is need for processing lot of data, go recommends concurrent programming by spanning multiple threads. My guess is since go is not meant for data processing and keep language simple and opinionated, it doesn't have data structures like java.

@VijayanB
Copy link
Member Author

question: why is the go programming language used for cli? if the programming language needs to be different from the rest of the project code, why not python for the equivalent implementation could be simpler?

I believe, there is a plan to have one cli in long run and at the same time we saw sql cli/ sql odbc individual repos are being integrated into their plugin repo to have single release and simplify maintenance and engage community in the mean time.
Go is being used as modern programming language for latest cli tools like kubectl ( mostly opensource cli ). It has advantages like no runtime is required for the clients to install and use, it is strongly typed like java which is developer friendly and has very good community support.

@VijayanB VijayanB requested a review from wnbts July 29, 2020 22:21
@wnbts
Copy link
Contributor

wnbts commented Jul 30, 2020

question: why is the go programming language used for cli? if the programming language needs to be different from the rest of the project code, why not python for the equivalent implementation could be simpler?

I believe, there is a plan to have one cli in long run and at the same time we saw sql cli/ sql odbc individual repos are being integrated into their plugin repo to have single release and simplify maintenance and engage community in the mean time.
Go is being used as modern programming language for latest cli tools like kubectl ( mostly opensource cli ). It has advantages like no runtime is required for the clients to install and use, it is strongly typed like java which is developer friendly and has very good community support.

thanks for the reasonable answers.

I am neither for nor against the decision. Just for the record, aws cli is based on python. Also, for an open source project, a more popular/familiar programming language might invite more contributions.

const mockDetectorName = "detector"

func helperLoadBytes(t *testing.T, name string) []byte {
path := filepath.Join("testdata", name) // relative path
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question. is it also a go convention to not have a separate directory/module for test code & data?

Copy link
Member Author

@VijayanB VijayanB Jul 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean test file and implementation? Yes, go uses package oriented design, hence code/test/testdata should always be close to each other. There is no guideline but i observed this being used in many of application tool
like gofmt / https://golang.org/src/cmd/gofmt/

@codecov
Copy link

codecov bot commented Jul 30, 2020

Codecov Report

❗ No coverage uploaded for pull request base (master@68429a8). Click here to learn what that means.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff            @@
##             master     #196   +/-   ##
=========================================
  Coverage          ?   79.74%           
=========================================
  Files             ?        6           
  Lines             ?      622           
  Branches          ?        0           
=========================================
  Hits              ?      496           
  Misses            ?       77           
  Partials          ?       49           
Flag Coverage Δ
#unittests 79.74% <0.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 68429a8...7bea3e6. Read the comment docs.

Copy link
Contributor

@wnbts wnbts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion. making pr smaller helps reviewers.

cli/README.md Outdated

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 and Linux, and connect to any valid Elasticsearch end-point such as Amazon Elasticsearch Service (AES).The ESAD CLI implements AD APIs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor. MacOS and Linux and Windows

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack.

suggestion. making pr smaller helps reviewers.

Definitely, i started as proof of concept and later i added features. I will definitely create small commits and multiple PR to make sure that code is easy to review .

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to add "Windows" or this CLI can't support windows?

cli/cmd/stop.go Outdated
if idStatus {
action = ad.StopAnomalyDetectorByID
}
err := execute(action, args)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question. why execute is not needed here but needed in start?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go's package scope allows us to refer to the method outside file as long as it is inside same package. I created one file per command but start and stop shares lot. Now i merged both the command in single file to keep code together.

Comment on lines 81 to 82
if bar != nil {
bar.Incr()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor. should increment be done after an operation is finished?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I updated it. Thanks for pointing it.

Comment on lines 220 to 222
res, err := c.gateway.StopDetector(ctx, id) // ignore error
if err != nil {
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue. the error looks like not ignored.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the comment. Initially i was ignoring but later decided to respect and return the same

Comment on lines 247 to 249
if err != nil {
log.Fatal(err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue. is it safe to continue with a fatal error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. It is not. I updated it.

if len(detectors) < 1 {
return
}
var deleted []entity.Detector
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor. this name should be undeleted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I fixed it now.

- endpoint: https://odfe-node1:9200
username: admin
password: foobar
name: dev
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will happen if I use same name for two profiles. Will throw error or override the prior one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no validation if file is specified by user. We only have validation if user is creating a profile. Let's say if user has two profile with same name and this config is passed as parameter to the command, it will not override instead, it will select the first profile which matches the name.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, so we have error handling for this case. How about add this in readme doc and the help doc?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added it in readme.

```
For example to start detector:
```
$ esad start [detector-name-pattern]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems the commands not follow same name convention. esad profile create looks like esad <resource> <action>, but here esad start [detector-name-pattern] looks like esad <action> <resource>. Is it possible to follow same name convention? Maybe consult Carlos to confirm.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, esad, profile, create, start are commands. You can think like tree structure.
esad -> profile -> create
-> delete
-> Create
-> Start
Anyway, i asked carlos to provide feedback. If i didn't receive anything related to that, i will bring this point.
Thanks for feedback.

```
To get the version of the ESAD CLI:
```
$ esad --version
Copy link
Contributor

@ylwu-amzn ylwu-amzn Aug 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding version compatibility table in the help doc and read-me doc?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I will add it.

return endpoint, nil
}

func (g *gateway) Search(ctx context.Context, index string, field string) ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is to get distinct value of one field. How about we rename this method as "SearchDistinctValue"? So I don't need to read the method implementation to learn this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the name convention for the file name? I see this ES controller named as "controller.go", but AD controller named as "ad.go". And I see five "ad.go" files in these folders: entity, mapper, gateway, handler, controller. Is it common practice in Go to use same file name and differentiate them with package path?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed to es.go. In Go, file name is not as vital as package name. I have seen in most of the popular go repositories, they use file name as same as package name. Yes, package is considered as module than file.

}
}

func (c controller) SearchDetectorByName(ctx context.Context, name string) ([]entity.Detector, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comments for public methods ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACk

return nil
}

func (c controller) StopDetectorByName(ctx context.Context, pattern string, display bool) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: how about move StartDetectorByName method before StopDetectorByName ? This can improve a little bit code readability.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACk.

return c.processDetectorByAction(ctx, pattern, "stop", c.StopDetector, display)
}

//DeleteDetector deletes detector
Copy link
Contributor

@ylwu-amzn ylwu-amzn Aug 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: this simple comment looks like some duplicate of the method name "DeleteDetector". How about add more details like what we will do for force is true/false? Suggest to check other places too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

//go:generate mockgen -destination=mocks/mock_ad.go -package=mocks . Controller

//Controller is an interface for ES Cluster to get distinct values
type Controller interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename it as EsController considering we have AnomalyDetectorController?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From Go Wiki:
All references to names in your package will be done using the package name, so you can omit that name from the identifiers. For example, if you are in package chubby, you don't need type ChubbyFile, which clients will write as chubby.ChubbyFile. Instead, name the type File, which clients will write as chubby.File.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed adController to Controller to keep consistent.

)

const (
baseURL = "_opendistro/_anomaly_detection/detectors"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should write const in uppercase letters.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately in GO, it is recommended to write like this. If const will be exported, then we can capitalize the first character.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, interesting way to use naming convention to control access.

cli/README.md Outdated
1. Install from source:

```
$ go get github.com/VijayanB/esad/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we change the repo ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing it out.

return h.CreateAnomalyDetector(fileName, interactive)
}

//GenerateAnomalyDetector creates detector based on file configurations
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong comments?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACk

1) Move methods close to help readability
2) Add comments to exported methods
3) Fix typo and copied comments
4) Rename file name
if !isProfileExists(name) {
break
}
fmt.Println("profile", name, "already exists.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about print all existing profile names as well? So user don't need to guess which name is not used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can use list command to get profile names. Do you still think that providing all names will be good for UX?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Let's keep it as is.

} else {
response = getUserInputAsText()
}
if len(response) < 1 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we abstract this as validate func and pass it into getUserInput ? So it's flexible to support more parameter's validation logic in future. As currently we have only one simple validation, don't need to make it too complicated currently. Add some todo is fine. It's up to you.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

func deleteProfiles(names []string) {

var existingProfileNames []string
for _, name := range names {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not so efficient to run isProfileExists for every profile name, which will getProfile and do iteration to check if name exists. How about read all profiles, construct map (key: profile name, value: profile), then remove deleted profiles ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK.

commandStop = "stop"
)

// createCmd represents the create command
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong comment?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I removed it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I removed it now.

},
}

var stopCmd = &cobra.Command{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing comment ? How about add comments for every command?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically in go, comment is required for exported variables or functions. Here we are not exporting stopCmd. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments can help others include ourselves to understand code quickly. I think we should add some basic description for these commands like what the command does, what's the input/output, potential exceptions. There are already some description for this command for line 47-48. If you don't like too much comment, how about make line48 more accurate as "Stops detectors based on detector id or detector name pattern, ...."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. I added comments and updated help as well.

return features, nil
}

func getUnit(request string) (*string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only support minutes on AD Kibana. Should we support more unit options in AD CLI? If user use "Hours" to create index with CLI, then AD Kibana may not show it correctly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, create command fails with response "Hours is not supported". In my opinion this is more as restriction in backend than frontend. What do you think? Do we plan to support this in future? if so then having this will have old clients to use this format.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently we have no plan to support other unit options. How about we support only minutes here to keep consistent with AD Kibana (support minutes only) and backend (supports minutes/seconds, code).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Will keep only minutes and will raise error for other cases

featureCount += len(f.AggregationType) * len(f.Field)
}
if featureCount == 0 || featureCount > featureCountLimit {
return fmt.Errorf("trying to create %d feautes, only upto %d features are allowed", featureCount, featureCountLimit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: typo, feautes -> features

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return
}
idStatus, _ := cmd.Flags().GetBool("id")
action := ad.StopAnomalyDetector
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about rename as "StopAnomalyDetectorByNamePattern" considering this method "StopAnomalyDetectorByID"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Rename Method name to include byNamePattern
Support only minutes for interval
Added extra comments
@VijayanB VijayanB merged commit d6dd8ab into opendistro-for-elasticsearch:master Aug 5, 2020
@VijayanB VijayanB deleted the merge-cli branch August 5, 2020 17:33
@VijayanB VijayanB changed the title Merge esad cli into anomaly-detection AD CLI Aug 25, 2020
yizheliu-amazon pushed a commit that referenced this pull request Aug 28, 2020
ESAD Version 0.1 to interact with AD Plugin through cli.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants