diff --git a/README.md b/README.md index 79df910..b3c6e94 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ cf uninstall-plugin AutoScaler | [autoscaling-policy, asp](#cf-autoscaling-policy) | Retrieve the scaling policy of an application | | [attach-autoscaling-policy, aasp](#cf-attach-autoscaling-policy) | Attach a scaling policy to an application | | [detach-autoscaling-policy, dasp](#cf-detach-autoscaling-policy) | Detach the scaling policy from an application | +| [create-autoscaling-credential, casc](#cf-create-autoscaling-credential) | Create custom metric credential for an application | +| [delete-autoscaling-credential, dasc](#cf-delete-autoscaling-credential) | Delete the custom metric credential of an application | | [autoscaling-metrics, asm](#cf-autoscaling-metrics) | Retrieve the metrics of an application | | [autoscaling-history, ash](#cf-autoscaling-history) | Retrieve the scaling history of an application| @@ -139,13 +141,13 @@ Showing policy for app APP_NAME... ``` $ cf asp APP_NAME --output PATH_TO_FILE -Showing policy for app APP_NAME... +Saving policy for app APP_NAME to PATH_TO_FILE... OK ``` ### `cf attach-autoscaling-policy` -Attach a scaling policy to an application, the policy file must be a JSON file, refer to [policy specification](https://github.com/cloudfoundry-incubator/blob/master/docs/policy.md) for the policy format. +Attach a scaling policy to an application, the policy file must be a JSON file, refer to [policy specification](https://github.com/cloudfoundry/app-autoscaler/blob/develop/docs/policy.md) for the policy format. ``` cf attach-autoscaling-policy APP_NAME PATH_TO_POLICY_FILE @@ -167,7 +169,7 @@ OK Detach the scaling policy from an application, the policy will be **deleted** when detached. ``` -cf detach-as-policy APP_NAME +cf detach-autoscaling-policy APP_NAME ``` #### ALIAS: dasp @@ -180,27 +182,87 @@ OK ``` +### `cf create-autoscaling-credential` + +Credential is required when submitting custom metrics to app-autoscaler. If an application is connecting to autoscaler through a service binding approach, the required credential could be found in Cloud Foundry `VCAP_SERVICES` environment variables. Otherwise, you need to generate the required credential explicitly with this command. + +The command will generate autoscaler credential and display it in JSON format. Then you need to set this credential to your application through environment variables or user-provided-service. + +Note: Auto-scaler only grants access with the most recent credential, so the newly generated credential will overwritten the old pairs. Please make sure to update the credential setting in your application once you launch the command `create-autoscaling-credential`. + +Random credential pair will be created by default when username and password are not specified by `--username` and `--password` option. + +``` +cf create-autoscaling-credential APP_NAME [--username USERNAME --password PASSWORD] [--output PATH_TO_FILE] +``` +#### ALIAS: casc + + +#### OPTIONS: +- `--username, -u` : username of the custom metric credential, random username will be set if not specified +- `--password, -p` : password of the custom metric credential, random password will be set if not specified +- `--output` : Dump the credential to a file in JSON format + +#### EXAMPLES: +- Create and view custom credential with user-defined username and password: +``` +$ cf create-autoscaling-credential APP_NAME --username MY_USERNAME --password MY_PASSWORD + +Creating custom metric credential for app APP_NAME... +{ + "app_id": "", + "username": "MY_USERNAME", + "password": "MY_PASSWORD", + "url": "https://autoscalermetrics." +} +``` +- Create random username and password and dump the credential to a file: +``` +$ cf create-autoscaling-credential APP_NAME --output PATH_TO_FILE + +Saving new created credential for app APP_NAME to PATH_TO_FILE... +OK +``` + + +### `cf delete-autoscaling-credential` + +Delete the custom metric credential of an application. + +``` +cf delete-autoscaling-credential APP_NAME +``` +#### ALIAS: dasc + +#### EXAMPLES: +``` +$ cf delete-autoscaling-credential APP_NAME + +Deleting custom metric credential for app APP_NAME... +OK +``` + + ### `cf autoscaling-metrics` -Retrieve the aggregated metrics of an application. You can specify the start/end time or the number of the returned query result, and the display order(ascending or descending). The metrics will be shown in a table. +Retrieve the aggregated metrics of an application. You can specify the start/end time of the returned query result, and the display order(ascending or descending). The metrics will be shown in a table. ``` -cf autoscaling-metrics APP_NAME METRIC_NAME [--number RECORD_NUMBER] [--start START_TIME] [--end END_TIME] [--desc] [--output PATH_TO_FILE] +cf autoscaling-metrics APP_NAME METRIC_NAME [--start START_TIME] [--end END_TIME] [--asc] [--output PATH_TO_FILE] ``` #### ALIAS: asm #### OPTIONS: -- `METRIC_NAME` : available metric supported: memoryused, memoryutil, responsetime, throughput and cpu. +- `METRIC_NAME` : default metrics "memoryused, memoryutil, responsetime, throughput, cpu" or customized name for your own metrics. - `--start` : start time of metrics collected with format `yyyy-MM-ddTHH:mm:ss+/-HH:mm` or `yyyy-MM-ddTHH:mm:ssZ`, default to very beginning if not specified. - `--end` : end time of the metrics collected with format `yyyy-MM-ddTHH:mm:ss+/-HH:mm` or `yyyy-MM-ddTHH:mm:ssZ`, default to current time if not speficied. -- `--number|-n` : the number of the records to return, will be ignored if both start time and end time are specified. -- `--desc` : display in descending order, default to ascending order if not specified +- `--asc` : display in ascending order, default to descending order if not specified - `--output` : dump the metrics to a file #### EXAMPLES: ``` -$ cf autoscaling-metrics APP_NAME memoryused --start 2018-12-27T11:49:00+08:00 --end 2018-12-27T11:52:20+08:00 --desc +$ cf autoscaling-metrics APP_NAME memoryused --start 2018-12-27T11:49:00+08:00 --end 2018-12-27T11:52:20+08:00 --asc Retriving aggregated metrics for app APP_NAME... Metrics Name Value Timestamp @@ -216,9 +278,9 @@ memoryused 62MB 2018-12-27T11:51:40+08:00 ### `cf autoscaling-history` -Retrieve the scaling event history of an application. You can specify the start/end time or the number of the returned query result, and the display order(ascending or descending). The scaling event history will be shown in a table. +Retrieve the scaling event history of an application. You can specify the start/end time of the returned query result, and the display order(ascending or descending). The scaling event history will be shown in a table. ``` -cf autoscaling-history APP_NAME [--number RECORD_NUMBER] [--start START_TIME] [--end END_TIME] [--desc] [--output PATH_TO_FILE] +cf autoscaling-history APP_NAME [--start START_TIME] [--end END_TIME] [--asc] [--output PATH_TO_FILE] ``` #### ALIAS: ash @@ -226,19 +288,18 @@ cf autoscaling-history APP_NAME [--number RECORD_NUMBER] [--start START_TIME] [- #### OPTIONS: - `--start` : start time of the scaling history with format `yyyy-MM-ddTHH:mm:ss+/-HH:mm` or `yyyy-MM-ddTHH:mm:ssZ`, default to very beginning if not specified. - `--end` : end time of the scaling history with format `yyyy-MM-ddTHH:mm:ss+/-HH:mm` or `yyyy-MM-ddTHH:mm:ssZ`, default to current time if not speficied. -- `--number|-n` : the number of the records to return, will be ignored if both start time and end time are specified. -- `--desc` : display in descending order, default to ascending order if not specified +- `--asc` : display in ascending order, default to descending order if not specified - `--output` : dump the scaling history to a file #### EXAMPLES: ``` -$ cf autoscaling-history APP_NAME --start 2018-08-16T17:58:53+08:00 --end 2018-08-16T18:01:00+08:00 --number 3 --desc +$ cf autoscaling-history APP_NAME --start 2018-08-16T17:58:53+08:00 --end 2018-08-16T18:01:00+08:00 --asc Showing history for app APP_NAME... Scaling Type Status Instance Changes Time Action Error -scheduled succeeded 3->6 2018-08-16T18:00:00+08:00 3 instance(s) because limited by min instances 6 -dynamic succeeded 2->3 2018-08-16T17:59:33+08:00 +1 instance(s) because memoryused >= 15MB for 120 seconds dynamic failed 2->-1 2018-08-16T17:58:53+08:00 -1 instance(s) because throughput < 10rps for 120 seconds app does not have policy set +dynamic succeeded 2->3 2018-08-16T17:59:33+08:00 +1 instance(s) because memoryused >= 15MB for 120 seconds +scheduled succeeded 3->6 2018-08-16T18:00:00+08:00 3 instance(s) because limited by min instances 6 ``` - `Scaling Type`: the trigger type of the scaling action, possible scaling types: `dynamic` and `scheduled` - `dynamic`: the scaling action is triggered by a dynamic rule (memoryused, memoryutil, responsetime or throughput) diff --git a/src/cli/api/apihelper.go b/src/cli/api/apihelper.go index e8833f2..df24d53 100644 --- a/src/cli/api/apihelper.go +++ b/src/cli/api/apihelper.go @@ -28,6 +28,7 @@ import ( const ( HealthPath = "/health" PolicyPath = "/v1/apps/{appId}/policy" + CredentialPath = "/v1/apps/{appId}/credential" AggregatedMetricPath = "/v1/apps/{appId}/aggregated_metric_histories/{metric_type}" HistoryPath = "/v1/apps/{appId}/scaling_histories" ) @@ -95,16 +96,24 @@ func (helper *APIHelper) DoRequest(req *http.Request) (*http.Response, error) { } -func parseErrResponse(raw []byte) string { - - var f interface{} - err := json.Unmarshal(raw, &f) - if err != nil { - return string(raw) +func parseErrArrayResponse(a []interface{}) string { + retMsg := "" + for _, entry := range a { + mentry := entry.(map[string]interface{}) + var context, description string + for ik, iv := range mentry { + if ik == "context" { + context = iv.(string) + } else if ik == "description" { + description,_ = strconv.Unquote(strings.Replace(strconv.Quote(iv.(string)), `\\u`, `\u`, -1)) + } + } + retMsg = retMsg + "\n" + fmt.Sprintf("%v: %v", context, description) } + return retMsg +} - m := f.(map[string]interface{}) - +func parseErrObjectResponse(m map[string]interface{}) string { retMsg := "" for k, v := range m { if k == "error" { @@ -131,12 +140,31 @@ func parseErrResponse(raw []byte) string { retMsg = fmt.Sprintf("%v", v) } + } else if k == "message" { + retMsg = fmt.Sprintf("%v", v) } } - return retMsg } +func parseErrResponse(raw []byte) string { + + var f interface{} + err := json.Unmarshal(raw, &f) + if err != nil { + return string(raw) + } + + switch f.(type) { + case map[string]interface{}: + return parseErrObjectResponse(f.(map[string]interface{})) + case []interface{}: + return parseErrArrayResponse(f.([]interface{})) + default: + return "" + } +} + func (helper *APIHelper) CheckHealth() error { baseURL := helper.Endpoint.URL requestURL := fmt.Sprintf("%s%s", baseURL, HealthPath) @@ -294,7 +322,7 @@ func (helper *APIHelper) DeletePolicy() error { } -func (helper *APIHelper) GetAggregatedMetrics(metricName string, startTime, endTime int64, desc bool, page uint64) (bool, [][]string, error) { +func (helper *APIHelper) GetAggregatedMetrics(metricName string, startTime, endTime int64, asc bool, page uint64) (bool, [][]string, error) { if page <= 1 { err := helper.CheckHealth() @@ -317,10 +345,10 @@ func (helper *APIHelper) GetAggregatedMetrics(metricName string, startTime, endT if endTime > 0 { q.Add("end-time", strconv.FormatInt(endTime, 10)) } - if desc { - q.Add("order", "desc") - } else { + if asc { q.Add("order", "asc") + } else { + q.Add("order", "desc") } q.Add("page", strconv.FormatUint(page, 10)) req.URL.RawQuery = q.Encode() @@ -362,7 +390,7 @@ func (helper *APIHelper) GetAggregatedMetrics(metricName string, startTime, endT } -func (helper *APIHelper) GetHistory(startTime, endTime int64, desc bool, page uint64) (bool, [][]string, error) { +func (helper *APIHelper) GetHistory(startTime, endTime int64, asc bool, page uint64) (bool, [][]string, error) { if page <= 1 { err := helper.CheckHealth() @@ -383,10 +411,10 @@ func (helper *APIHelper) GetHistory(startTime, endTime int64, desc bool, page ui if endTime > 0 { q.Add("end-time", strconv.FormatInt(endTime, 10)) } - if desc { - q.Add("order", "desc") - } else { + if asc { q.Add("order", "asc") + } else { + q.Add("order", "desc") } q.Add("page", strconv.FormatUint(page, 10)) req.URL.RawQuery = q.Encode() @@ -449,3 +477,97 @@ func (helper *APIHelper) GetHistory(startTime, endTime int64, desc bool, page ui } } + +func (helper *APIHelper) DeleteCredential() error { + + err := helper.CheckHealth() + if err != nil { + return err + } + + baseURL := helper.Endpoint.URL + requestURL := fmt.Sprintf("%s%s", baseURL, strings.Replace(CredentialPath, "{appId}", helper.Client.AppId, -1)) + + req, err := http.NewRequest("DELETE", requestURL, nil) + req.Header.Add("Authorization", helper.Client.AuthToken) + + resp, err := helper.DoRequest(req) + if err != nil { + return err + } + defer resp.Body.Close() + + raw, err := ioutil.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + var errorMsg string + switch resp.StatusCode { + case 401: + errorMsg = fmt.Sprintf(ui.Unauthorized, baseURL) + default: + errorMsg = parseErrResponse(raw) + } + return errors.New(errorMsg) + } + + return nil + +} + +func (helper *APIHelper) CreateCredential(data interface{}) ([]byte, error) { + + err := helper.CheckHealth() + if err != nil { + return nil, err + } + + baseURL := helper.Endpoint.URL + requestURL := fmt.Sprintf("%s%s", baseURL, strings.Replace(CredentialPath, "{appId}", helper.Client.AppId, -1)) + + var body io.Reader + if data != nil { + jsonByte, e := json.Marshal(data) + if e != nil { + return nil, fmt.Errorf(ui.InvalidCredential, e) + } + body = bytes.NewBuffer(jsonByte) + } + + req, err := http.NewRequest("PUT", requestURL, body) + req.Header.Add("Authorization", helper.Client.AuthToken) + req.Header.Add("Content-Type", "application/json") + + resp, err := helper.DoRequest(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + raw, err := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + + var errorMsg string + switch resp.StatusCode { + case 401: + errorMsg = fmt.Sprintf(ui.Unauthorized, baseURL) + case 400: + errorMsg = fmt.Sprintf(ui.InvalidCredential, parseErrResponse(raw)) + default: + errorMsg = parseErrResponse(raw) + } + return nil, errors.New(errorMsg) + } + + var credential models.CredentialResponse + err = json.Unmarshal(raw, &credential) + if err != nil { + return nil, err + } + + prettyCredential, err := cjson.MarshalWithoutHTMLEscape(credential) + if err != nil { + return nil, err + } + + return prettyCredential, nil +} \ No newline at end of file diff --git a/src/cli/api/apihelper_test.go b/src/cli/api/apihelper_test.go index 1d2f2f7..23c4149 100644 --- a/src/cli/api/apihelper_test.go +++ b/src/cli/api/apihelper_test.go @@ -44,6 +44,10 @@ var _ = Describe("API Helper Test", func() { }, }, } + fakeCredential Credential = Credential{ + Username: "fake-user", + Password: "fake-password", + } ) BeforeEach(func() { @@ -316,16 +320,32 @@ var _ = Describe("API Helper Test", func() { }) Context("Invalid Policy Format", func() { - BeforeEach(func() { - apiServer.RouteToHandler("PUT", urlpath, - ghttp.RespondWith(http.StatusBadRequest, `{"success":false,"error":[{"property":"instance_min_count","message":"instance_min_count and instance_max_count values are not compatible","instance":{"instance_max_count":2,"instance_min_count":10,"scaling_rules":[{"adjustment":"+1","breach_duration_secs":600,"cool_down_secs":300,"metric_type":"memoryused","operator":">","stat_window_secs":300,"threshold":100},{"adjustment":"-1","breach_duration_secs":600,"cool_down_secs":300,"metric_type":"memoryused","operator":"<=","stat_window_secs":300,"threshold":5}]},"stack":"instance_min_count 10 is higher or equal to instance_max_count 2 in policy_json"}],"result":null}`), - ) + Context("Received error object", func() { + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, + ghttp.RespondWith(http.StatusBadRequest, `{"success":false,"error":[{"property":"instance_min_count","message":"instance_min_count and instance_max_count values are not compatible","instance":{"instance_max_count":2,"instance_min_count":10,"scaling_rules":[{"adjustment":"+1","breach_duration_secs":600,"cool_down_secs":300,"metric_type":"memoryused","operator":">","stat_window_secs":300,"threshold":100},{"adjustment":"-1","breach_duration_secs":600,"cool_down_secs":300,"metric_type":"memoryused","operator":"<=","stat_window_secs":300,"threshold":5}]},"stack":"instance_min_count 10 is higher or equal to instance_max_count 2 in policy_json"}],"result":null}`), + ) + }) + + It("Fail with 400 error", func() { + err = apihelper.CreatePolicy(fakePolicy) + Expect(err).Should(HaveOccurred()) + Expect(err).Should(MatchError(fmt.Sprintf(ui.InvalidPolicy, "\n"+"instance_min_count 10 is higher or equal to instance_max_count 2 in policy_json"))) + }) }) - It("Fail with 400 error", func() { - err = apihelper.CreatePolicy(fakePolicy) - Expect(err).Should(HaveOccurred()) - Expect(err).Should(MatchError(fmt.Sprintf(ui.InvalidPolicy, "\n"+"instance_min_count 10 is higher or equal to instance_max_count 2 in policy_json"))) + Context("Received error array", func() { + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, + ghttp.RespondWith(http.StatusBadRequest, `[{"context":"(root).scaling_rules.0.operator","description":"scaling_rules.0.operator must be one of the following: \"\\u003c\", \"\\u003e\", \"\\u003c=\", \"\\u003e=\""},{"context":"(root).schedules.recurring_schedule.0.start_time","description":"Does not match pattern '^(2[0-3]|1[0-9]|0[0-9]):([0-5][0-9])$'"}]`), + ) + }) + + It("Fail with 400 error", func() { + err = apihelper.CreatePolicy(fakePolicy) + Expect(err).Should(HaveOccurred()) + Expect(err).Should(MatchError(fmt.Sprintf(ui.InvalidPolicy, "\n(root).scaling_rules.0.operator: scaling_rules.0.operator must be one of the following: \"<\", \">\", \"<=\", \">=\"\n(root).schedules.recurring_schedule.0.start_time: Does not match pattern '^(2[0-3]|1[0-9]|0[0-9]):([0-5][0-9])$'"))) + }) }) }) @@ -436,6 +456,204 @@ var _ = Describe("API Helper Test", func() { }) + Context("Create Credential", func() { + var urlpath string = "/v1/apps/" + fakeAppId + "/credential" + + Context("201 Created with valid auth token", func() { + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, + ghttp.CombineHandlers( + ghttp.RespondWithJSONEncoded(http.StatusCreated, &fakeCredential), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ), + ) + }) + + It("succeed", func() { + response, err := apihelper.CreateCredential(fakeCredential) + Expect(err).NotTo(HaveOccurred()) + + var actualCredential Credential + _ = json.Unmarshal([]byte(response), &actualCredential) + Expect(actualCredential).To(MatchFields(IgnoreExtras, Fields{ + "Username": Equal(fakeCredential.Username), + "Password": Equal(fakeCredential.Password), + })) + }) + }) + + Context("200 OK with valid auth token", func() { + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, + ghttp.CombineHandlers( + ghttp.RespondWithJSONEncoded(http.StatusCreated, &fakeCredential), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ), + ) + }) + + It("succeed", func() { + response, err := apihelper.CreateCredential(fakeCredential) + Expect(err).NotTo(HaveOccurred()) + + var actualCredential Credential + _ = json.Unmarshal([]byte(response), &actualCredential) + Expect(actualCredential).To(MatchFields(IgnoreExtras, Fields{ + "Username": Equal(fakeCredential.Username), + "Password": Equal(fakeCredential.Password), + })) + }) + }) + + Context("Forbidden Request", func() { + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, + ghttp.RespondWith(http.StatusForbidden, `{"code":"Forbidden","message":"This command is only valid for build-in auto-scaling capacity. Please operate service credential with \"cf bind/unbind-service\" command."}`), + ) + }) + + It("Fail with 403 error", func() { + _, err = apihelper.CreateCredential(fakeCredential) + Expect(err).Should(HaveOccurred()) + Expect(err).Should(MatchError(fmt.Sprintf(`This command is only valid for build-in auto-scaling capacity. Please operate service credential with "cf bind/unbind-service" command.`))) + }) + }) + + Context("Unauthorized Access", func() { + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, + ghttp.RespondWith(http.StatusUnauthorized, ""), + ) + }) + + It("Fail with 401 error", func() { + _, err = apihelper.CreateCredential(fakeCredential) + Expect(err).Should(HaveOccurred()) + Expect(err).Should(MatchError(fmt.Sprintf(ui.Unauthorized, apihelper.Endpoint.URL))) + }) + }) + + Context("Invalid Credential Format", func() { + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, + ghttp.RespondWith(http.StatusBadRequest, `{"code":"Bad Request","message":"Username and password are both required"}`), + ) + }) + + It("Fail with 400 error", func() { + _, err = apihelper.CreateCredential(fakeCredential) + Expect(err).Should(HaveOccurred()) + Expect(err).Should(MatchError(fmt.Sprintf(ui.InvalidCredential, "Username and password are both required"))) + }) + }) + + Context("Default error handling", func() { + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, + ghttp.RespondWith(http.StatusInternalServerError, `{"success":false,"error":{"message":"Internal error","statusCode":500},"result":null}`), + ) + }) + + It("Fail with 500 error", func() { + _, err = apihelper.CreateCredential(fakeCredential) + Expect(err).Should(HaveOccurred()) + Expect(err).Should(MatchError("Internal error")) + }) + }) + + Context("When error msg is a plain text", func() { + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, + ghttp.RespondWith(http.StatusBadGateway, "502 bad gateway"), + ) + }) + + It("Fail with 502 error", func() { + _, err = apihelper.CreateCredential(fakeCredential) + Expect(err).Should(HaveOccurred()) + Expect(err).Should(MatchError("502 bad gateway")) + }) + }) + + }) + + Context("Delete Credential", func() { + var urlpath string = "/v1/apps/" + fakeAppId + "/credential" + + Context("Succeed with valid auth token", func() { + BeforeEach(func() { + apiServer.RouteToHandler("DELETE", urlpath, + ghttp.CombineHandlers( + ghttp.RespondWith(http.StatusOK, ""), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ), + ) + }) + + It("succeed", func() { + err = apihelper.DeleteCredential() + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("Forbidden Request", func() { + BeforeEach(func() { + apiServer.RouteToHandler("DELETE", urlpath, + ghttp.RespondWith(http.StatusForbidden, `{"code":"Forbidden","message":"This command is only valid for build-in auto-scaling capacity. Please operate service credential with \"cf bind/unbind-service\" command."}`), + ) + }) + + It("Fail with 403 error", func() { + err = apihelper.DeleteCredential() + Expect(err).Should(HaveOccurred()) + Expect(err).Should(MatchError(fmt.Sprintf(`This command is only valid for build-in auto-scaling capacity. Please operate service credential with "cf bind/unbind-service" command.`))) + }) + }) + + Context("Unauthorized Access", func() { + BeforeEach(func() { + apiServer.RouteToHandler("DELETE", urlpath, + ghttp.RespondWith(http.StatusUnauthorized, ""), + ) + }) + + It("Fail with 401 error", func() { + err = apihelper.DeleteCredential() + Expect(err).Should(HaveOccurred()) + Expect(err).Should(MatchError(fmt.Sprintf(ui.Unauthorized, apihelper.Endpoint.URL))) + }) + }) + + Context("Default error handling", func() { + BeforeEach(func() { + apiServer.RouteToHandler("DELETE", urlpath, + ghttp.RespondWith(http.StatusInternalServerError, `{"success":false,"error":{"message":"Internal error","statusCode":500},"result":null}`), + ) + }) + + It("Fail with 500 error", func() { + err = apihelper.DeleteCredential() + Expect(err).Should(HaveOccurred()) + Expect(err).Should(MatchError("Internal error")) + }) + }) + + Context("When error msg is a plain text", func() { + BeforeEach(func() { + apiServer.RouteToHandler("DELETE", urlpath, + ghttp.RespondWith(http.StatusBadGateway, "502 bad gateway"), + ) + }) + + It("Fail with 502 error", func() { + err = apihelper.DeleteCredential() + Expect(err).Should(HaveOccurred()) + Expect(err).Should(MatchError("502 bad gateway")) + }) + }) + + }) + Context("Get Aggregated Metrics", func() { var urlpath = "/v1/apps/" + fakeAppId + "/aggregated_metric_histories/memoryused" var now int64 @@ -461,7 +679,7 @@ var _ = Describe("API Helper Test", func() { Context("With valid auth token", func() { - Context("Query multiple pages with order asc", func() { + Context("Query multiple pages with order desc", func() { BeforeEach(func() { apiServer.AppendHandlers( ghttp.CombineHandlers( @@ -472,7 +690,7 @@ var _ = Describe("API Helper Test", func() { Metrics: metrics[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=1"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=1"), ), ) @@ -485,7 +703,7 @@ var _ = Describe("API Helper Test", func() { Metrics: metrics[10:20], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=2"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=2"), ), ) @@ -498,7 +716,7 @@ var _ = Describe("API Helper Test", func() { Metrics: metrics[20:30], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=3"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=3"), ), ) }) @@ -541,7 +759,7 @@ var _ = Describe("API Helper Test", func() { }) }) - Context("Query multiple pages with order desc", func() { + Context("Query multiple pages with order asc", func() { BeforeEach(func() { apiServer.AppendHandlers( ghttp.CombineHandlers( @@ -552,7 +770,7 @@ var _ = Describe("API Helper Test", func() { Metrics: reversedMetrics[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=desc&page=1"), + ghttp.VerifyRequest("GET", urlpath, "order=asc&page=1"), ), ) @@ -565,7 +783,7 @@ var _ = Describe("API Helper Test", func() { Metrics: reversedMetrics[10:20], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=desc&page=2"), + ghttp.VerifyRequest("GET", urlpath, "order=asc&page=2"), ), ) @@ -578,7 +796,7 @@ var _ = Describe("API Helper Test", func() { Metrics: reversedMetrics[20:30], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=desc&page=3"), + ghttp.VerifyRequest("GET", urlpath, "order=asc&page=3"), ), ) }) @@ -621,7 +839,7 @@ var _ = Describe("API Helper Test", func() { }) }) - Context("Query with asc & start time & end time ", func() { + Context("Query with desc & start time & end time ", func() { BeforeEach(func() { apiServer.AppendHandlers( ghttp.CombineHandlers( @@ -632,7 +850,7 @@ var _ = Describe("API Helper Test", func() { Metrics: metrics[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, fmt.Sprintf("order=asc&page=1&start-time=%v&end-time=%v", now, now+int64(9*30*1E9))), + ghttp.VerifyRequest("GET", urlpath, fmt.Sprintf("order=desc&page=1&start-time=%v&end-time=%v", now, now+int64(9*30*1E9))), ), ) }) @@ -664,7 +882,7 @@ var _ = Describe("API Helper Test", func() { Metrics: []*AppAggregatedMetric{}, }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, fmt.Sprintf("order=asc&page=1&start-time=%v&end-time=%v", now, now+int64(9*30*1E9))), + ghttp.VerifyRequest("GET", urlpath, fmt.Sprintf("order=desc&page=1&start-time=%v&end-time=%v", now, now+int64(9*30*1E9))), ), ) }) @@ -684,7 +902,7 @@ var _ = Describe("API Helper Test", func() { apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWith(http.StatusUnauthorized, ""), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=1"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=1"), ), ) }) @@ -701,7 +919,7 @@ var _ = Describe("API Helper Test", func() { apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWith(http.StatusInternalServerError, `{"success":false,"error":{"message":"Internal error","statusCode":500},"result":null}`), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=1"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=1"), ), ) }) @@ -718,7 +936,7 @@ var _ = Describe("API Helper Test", func() { apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWith(http.StatusNotFound, "502 bad gateway"), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=1"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=1"), ), ) }) @@ -835,7 +1053,7 @@ var _ = Describe("API Helper Test", func() { Histories: histories_ut[0:3], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=1"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=1"), ), ) @@ -871,7 +1089,7 @@ var _ = Describe("API Helper Test", func() { }) }) - Context("Query multiple pages with order asc", func() { + Context("Query multiple pages with order desc", func() { BeforeEach(func() { apiServer.AppendHandlers( ghttp.CombineHandlers( @@ -882,7 +1100,7 @@ var _ = Describe("API Helper Test", func() { Histories: histories[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=1"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=1"), ), ) @@ -895,7 +1113,7 @@ var _ = Describe("API Helper Test", func() { Histories: histories[10:20], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=2"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=2"), ), ) @@ -908,7 +1126,7 @@ var _ = Describe("API Helper Test", func() { Histories: histories[20:30], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=3"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=3"), ), ) @@ -961,7 +1179,7 @@ var _ = Describe("API Helper Test", func() { }) }) - Context("Query multiple pages with order desc", func() { + Context("Query multiple pages with order asc", func() { BeforeEach(func() { apiServer.AppendHandlers( ghttp.CombineHandlers( @@ -972,7 +1190,7 @@ var _ = Describe("API Helper Test", func() { Histories: reversedHistories[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=desc&page=1"), + ghttp.VerifyRequest("GET", urlpath, "order=asc&page=1"), ), ) @@ -985,7 +1203,7 @@ var _ = Describe("API Helper Test", func() { Histories: reversedHistories[10:20], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=desc&page=2"), + ghttp.VerifyRequest("GET", urlpath, "order=asc&page=2"), ), ) @@ -998,7 +1216,7 @@ var _ = Describe("API Helper Test", func() { Histories: reversedHistories[20:30], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, "order=desc&page=3"), + ghttp.VerifyRequest("GET", urlpath, "order=asc&page=3"), ), ) }) @@ -1050,7 +1268,7 @@ var _ = Describe("API Helper Test", func() { }) }) - Context("Query with asc & start time & end time ", func() { + Context("Query with desc & start time & end time ", func() { BeforeEach(func() { apiServer.AppendHandlers( ghttp.CombineHandlers( @@ -1061,7 +1279,7 @@ var _ = Describe("API Helper Test", func() { Histories: histories[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, fmt.Sprintf("order=asc&page=1&start-time=%v&end-time=%v", now, now+int64(9*120*1E9))), + ghttp.VerifyRequest("GET", urlpath, fmt.Sprintf("order=desc&page=1&start-time=%v&end-time=%v", now, now+int64(9*120*1E9))), ), ) }) @@ -1096,7 +1314,7 @@ var _ = Describe("API Helper Test", func() { Histories: []*AppScalingHistory{}, }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, fmt.Sprintf("order=asc&page=1&start-time=%v&end-time=%v", now, now+int64(9*120*1E9))), + ghttp.VerifyRequest("GET", urlpath, fmt.Sprintf("order=desc&page=1&start-time=%v&end-time=%v", now, now+int64(9*120*1E9))), ), ) }) @@ -1116,7 +1334,7 @@ var _ = Describe("API Helper Test", func() { apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWith(http.StatusUnauthorized, ""), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=1"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=1"), ), ) }) @@ -1133,7 +1351,7 @@ var _ = Describe("API Helper Test", func() { apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWith(http.StatusInternalServerError, `{"success":false,"error":{"message":"Internal error","statusCode":500},"result":null}`), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=1"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=1"), ), ) }) @@ -1150,7 +1368,7 @@ var _ = Describe("API Helper Test", func() { apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWith(http.StatusNotFound, "502 bad gateway"), - ghttp.VerifyRequest("GET", urlpath, "order=asc&page=1"), + ghttp.VerifyRequest("GET", urlpath, "order=desc&page=1"), ), ) }) diff --git a/src/cli/commands/autoscaler.go b/src/cli/commands/autoscaler.go index 23a5ff3..1e3e471 100644 --- a/src/cli/commands/autoscaler.go +++ b/src/cli/commands/autoscaler.go @@ -7,12 +7,14 @@ import ( type AutoScalerCmds struct { CLIConnection api.Connection - API ApiCommand `command:"autoscaling-api" description:"Set or view AutoScaler service API endpoint"` - Policy PolicyCommand `command:"autoscaling-policy" description:"Retrieve the scaling policy of an application"` - AttachPolicy AttachPolicyCommand `command:"attach-autoscaling-policy" description:"Attach a scaling policy to an application"` - DetachPolicy DetachPolicyCommand `command:"detach-autoscaling-policy" description:"Detach a scaling policy from an application"` - Metrics MetricsCommand `command:"autoscaling-metrics" description:"Retrieve the metrics of an application"` - History HistoryCommand `command:"autoscaling-history" description:"Retrieve the history of an application"` + API ApiCommand `command:"autoscaling-api" description:"Set or view AutoScaler service API endpoint"` + Policy PolicyCommand `command:"autoscaling-policy" description:"Retrieve the scaling policy of an application"` + AttachPolicy AttachPolicyCommand `command:"attach-autoscaling-policy" description:"Attach a scaling policy to an application"` + DetachPolicy DetachPolicyCommand `command:"detach-autoscaling-policy" description:"Detach a scaling policy from an application"` + CreateCredential CreateCredentialCommand `command:"create-autoscaling-credential" description:"Create custom metric credential for an application"` + DeleteCredential DeleteCredentialCommand `command:"delete-autoscaling-credential" description:"Delete the custom metric credential of an application"` + Metrics MetricsCommand `command:"autoscaling-metrics" description:"Retrieve the metrics of an application"` + History HistoryCommand `command:"autoscaling-history" description:"Retrieve the history of an application"` UninstallPlugin UninstallHook `command:"CLI-MESSAGE-UNINSTALL"` } diff --git a/src/cli/commands/create_credential.go b/src/cli/commands/create_credential.go new file mode 100644 index 0000000..eeb3197 --- /dev/null +++ b/src/cli/commands/create_credential.go @@ -0,0 +1,102 @@ +package commands + +import ( + "cli/api" + "cli/ui" + "errors" + "fmt" + "io" + "os" + + "cli/models" +) + +type CreateCredentialCommand struct { + RequiredArgs CreateCredentialPositionalArgs `positional-args:"yes"` + Username string `short:"u" long:"username" description:"username of the custom metric credential, random username will be set if not specified"` + Password string `short:"p" long:"password" description:"password of the custom metric credential, random password will be set if not specified"` + Output string `long:"output" description:"dump the credential to a file in JSON format"` +} + +type CreateCredentialPositionalArgs struct { + AppName string `positional-arg-name:"APP_NAME" required:"true" ` +} + +func (command CreateCredentialCommand) Execute([]string) error { + + var ( + err error + writer *os.File + ) + + if command.Username == "" && command.Password != "" { + return fmt.Errorf(ui.InvalidCredentialUsername) + } else if command.Username != "" && command.Password == "" { + return fmt.Errorf(ui.InvalidCredentialPassword) + } + + if command.Output != "" { + writer, err = os.OpenFile(command.Output, os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + return err + } + defer writer.Close() + } else { + writer = os.Stdout + } + + return CreateCredential(AutoScaler.CLIConnection, command.RequiredArgs.AppName, command.Username, command.Password, writer, command.Output) +} + +func CreateCredential(cliConnection api.Connection, appName string, username string, password string, writer io.Writer, outputfile string) error { + + cfclient, err := api.NewCFClient(cliConnection) + if err != nil { + return err + } + endpoint, err := api.GetEndpoint(cfclient) + if err != nil { + return err + } + if endpoint.URL == "" { + return errors.New(ui.NoEndpoint) + } + + err = cfclient.Configure(appName) + if err != nil { + return err + } + + apihelper := api.NewAPIHelper(endpoint, cfclient, os.Getenv("CF_TRACE")) + + if outputfile != "" { + ui.SayMessage(ui.SaveCredentialHint, appName, outputfile) + } else { + ui.SayMessage(ui.CreateCredentialHint, appName) + } + + var credentialResult []byte + if username != "" && password != "" { + credentialSource := models.Credential { + Username: username, + Password: password, + } + credentialResult, err = apihelper.CreateCredential(credentialSource) + if err != nil { + return err + } + } else { + credentialResult, err = apihelper.CreateCredential(nil) + if err != nil { + return err + } + } + + fmt.Fprintf(writer, "%v", string(credentialResult)) + + if outputfile != "" { + ui.SayOK() + } + ui.SayWarningMessage(ui.CreateCredentialWarning, appName) + return nil +} diff --git a/src/cli/commands/delete_credential.go b/src/cli/commands/delete_credential.go new file mode 100644 index 0000000..7f84ecb --- /dev/null +++ b/src/cli/commands/delete_credential.go @@ -0,0 +1,53 @@ +package commands + +import ( + "cli/api" + "cli/ui" + "errors" + "os" +) + +type DeleteCredentialCommand struct { + RequiredlArgs DeleteCredentialPositionalArgs `positional-args:"yes"` +} + +type DeleteCredentialPositionalArgs struct { + AppName string `positional-arg-name:"APP_NAME" required:"true" ` +} + +func (command DeleteCredentialCommand) Execute([]string) error { + return DeleteCredential(AutoScaler.CLIConnection, command.RequiredlArgs.AppName) +} + +func DeleteCredential(cliConnection api.Connection, appName string) error { + + cfclient, err := api.NewCFClient(cliConnection) + if err != nil { + return err + } + + endpoint, err := api.GetEndpoint(cfclient) + if err != nil { + return err + } + if endpoint.URL == "" { + return errors.New(ui.NoEndpoint) + } + + err = cfclient.Configure(appName) + if err != nil { + return err + } + + apihelper := api.NewAPIHelper(endpoint, cfclient, os.Getenv("CF_TRACE")) + + ui.SayMessage(ui.DeleteCredentialHint, appName) + err = apihelper.DeleteCredential() + if err != nil { + return err + } + + ui.SayOK() + ui.SayWarningMessage(ui.DeleteCredentialWarning, appName) + return nil +} diff --git a/src/cli/commands/retrieve_history.go b/src/cli/commands/retrieve_history.go index 6bb1ec2..ad54af6 100644 --- a/src/cli/commands/retrieve_history.go +++ b/src/cli/commands/retrieve_history.go @@ -7,9 +7,7 @@ import ( "errors" "fmt" "io" - "math" "os" - "strconv" "time" ) @@ -17,8 +15,8 @@ type HistoryCommand struct { RequiredlArgs HistoryPositionalArgs `positional-args:"yes"` StartTime string `long:"start" description:"start time of metrics collected with format \"yyyy-MM-ddTHH:mm:ss+/-HH:mm\" or \"yyyy-MM-ddTHH:mm:ssZ\", default to very beginning if not specified."` EndTime string `long:"end" description:"end time of the metrics collected with format \"yyyy-MM-ddTHH:mm:ss+/-HH:mm\" or \"yyyy-MM-ddTHH:mm:ssZ\", default to current time if not speficied."` - RecordNumber string `long:"number" short:"n" description:"the number of the records to return, will be ignored if both start time and end time are specified."` Desc bool `long:"desc" description:"display in descending order, default to ascending order if not specified."` + Asc bool `long:"asc" description:"display in ascending order, default to descending order if not specified."` Output string `long:"output" description:"dump the policy to a file in JSON format"` } @@ -31,10 +29,13 @@ func (command HistoryCommand) Execute([]string) error { var ( st int64 = 0 et int64 = time.Now().UnixNano() - rn int64 = 0 + fpo bool = false err error writer *os.File ) + if command.Desc && command.Asc { + return fmt.Errorf(ui.DeprecatedDescWarning) + } if command.StartTime != "" { st, err = ctime.ParseTimeFormat(command.StartTime) if err != nil { @@ -50,15 +51,7 @@ func (command HistoryCommand) Execute([]string) error { if st > et { return errors.New(fmt.Sprintf(ui.InvalidTimeRange, command.StartTime, command.EndTime)) } - if command.RecordNumber != "" { - rn, err = strconv.ParseInt(command.RecordNumber, 10, 64) - if rn <= 0 || err != nil { - return errors.New(fmt.Sprintf(ui.InvalidRecordNumber, command.RecordNumber)) - } - } - if command.StartTime != "" && command.EndTime != "" { - rn = math.MaxInt64 - } + fpo = command.StartTime == "" && command.EndTime == "" if command.Output != "" { writer, err = os.OpenFile(command.Output, os.O_CREATE|os.O_WRONLY, 0666) @@ -72,10 +65,10 @@ func (command HistoryCommand) Execute([]string) error { return RetrieveHistory(AutoScaler.CLIConnection, command.RequiredlArgs.AppName, - st, et, rn, command.Desc, writer, command.Output) + st, et, fpo, command.Desc, command.Asc, writer, command.Output) } -func RetrieveHistory(cliConnection api.Connection, appName string, startTime, endTime, recordNumber int64, desc bool, writer io.Writer, outputfile string) error { +func RetrieveHistory(cliConnection api.Connection, appName string, startTime, endTime int64, firstPageOnly bool, desc bool, asc bool, writer io.Writer, outputfile string) error { cfclient, err := api.NewCFClient(cliConnection) if err != nil { @@ -104,31 +97,29 @@ func RetrieveHistory(cliConnection api.Connection, appName string, startTime, en table := ui.NewTable(writer, []string{"Scaling Type", "Status", "Instance Changes", "Time", "Action", "Error"}) var ( - page uint64 = 1 - currentNumber int64 = 0 - next bool = true - noResult bool = true - data [][]string + page uint64 = 1 + next bool = true + noResult bool = true + moreResult bool = false + data [][]string ) for true { - next, data, err = apihelper.GetHistory(startTime, endTime, desc, page) + next, data, err = apihelper.GetHistory(startTime, endTime, asc, page) if err != nil { return err } for _, row := range data { - if recordNumber == 0 || currentNumber < recordNumber { - table.Add(row) - currentNumber++ - } + table.Add(row) } if len(data) > 0 { noResult = false table.Print() } - if !next || currentNumber >= recordNumber { + moreResult = next && firstPageOnly + if !next || firstPageOnly { break } page += 1 @@ -142,6 +133,12 @@ func RetrieveHistory(cliConnection api.Connection, appName string, startTime, en ui.SayOK() } } + if moreResult { + ui.SayWarningMessage(ui.MoreRecordsWarning) + } + if desc { + ui.SayWarningMessage(ui.DeprecatedDescWarning) + } return nil } diff --git a/src/cli/commands/retrieve_metrics.go b/src/cli/commands/retrieve_metrics.go index c30b29a..ee7e786 100644 --- a/src/cli/commands/retrieve_metrics.go +++ b/src/cli/commands/retrieve_metrics.go @@ -7,9 +7,8 @@ import ( "errors" "fmt" "io" - "math" "os" - "strconv" + "regexp" "time" ) @@ -17,35 +16,31 @@ type MetricsCommand struct { RequiredlArgs MetricsPositionalArgs `positional-args:"yes"` StartTime string `long:"start" description:"start time of metrics collected with format \"yyyy-MM-ddTHH:mm:ss+/-HH:mm\" or \"yyyy-MM-ddTHH:mm:ssZ\", default to very beginning if not specified."` EndTime string `long:"end" description:"end time of the metrics collected with format \"yyyy-MM-ddTHH:mm:ss+/-HH:mm\" or \"yyyy-MM-ddTHH:mm:ssZ\", default to current time if not speficied."` - RecordNumber string `long:"number" short:"n" description:"the number of the records to return, will be ignored if both start time and end time are specified."` Desc bool `long:"desc" description:"display in descending order, default to ascending order if not specified."` + Asc bool `long:"asc" description:"display in ascending order, default to descending order if not specified."` Output string `long:"output" description:"dump the policy to a file in JSON format"` } type MetricsPositionalArgs struct { AppName string `positional-arg-name:"APP_NAME" required:"true"` - MetricName string `positional-arg-name:"METRIC_NAME" required:"true" description:"available metric supported: \n memoryused, memoryutil, responsetime, throughput, cpu"` + MetricName string `positional-arg-name:"METRIC_NAME" required:"true" description:"available metric for the application:\n memoryused, memoryutil, responsetime, throughput, cpu or custom metrics"` } func (command MetricsCommand) Execute([]string) error { - - switch command.RequiredlArgs.MetricName { - case "memoryused": - case "memoryutil": - case "responsetime": - case "throughput": - case "cpu": - default: + if ok, _ := regexp.MatchString("^[a-zA-Z0-9_]+$", command.RequiredlArgs.MetricName); !ok { return errors.New(fmt.Sprintf(ui.UnrecognizedMetricName, command.RequiredlArgs.MetricName)) } var ( st int64 = 0 et int64 = time.Now().UnixNano() - rn int64 = 0 + fpo bool = false err error writer *os.File ) + if command.Desc && command.Asc { + return fmt.Errorf(ui.DeprecatedDescWarning) + } if command.StartTime != "" { st, err = ctime.ParseTimeFormat(command.StartTime) if err != nil { @@ -61,15 +56,7 @@ func (command MetricsCommand) Execute([]string) error { if st > et { return errors.New(fmt.Sprintf(ui.InvalidTimeRange, command.StartTime, command.EndTime)) } - if command.RecordNumber != "" { - rn, err = strconv.ParseInt(command.RecordNumber, 10, 64) - if rn <= 0 || err != nil { - return errors.New(fmt.Sprintf(ui.InvalidRecordNumber, command.RecordNumber)) - } - } - if command.StartTime != "" && command.EndTime != "" { - rn = math.MaxInt64 - } + fpo = command.StartTime == "" && command.EndTime == "" if command.Output != "" { writer, err = os.OpenFile(command.Output, os.O_CREATE|os.O_WRONLY, 0666) @@ -82,10 +69,10 @@ func (command MetricsCommand) Execute([]string) error { } return RetrieveAggregatedMetrics(AutoScaler.CLIConnection, command.RequiredlArgs.AppName, command.RequiredlArgs.MetricName, - st, et, rn, command.Desc, writer, command.Output) + st, et, fpo, command.Desc, command.Asc, writer, command.Output) } -func RetrieveAggregatedMetrics(cliConnection api.Connection, appName, metricName string, startTime, endTime, recordNumber int64, desc bool, writer io.Writer, outputfile string) error { +func RetrieveAggregatedMetrics(cliConnection api.Connection, appName, metricName string, startTime, endTime int64, firstPageOnly bool, desc bool, asc bool, writer io.Writer, outputfile string) error { cfclient, err := api.NewCFClient(cliConnection) if err != nil { @@ -115,30 +102,28 @@ func RetrieveAggregatedMetrics(cliConnection api.Connection, appName, metricName table := ui.NewTable(writer, []string{"Metrics Name", "Value", "Timestamp"}) var ( - page uint64 = 1 - currentNumber int64 = 0 - next bool = true - noResult bool = true - data [][]string + page uint64 = 1 + next bool = true + noResult bool = true + moreResult bool = false + data [][]string ) for true { - next, data, err = apihelper.GetAggregatedMetrics(metricName, startTime, endTime, desc, page) + next, data, err = apihelper.GetAggregatedMetrics(metricName, startTime, endTime, asc, page) if err != nil { return err } for _, row := range data { - if recordNumber == 0 || currentNumber < recordNumber { - table.Add(row) - currentNumber++ - } + table.Add(row) } if len(data) > 0 { noResult = false table.Print() } - if !next || currentNumber >= recordNumber { + moreResult = next && firstPageOnly + if !next || firstPageOnly { break } page += 1 @@ -154,6 +139,12 @@ func RetrieveAggregatedMetrics(cliConnection api.Connection, appName, metricName ui.SayOK() } } + if moreResult { + ui.SayWarningMessage(ui.MoreRecordsWarning) + } + if desc { + ui.SayWarningMessage(ui.DeprecatedDescWarning) + } return nil } diff --git a/src/cli/main.go b/src/cli/main.go index 95aca87..6316cba 100644 --- a/src/cli/main.go +++ b/src/cli/main.go @@ -60,21 +60,41 @@ OPTIONS: Usage: `cf detach-as-policy APP_NAME`, }, }, + { + Name: "create-autoscaling-credential", + Alias: "casc", + HelpText: "Create custom metric credential for an application", + UsageDetails: plugin.Usage{ + Usage: `cf create-autoscaling-credential APP_NAME [--username USERNAME --password PASSWORD] [--output PATH_TO_FILE] + +OPTIONS: + --username, -u Username of the custom metric credential, random username will be set if not specified. + --password, -p Password of the custom metric credential, random password will be set if not specified. + --output Dump the credential to a file in JSON format. + `, + }, + }, + { + Name: "delete-autoscaling-credential", + Alias: "dasc", + HelpText: "Delete the custom metric credential of an application", + UsageDetails: plugin.Usage{ + Usage: `cf delete-autoscaling-credential APP_NAME`, + }, + }, { Name: "autoscaling-metrics", Alias: "asm", HelpText: "Retrieve the metrics of an application", UsageDetails: plugin.Usage{ - Usage: `cf autoscaling-metrics APP_NAME METRIC_NAME [--start START_TIME] [--end END_TIME] [--number NUMBER] [--desc] [--output PATH_TO_FILE] - -METRIC_NAME: - memoryused, memoryutil, responsetime, throughput, cpu. + Usage: `cf autoscaling-metrics APP_NAME METRIC_NAME [--start START_TIME] [--end END_TIME] [--asc] [--output PATH_TO_FILE] +METRIC_NAME: + memoryused, memoryutil, responsetime, throughput, cpu or custom metric names. OPTIONS: --start Start time of metrics collected with format "yyyy-MM-ddTHH:mm:ss+/-HH:mm" or "yyyy-MM-ddTHH:mm:ssZ", default to very beginning if not specified. --end End time of the metrics collected with format "yyyy-MM-ddTHH:mm:ss+/-HH:mm" or "yyyy-MM-ddTHH:mm:ssZ", default to current time if not speficied. - --number The number of the records to return, will be ignored if both start time and end time are specified. - --desc Display in descending order, default to ascending order if not specified. + --asc Display in ascending order, default to descending order if not specified. --output Dump the metrics to a file in table format. `, }, @@ -84,13 +104,12 @@ OPTIONS: Alias: "ash", HelpText: "Retrieve the scaling history of an application", UsageDetails: plugin.Usage{ - Usage: `cf autoscaling-history APP_NAME [--start START_TIME] [--end END_TIME] [--number NUMBER] [--desc] [--output PATH_TO_FILE] + Usage: `cf autoscaling-history APP_NAME [--start START_TIME] [--end END_TIME] [--asc] [--output PATH_TO_FILE] OPTIONS: --start Start time of the scaling history with format "yyyy-MM-ddTHH:mm:ss+/-HH:mm" or "yyyy-MM-ddTHH:mm:ssZ", default to very beginning if not specified. --end End time of the scaling history with format "yyyy-MM-ddTHH:mm:ss+/-HH:mm" or "yyyy-MM-ddTHH:mm:ssZ", default to current time if not speficied. - --number The number of the records to return, will be ignored if both start time and end time are specified. - --desc Display in descending order, default to ascending order if not specified. + --asc Display in ascending order, default to descending order if not specified. --output Dump the scaling history to a file in table format. `, }, diff --git a/src/cli/main_test.go b/src/cli/main_test.go index 430793a..090510b 100644 --- a/src/cli/main_test.go +++ b/src/cli/main_test.go @@ -93,6 +93,11 @@ var _ = Describe("App-AutoScaler Commands", func() { }, }, } + + fakeCredential Credential = Credential{ + Username: "fake-user", + Password: "fake-password", + } ) BeforeEach(func() { @@ -976,7 +981,7 @@ var _ = Describe("App-AutoScaler Commands", func() { } }) - Context("when attached policy definition is invalid ", func() { + Context("when attached policy definition is invalid with error object response", func() { BeforeEach(func() { apiServer.RouteToHandler("PUT", urlpath, ghttp.CombineHandlers( @@ -1009,6 +1014,39 @@ var _ = Describe("App-AutoScaler Commands", func() { }) }) + Context("when attached policy definition is invalid with error array response", func() { + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, + ghttp.CombineHandlers( + ghttp.RespondWith(http.StatusBadRequest, `[{"context":"(root).instance_min_count","description":"instance_min_count 10 is higher or equal to instance_max_count 2"}]`), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ), + ) + + fakePolicy.InstanceMin = 10 + fakePolicy.InstanceMax = 2 + policyBytes, err := cjson.MarshalWithoutHTMLEscape(fakePolicy) + Expect(err).NotTo(HaveOccurred()) + err = ioutil.WriteFile(outputFile, policyBytes, 0666) + Expect(err).NotTo(HaveOccurred()) + + }) + + It("Failed with 400", func() { + + args = []string{ts.Port(), "attach-autoscaling-policy", fakeAppName, outputFile} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session.Out).To(gbytes.Say(ui.AttachPolicyHint, fakeAppName)) + Expect(session).To(gbytes.Say("FAILED")) + Expect(session).To(gbytes.Say(ui.InvalidPolicy, "\n"+`\(root\)\.instance_min_count: instance_min_count 10 is higher or equal to instance_max_count 2`)) + Expect(session.ExitCode()).To(Equal(1)) + + }) + }) + Context("when No policy defined previously", func() { BeforeEach(func() { apiServer.RouteToHandler("PUT", urlpath, @@ -1273,137 +1311,41 @@ var _ = Describe("App-AutoScaler Commands", func() { }) }) - Describe("Commands autoscaling-metrics, asm", func() { - - var ( - metricName = "memoryused" - aggregatedMetricsUrlPath = "/v1/apps/" + fakeAppId + "/aggregated_metric_histories/" + metricName - now = time.Now() - lowPrecisionNowInNano = (now.UnixNano() / 1E9) * 1E9 - ) + Describe("Commands create-autoscaling-credential, aasp", func() { - Context("autoscaling-metrics", func() { - - Context("when the args or options are not properly provided", func() { + var urlpath = "/v1/apps/" + fakeAppId + "/credential" + Context("create-autoscaling-credential", func() { + Context("when the args are not properly provided", func() { It("Require APP_NAME as argument", func() { - args = []string{ts.Port(), "autoscaling-metrics"} - session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() - - Expect(session).To(gbytes.Say("required arguments `APP_NAME` and `METRIC_NAME` were not provided")) - Expect(session.ExitCode()).To(Equal(1)) - }) - - It("Require METRIC_NAME as argument", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName} - session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() - - Expect(session).To(gbytes.Say("required argument `METRIC_NAME` was not provided")) - Expect(session.ExitCode()).To(Equal(1)) - }) - - It("Failed when METRIC_NAME is unsupported", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, "fakeMetricName"} + args = []string{ts.Port(), "create-autoscaling-credential"} session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() - Expect(session).To(gbytes.Say(fmt.Sprintf(ui.UnrecognizedMetricName, "fakeMetricName"))) - Expect(session.ExitCode()).To(Equal(1)) - }) - - It("Failed when start/end time is defined in unsupported time format", func() { - invalidTime := now.Format(time.UnixDate) - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "--start", invalidTime} - session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() - - Expect(session).To(gbytes.Say("Unrecognized date time format")) - Expect(session.ExitCode()).To(Equal(1)) - - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "--end", invalidTime} - session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() - - Expect(session).To(gbytes.Say("Unrecognized date time format")) - Expect(session.ExitCode()).To(Equal(1)) - }) - - It("Failed when start/end time is prior to 1970-01-01T00:00:00Z", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, - "--start", "1969-12-31-T00:00:00Z", - "--end", "1969-12-31-T23:59:59Z", - } - session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() - - Expect(session).To(gbytes.Say("Unrecognized date time format")) - Expect(session.ExitCode()).To(Equal(1)) - }) - - It("Failed when start time is greater than end time", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, - "--start", now.Format(time.RFC3339), - "--end", time.Unix(0, now.UnixNano()-int64(30*1E9)).Format(time.RFC3339), - } - session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() - - expects := strings.Split(ui.InvalidTimeRange, "%s") - for _, expect := range expects { - Expect(session).To(gbytes.Say(expect)) - } - Expect(session.ExitCode()).To(Equal(1)) - }) - - It("Failed when record number is not an integer", func() { - By("long") - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "--number", "not-integer"} - session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() - - Expect(session).To(gbytes.Say(fmt.Sprintf(ui.InvalidRecordNumber, "not-integer"))) - Expect(session.ExitCode()).To(Equal(1)) - - By("short") - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "-n", "0"} - session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() - - Expect(session).To(gbytes.Say(fmt.Sprintf(ui.InvalidRecordNumber, "0"))) + Expect(session).To(gbytes.Say("the required argument `APP_NAME` was not provided")) Expect(session.ExitCode()).To(Equal(1)) }) - It("Failed when --desc is wrong spelled", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "--dddesc"} + It("Require USERNAME when PASSWORD is provided", func() { + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName, "--password" , fakeCredential.Password} session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() - Expect(session).To(gbytes.Say("unknown flag")) + Expect(session).To(gbytes.Say("Both USERNAME and PASSWORD need to be provided for user-defined credential.")) Expect(session.ExitCode()).To(Equal(1)) }) - It("Failed when output file path is invalid", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "--output", "invalidDir/invalidFile"} + It("Require PASSWORD when USERNAME is provided", func() { + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName, "--username" , fakeCredential.Username} session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() - Expect(session).To(gbytes.Say("open invalidDir/invalidFile: no such file or directory")) + Expect(session).To(gbytes.Say("Both USERNAME and PASSWORD need to be provided for user-defined credential.")) Expect(session.ExitCode()).To(Equal(1)) }) - }) Context("When cf api is not set ", func() { @@ -1424,7 +1366,7 @@ var _ = Describe("App-AutoScaler Commands", func() { }) It("Failed with missing cf api setting", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName} session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() @@ -1450,7 +1392,7 @@ var _ = Describe("App-AutoScaler Commands", func() { }) It("Failed with no api endpoint setting", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName} session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() @@ -1462,7 +1404,7 @@ var _ = Describe("App-AutoScaler Commands", func() { Context("when cf not login", func() { It("exits with 'You must be logged in' error ", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName} session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() @@ -1487,7 +1429,7 @@ var _ = Describe("App-AutoScaler Commands", func() { }) It("exits with 'App not found' error ", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName} session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() @@ -1520,15 +1462,13 @@ var _ = Describe("App-AutoScaler Commands", func() { return nil } - apiServer.RouteToHandler("GET", aggregatedMetricsUrlPath, - ghttp.CombineHandlers( - ghttp.RespondWith(http.StatusUnauthorized, ""), - ), + apiServer.RouteToHandler("PUT", urlpath, + ghttp.RespondWith(http.StatusUnauthorized, ""), ) }) It("failed with 401 error", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName} session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() @@ -1539,256 +1479,769 @@ var _ = Describe("App-AutoScaler Commands", func() { }) Context("when access token is correct", func() { - BeforeEach(func() { rpcHandlers.AccessTokenStub = func(args string, retVal *string) error { *retVal = fakeAccessToken return nil } }) - Context("when no aggregated metric record in desired duration", func() { + + Context("when request is forbidden", func() { BeforeEach(func() { - apiServer.RouteToHandler("GET", aggregatedMetricsUrlPath, + apiServer.RouteToHandler("PUT", urlpath, ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 0, - TotalPages: 0, - Page: 1, - Metrics: []*AppAggregatedMetric{}, - }), + ghttp.RespondWith(http.StatusForbidden, `{"code":"Forbidden","message":"This command is only valid for build-in auto-scaling capacity. Please operate service credential with \"cf bind/unbind-service\" command."}`), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), ), ) }) - It("Succeed and return no record", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, - "--start", now.Format(time.RFC3339), - "--end", time.Unix(0, lowPrecisionNowInNano+int64(9*30*1E9)).Format(time.RFC3339)} - + It("Failed with 403", func() { + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName} session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() - Expect(session).To(gbytes.Say("OK")) - Expect(session).To(gbytes.Say(ui.AggregatedMetricsNotFound, metricName, fakeAppName)) - Expect(session.ExitCode()).To(Equal(0)) - + Expect(session.Out).To(gbytes.Say(ui.CreateCredentialHint, fakeAppName)) + Expect(session).To(gbytes.Say("FAILED")) + Expect(session).To(gbytes.Say(`This command is only valid for build-in auto-scaling capacity. Please operate service credential with "cf bind/unbind-service" command.`)) + Expect(session.ExitCode()).To(Equal(1)) }) }) - Context("when metrics are available", func() { - var metrics, reversedMetrics []*AppAggregatedMetric - + Context("when created credential definition is invalid with error object response", func() { BeforeEach(func() { - for i := 0; i < 30; i++ { - metrics = append(metrics, &AppAggregatedMetric{ - AppId: fakeAppId, - Name: "memoryused", - Unit: "MB", - Value: "100", - Timestamp: now.UnixNano() + int64(i*30*1E9), - }) - } + apiServer.RouteToHandler("PUT", urlpath, + ghttp.CombineHandlers( + ghttp.RespondWith(http.StatusBadRequest, `{"code":"Bad Request","message":"Username and password are both required"}`), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ), + ) + }) - for i := 0; i < 30; i++ { - reversedMetrics = append(reversedMetrics, metrics[len(metrics)-1-i]) - } + It("Failed with 400", func() { + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName, "--username", fakeCredential.Username, "--password", fakeCredential.Password} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + Expect(session.Out).To(gbytes.Say(ui.CreateCredentialHint, fakeAppName)) + Expect(session).To(gbytes.Say("FAILED")) + Expect(session).To(gbytes.Say(ui.InvalidCredential, "Username and password are both required")) + Expect(session.ExitCode()).To(Equal(1)) }) - Context("Query with default options ", func() { + }) + Context("Succeed to print the credential to stdout", func() { + Context("when No credential defined previously", func() { BeforeEach(func() { - apiServer.AppendHandlers( + apiServer.RouteToHandler("PUT", urlpath, ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 20, - TotalPages: 2, - Page: 1, - Metrics: metrics[0:10], - }), + ghttp.RespondWithJSONEncoded(http.StatusCreated, &fakeCredential), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), ), ) - apiServer.AppendHandlers( + }) + + It("Succeed with 201 when credential metadata is not provided", func() { + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session.Out).To(gbytes.Say(ui.CreateCredentialHint, fakeAppName)) + Expect(session.Out).To(gbytes.Say(ui.CreateCredentialWarning, fakeAppName)) + + credential := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.CreateCredentialHint+"\n", fakeAppName))) + credential = bytes.TrimSuffix(credential, []byte(fmt.Sprintf(ui.CreateCredentialWarning+"\n", fakeAppName))) + var actualCredential Credential + _ = json.Unmarshal(credential, &actualCredential) + Expect(actualCredential).To(MatchFields(IgnoreExtras, Fields{ + "Username": Equal(fakeCredential.Username), + "Password": Equal(fakeCredential.Password), + })) + + Expect(session.ExitCode()).To(Equal(0)) + }) + + It("Succeed with 201 when credential metadata is provided", func() { + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName, "--username", fakeCredential.Username, "--password", fakeCredential.Password} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session.Out).To(gbytes.Say(ui.CreateCredentialHint, fakeAppName)) + Expect(session.Out).To(gbytes.Say(ui.CreateCredentialWarning, fakeAppName)) + + credential := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.CreateCredentialHint+"\n", fakeAppName))) + credential = bytes.TrimSuffix(credential, []byte(fmt.Sprintf(ui.CreateCredentialWarning+"\n", fakeAppName))) + var actualCredential Credential + _ = json.Unmarshal(credential, &actualCredential) + Expect(actualCredential).To(MatchFields(IgnoreExtras, Fields{ + "Username": Equal(fakeCredential.Username), + "Password": Equal(fakeCredential.Password), + })) + + Expect(session.ExitCode()).To(Equal(0)) + }) + }) + + Context("when credential exist previously ", func() { + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 20, - TotalPages: 2, - Page: 2, - Metrics: metrics[10:20], - }), + ghttp.RespondWithJSONEncoded(http.StatusCreated, &fakeCredential), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), ), ) - }) - - It("Succeed to print first page of the metrics to stdout with asc order", func() { - - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} - + + It("Succeed with 200 when credential metadata is not provided", func() { + + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName} session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() - - Expect(session.Out).To(gbytes.Say(ui.ShowAggregatedMetricsHint, metricName, fakeAppName)) - metricsRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowAggregatedMetricsHint+"\n", metricName, fakeAppName))) - metricsTable := strings.Split(string(bytes.TrimRight(metricsRaw, "\n")), "\n") - Expect(len(metricsTable)).To(Equal(11)) - for i, row := range metricsTable { - colomns := strings.Split(row, "\t") - if i == 0 { - Expect(strings.Trim(colomns[0], " ")).To(Equal("Metrics Name")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("Value")) - Expect(strings.Trim(colomns[2], " ")).To(Equal("Timestamp")) - } else { - Expect(strings.Trim(colomns[0], " ")).To(Equal("memoryused")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("100MB")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*30*1E9)).Format(time.RFC3339))) - } - } + + Expect(session.Out).To(gbytes.Say(ui.CreateCredentialHint, fakeAppName)) + Expect(session.Out).To(gbytes.Say(ui.CreateCredentialWarning, fakeAppName)) + + credential := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.CreateCredentialHint+"\n", fakeAppName))) + credential = bytes.TrimSuffix(credential, []byte(fmt.Sprintf(ui.CreateCredentialWarning+"\n", fakeAppName))) + var actualCredential Credential + _ = json.Unmarshal(credential, &actualCredential) + Expect(actualCredential).To(MatchFields(IgnoreExtras, Fields{ + "Username": Equal(fakeCredential.Username), + "Password": Equal(fakeCredential.Password), + })) + Expect(session.ExitCode()).To(Equal(0)) }) - + + It("Succeed with 200 when credential metadata is provided", func() { + + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName, "--username", fakeCredential.Username, "--password", fakeCredential.Password} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session.Out).To(gbytes.Say(ui.CreateCredentialHint, fakeAppName)) + Expect(session.Out).To(gbytes.Say(ui.CreateCredentialWarning, fakeAppName)) + + credential := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.CreateCredentialHint+"\n", fakeAppName))) + credential = bytes.TrimSuffix(credential, []byte(fmt.Sprintf(ui.CreateCredentialWarning+"\n", fakeAppName))) + var actualCredential Credential + _ = json.Unmarshal(credential, &actualCredential) + Expect(actualCredential).To(MatchFields(IgnoreExtras, Fields{ + "Username": Equal(fakeCredential.Username), + "Password": Equal(fakeCredential.Password), + })) + + Expect(session.ExitCode()).To(Equal(0)) + }) + }) + }) - Context("Query multiple pages with asc order ", func() { + Context("Succeed to print the credential to file", func() { - BeforeEach(func() { - //simulate the asc response from api server - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 30, - TotalPages: 3, - Page: 1, - Metrics: metrics[0:10], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=asc&page=1&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), - ), - ), - ) - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 30, - TotalPages: 3, - Page: 2, - Metrics: metrics[10:20], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=asc&page=2&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), - ), - ), - ) - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 30, - TotalPages: 3, - Page: 3, - Metrics: metrics[20:30], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=asc&page=3&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), - ), - ), - ) + BeforeEach(func() { + apiServer.RouteToHandler("PUT", urlpath, + ghttp.CombineHandlers( + ghttp.RespondWithJSONEncoded(http.StatusCreated, &fakeCredential), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ), + ) + }) + + It("succeed", func() { + args = []string{ts.Port(), "create-autoscaling-credential", fakeAppName, "--output", outputFile} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session.Out).To(gbytes.Say(ui.SaveCredentialHint, fakeAppName, outputFile)) + Expect(session.Out).To(gbytes.Say(ui.CreateCredentialWarning, fakeAppName)) + + Expect(outputFile).To(BeARegularFile()) + contents, err := ioutil.ReadFile(outputFile) + Expect(err).NotTo(HaveOccurred()) + + var actualCredential Credential + _ = json.Unmarshal(contents, &actualCredential) + + Expect(actualCredential).To(MatchFields(IgnoreExtras, Fields{ + "Username": Equal(fakeCredential.Username), + "Password": Equal(fakeCredential.Password), + })) + Expect(session.ExitCode()).To(Equal(0)) + }) + + }) + + }) + + }) + + }) + }) + }) + + Describe("Commands delete-autoscaling-credential, dasc", func() { + + var urlpath = "/v1/apps/" + fakeAppId + "/credential" + Context("delete-autoscaling-credential", func() { + + Context("when the args are not properly provided", func() { + It("Require APP_NAME as argument", func() { + args = []string{ts.Port(), "delete-autoscaling-credential"} + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say("required argument `APP_NAME` was not provided")) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) + + Context("When cf api is not set ", func() { + BeforeEach(func() { + + rpcHandlers.ApiEndpointStub = func(_ string, retVal *string) error { + *retVal = "" + return nil + } + + args = []string{ts.Port(), "autoscaling-api", apiEndpoint} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + + rpcHandlers.ApiEndpointStub = func(_ string, retVal *string) error { + *retVal = "" + return nil + } + }) + + It("Failed with missing cf api setting", func() { + args = []string{ts.Port(), "delete-autoscaling-credential", fakeAppName} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + Expect(session).To(gbytes.Say(ui.NOCFAPIEndpoint)) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) + + Context("When cf api is changed ", func() { + BeforeEach(func() { + urlConfig := []byte(fmt.Sprintf(`{"URL":"%s"}`, "autoscaler.bosh-lite.com")) + err = ioutil.WriteFile(api.ConfigFile(), urlConfig, 0600) + Expect(err).NotTo(HaveOccurred()) + + }) + + Context("When the default endpoint doesn't work", func() { + + BeforeEach(func() { + apiServer.RouteToHandler("GET", "/health", + ghttp.RespondWith(http.StatusNotFound, ""), + ) + }) + + It("Failed with no api endpoint setting", func() { + args = []string{ts.Port(), "delete-autoscaling-credential", fakeAppName} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + Expect(session).To(gbytes.Say(ui.NoEndpoint)) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) + }) + + Context("when cf not login", func() { + It("exits with 'You must be logged in' error ", func() { + args = []string{ts.Port(), "delete-autoscaling-credential", fakeAppName} + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + Expect(session).To(gbytes.Say("You must be logged in")) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) + + Context("when cf login", func() { + BeforeEach(func() { + rpcHandlers.IsLoggedInStub = func(args string, retVal *bool) error { + *retVal = true + return nil + } + }) + + Context("when app not found", func() { + BeforeEach(func() { + rpcHandlers.GetAppStub = func(_ string, retVal *plugin_models.GetAppModel) error { + return errors.New("App fakeApp not found") + } + }) + + It("exits with 'App not found' error ", func() { + args = []string{ts.Port(), "delete-autoscaling-credential", fakeAppName} + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + Expect(session).To(gbytes.Say("App fakeApp not found")) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) + + Context("when the app is found", func() { + BeforeEach(func() { + rpcHandlers.GetAppStub = func(_ string, retVal *plugin_models.GetAppModel) error { + *retVal = plugin_models.GetAppModel{ + Guid: fakeAppId, + } + return nil + } + }) + + JustBeforeEach(func() { + args = []string{ts.Port(), "autoscaling-api", apiEndpoint} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + }) + + Context("when access token is wrong", func() { + BeforeEach(func() { + rpcHandlers.AccessTokenStub = func(args string, retVal *string) error { + *retVal = "incorrectAccessToken" + return nil + } + + apiServer.RouteToHandler("DELETE", urlpath, + ghttp.RespondWith(http.StatusUnauthorized, ""), + ) + }) + + It("failed with 401 error", func() { + args = []string{ts.Port(), "delete-autoscaling-credential", fakeAppName} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say("Failed to access AutoScaler API endpoint")) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) + + Context("when access token is correct", func() { + BeforeEach(func() { + rpcHandlers.AccessTokenStub = func(args string, retVal *string) error { + *retVal = fakeAccessToken + return nil + } + }) + + Context("when request is forbidden", func() { + BeforeEach(func() { + apiServer.RouteToHandler("DELETE", urlpath, + ghttp.CombineHandlers( + ghttp.RespondWith(http.StatusForbidden, `{"code":"Forbidden","message":"This command is only valid for build-in auto-scaling capacity. Please operate service credential with \"cf bind/unbind-service\" command."}`), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ), + ) + }) + + It("Failed with 403", func() { + args = []string{ts.Port(), "delete-autoscaling-credential", fakeAppName} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session.Out).To(gbytes.Say(ui.DeleteCredentialHint, fakeAppName)) + Expect(session).To(gbytes.Say("FAILED")) + Expect(session).To(gbytes.Say(`This command is only valid for build-in auto-scaling capacity. Please operate service credential with "cf bind/unbind-service" command.`)) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) + + Context("when credential exist or not ", func() { + BeforeEach(func() { + apiServer.RouteToHandler("DELETE", urlpath, + ghttp.CombineHandlers( + ghttp.RespondWith(http.StatusOK, ""), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ), + ) + }) + + It("succeed", func() { + + args = []string{ts.Port(), "delete-autoscaling-credential", fakeAppName} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session.Out).To(gbytes.Say(ui.DeleteCredentialHint, fakeAppName)) + Expect(session.Out).To(gbytes.Say("OK")) + Expect(session.Out).To(gbytes.Say(ui.DeleteCredentialWarning, fakeAppName)) + Expect(session.ExitCode()).To(Equal(0)) + }) + + }) + + }) + + }) + + }) + }) + }) + + Describe("Commands autoscaling-metrics, asm", func() { + + var ( + metricName = "memoryused" + aggregatedMetricsUrlPath = "/v1/apps/" + fakeAppId + "/aggregated_metric_histories/" + metricName + now = time.Now() + lowPrecisionNowInNano = (now.UnixNano() / 1E9) * 1E9 + ) + + Context("autoscaling-metrics", func() { + + Context("when the args or options are not properly provided", func() { + + It("Require APP_NAME as argument", func() { + args = []string{ts.Port(), "autoscaling-metrics"} + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say("required arguments `APP_NAME` and `METRIC_NAME` were not provided")) + Expect(session.ExitCode()).To(Equal(1)) + }) + + It("Require METRIC_NAME as argument", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName} + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say("required argument `METRIC_NAME` was not provided")) + Expect(session.ExitCode()).To(Equal(1)) + }) + + It("Failed when METRIC_NAME is unsupported", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, "invalid-metric-name%"} + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say(fmt.Sprintf(ui.UnrecognizedMetricName, "invalid-metric-name%"))) + Expect(session.ExitCode()).To(Equal(1)) + }) + + It("Failed when start/end time is defined in unsupported time format", func() { + invalidTime := now.Format(time.UnixDate) + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "--start", invalidTime} + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say("Unrecognized date time format")) + Expect(session.ExitCode()).To(Equal(1)) + + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "--end", invalidTime} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say("Unrecognized date time format")) + Expect(session.ExitCode()).To(Equal(1)) + }) + + It("Failed when start/end time is prior to 1970-01-01T00:00:00Z", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, + "--start", "1969-12-31-T00:00:00Z", + "--end", "1969-12-31-T23:59:59Z", + } + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say("Unrecognized date time format")) + Expect(session.ExitCode()).To(Equal(1)) + }) + + It("Failed when start time is greater than end time", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, + "--start", now.Format(time.RFC3339), + "--end", time.Unix(0, now.UnixNano()-int64(30*1E9)).Format(time.RFC3339), + } + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + expects := strings.Split(ui.InvalidTimeRange, "%s") + for _, expect := range expects { + Expect(session).To(gbytes.Say(expect)) + } + Expect(session.ExitCode()).To(Equal(1)) + }) + + It("Failed when --asc is wrong spelled", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "--aaasc"} + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say("unknown flag")) + Expect(session.ExitCode()).To(Equal(1)) + }) + + It("Failed when --desc and --asc are used at the same time", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "--asc", "--desc"} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say(ui.DeprecatedDescWarning)) + Expect(session.ExitCode()).To(Equal(1)) + }) + + It("Failed when output file path is invalid", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "--output", "invalidDir/invalidFile"} + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say("open invalidDir/invalidFile: no such file or directory")) + Expect(session.ExitCode()).To(Equal(1)) + }) + + }) + + Context("When cf api is not set ", func() { + BeforeEach(func() { + + rpcHandlers.ApiEndpointStub = func(_ string, retVal *string) error { + *retVal = "" + return nil + } + + args = []string{ts.Port(), "autoscaling-api", apiEndpoint} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + + rpcHandlers.ApiEndpointStub = func(_ string, retVal *string) error { + *retVal = "" + return nil + } + }) + + It("Failed with missing cf api setting", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + Expect(session).To(gbytes.Say(ui.NOCFAPIEndpoint)) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) + + Context("When cf api is changed ", func() { + BeforeEach(func() { + urlConfig := []byte(fmt.Sprintf(`{"URL":"%s"}`, "autoscaler.bosh-lite.com")) + err = ioutil.WriteFile(api.ConfigFile(), urlConfig, 0600) + Expect(err).NotTo(HaveOccurred()) + + }) + + Context("When the default endpoint doesn't work", func() { + + BeforeEach(func() { + apiServer.RouteToHandler("GET", "/health", + ghttp.RespondWith(http.StatusNotFound, ""), + ) + }) + + It("Failed with no api endpoint setting", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + Expect(session).To(gbytes.Say(ui.NoEndpoint)) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) + }) - }) + Context("when cf not login", func() { + It("exits with 'You must be logged in' error ", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + Expect(session).To(gbytes.Say("You must be logged in")) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) - It("Succeed to print all pages of the metrics to stdout with asc order", func() { + Context("when cf login", func() { + BeforeEach(func() { + rpcHandlers.IsLoggedInStub = func(args string, retVal *bool) error { + *retVal = true + return nil + } + }) - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, - "--start", now.Format(time.RFC3339), - "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*30*1E9)).Format(time.RFC3339)} + Context("when app not found", func() { + BeforeEach(func() { + rpcHandlers.GetAppStub = func(_ string, retVal *plugin_models.GetAppModel) error { + return errors.New("App fakeApp not found") + } + }) - session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() + It("exits with 'App not found' error ", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} + session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + Expect(session).To(gbytes.Say("App fakeApp not found")) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) - Expect(session.Out).To(gbytes.Say(ui.ShowAggregatedMetricsHint, metricName, fakeAppName)) - metricsRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowAggregatedMetricsHint+"\n", metricName, fakeAppName))) - metricsTable := strings.Split(string(bytes.TrimRight(metricsRaw, "\n")), "\n") - Expect(len(metricsTable)).To(Equal(31)) - for i, row := range metricsTable { - colomns := strings.Split(row, "\t") - if i == 0 { - Expect(strings.Trim(colomns[0], " ")).To(Equal("Metrics Name")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("Value")) - Expect(strings.Trim(colomns[2], " ")).To(Equal("Timestamp")) - } else { - Expect(strings.Trim(colomns[0], " ")).To(Equal("memoryused")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("100MB")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*30*1E9)).Format(time.RFC3339))) - } - } - Expect(session.ExitCode()).To(Equal(0)) - }) + Context("when the app is found", func() { + BeforeEach(func() { + rpcHandlers.GetAppStub = func(_ string, retVal *plugin_models.GetAppModel) error { + *retVal = plugin_models.GetAppModel{ + Guid: fakeAppId, + } + return nil + } + }) + + JustBeforeEach(func() { + args = []string{ts.Port(), "autoscaling-api", apiEndpoint} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + }) + + Context("when access token is wrong", func() { + BeforeEach(func() { + rpcHandlers.AccessTokenStub = func(args string, retVal *string) error { + *retVal = "incorrectAccessToken" + return nil + } + + apiServer.RouteToHandler("GET", aggregatedMetricsUrlPath, + ghttp.CombineHandlers( + ghttp.RespondWith(http.StatusUnauthorized, ""), + ), + ) + }) + + It("failed with 401 error", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say("Failed to access AutoScaler API endpoint")) + Expect(session.ExitCode()).To(Equal(1)) + }) + }) + + Context("when access token is correct", func() { + + BeforeEach(func() { + rpcHandlers.AccessTokenStub = func(args string, retVal *string) error { + *retVal = fakeAccessToken + return nil + } + }) + Context("when no aggregated metric record in desired duration", func() { + BeforeEach(func() { + apiServer.RouteToHandler("GET", aggregatedMetricsUrlPath, + ghttp.CombineHandlers( + ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ + TotalResults: 0, + TotalPages: 0, + Page: 1, + Metrics: []*AppAggregatedMetric{}, + }), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ), + ) + }) + + It("Succeed and return no record", func() { + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, + "--start", now.Format(time.RFC3339), + "--end", time.Unix(0, lowPrecisionNowInNano+int64(9*30*1E9)).Format(time.RFC3339)} + + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session).To(gbytes.Say("OK")) + Expect(session).To(gbytes.Say(ui.AggregatedMetricsNotFound, metricName, fakeAppName)) + Expect(session.ExitCode()).To(Equal(0)) }) + }) - Context("Query multiple pages with desc order ", func() { + Context("when metrics are available", func() { + var metrics, reversedMetrics []*AppAggregatedMetric + + BeforeEach(func() { + for i := 0; i < 30; i++ { + metrics = append(metrics, &AppAggregatedMetric{ + AppId: fakeAppId, + Name: "memoryused", + Unit: "MB", + Value: "100", + Timestamp: now.UnixNano() + int64(i*30*1E9), + }) + } + + for i := 0; i < 30; i++ { + reversedMetrics = append(reversedMetrics, metrics[len(metrics)-1-i]) + } + + }) + Context("Query with default options ", func() { BeforeEach(func() { - //simulate the desc response from api server apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 30, - TotalPages: 3, + TotalResults: 20, + TotalPages: 2, Page: 1, Metrics: reversedMetrics[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=desc&page=1&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), - ), ), ) apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 30, - TotalPages: 3, + TotalResults: 20, + TotalPages: 2, Page: 2, Metrics: reversedMetrics[10:20], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=desc&page=2&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), - ), - ), - ) - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 30, - TotalPages: 3, - Page: 3, - Metrics: reversedMetrics[20:30], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=desc&page=3&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), - ), ), ) }) - It("Succeed to print all pages of the metrics to stdout with desc order", func() { + It("Succeed to print first page of the metrics to stdout with desc order", func() { - args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, - "--start", now.Format(time.RFC3339), - "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*30*1E9)).Format(time.RFC3339), - "--desc", - } + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName} session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) @@ -1797,15 +2250,14 @@ var _ = Describe("App-AutoScaler Commands", func() { Expect(session.Out).To(gbytes.Say(ui.ShowAggregatedMetricsHint, metricName, fakeAppName)) metricsRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowAggregatedMetricsHint+"\n", metricName, fakeAppName))) metricsTable := strings.Split(string(bytes.TrimRight(metricsRaw, "\n")), "\n") - Expect(len(metricsTable)).To(Equal(31)) + Expect(len(metricsTable)).To(Equal(12)) for i, row := range metricsTable { colomns := strings.Split(row, "\t") if i == 0 { Expect(strings.Trim(colomns[0], " ")).To(Equal("Metrics Name")) Expect(strings.Trim(colomns[1], " ")).To(Equal("Value")) Expect(strings.Trim(colomns[2], " ")).To(Equal("Timestamp")) - } else { - //use "29-(i-1)" to simulate the expected output in desc order + } else if i != len(metricsTable)-1 { Expect(strings.Trim(colomns[0], " ")).To(Equal("memoryused")) Expect(strings.Trim(colomns[1], " ")).To(Equal("100MB")) Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((29-(i-1))*30*1E9)).Format(time.RFC3339))) @@ -1816,21 +2268,23 @@ var _ = Describe("App-AutoScaler Commands", func() { }) - Context("Query multiple pages with specified record number ", func() { - - Context("not specify starttime and entime ", func() { + Context("Query multiple pages with desc order ", func() { + Context("specifiy --start and --end both ", func() { BeforeEach(func() { - //simulate the asc response from api server + //simulate the desc response from api server apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ TotalResults: 30, TotalPages: 3, Page: 1, - Metrics: metrics[0:10], + Metrics: reversedMetrics[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, + fmt.Sprintf("order=desc&page=1&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), + ), ), ) apiServer.AppendHandlers( @@ -1839,9 +2293,12 @@ var _ = Describe("App-AutoScaler Commands", func() { TotalResults: 30, TotalPages: 3, Page: 2, - Metrics: metrics[10:20], + Metrics: reversedMetrics[10:20], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, + fmt.Sprintf("order=desc&page=2&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), + ), ), ) apiServer.AppendHandlers( @@ -1850,18 +2307,22 @@ var _ = Describe("App-AutoScaler Commands", func() { TotalResults: 30, TotalPages: 3, Page: 3, - Metrics: metrics[20:30], + Metrics: reversedMetrics[20:30], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, + fmt.Sprintf("order=desc&page=3&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), + ), ), ) }) - It("Succeed to print limited number of the metrics to stdout with asc order ", func() { + It("Succeed to print all pages of the metrics to stdout with default desc order", func() { args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, - "--number", "15"} + "--start", now.Format(time.RFC3339), + "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*30*1E9)).Format(time.RFC3339)} session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) @@ -1870,7 +2331,7 @@ var _ = Describe("App-AutoScaler Commands", func() { Expect(session.Out).To(gbytes.Say(ui.ShowAggregatedMetricsHint, metricName, fakeAppName)) metricsRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowAggregatedMetricsHint+"\n", metricName, fakeAppName))) metricsTable := strings.Split(string(bytes.TrimRight(metricsRaw, "\n")), "\n") - Expect(len(metricsTable)).To(Equal(16)) + Expect(len(metricsTable)).To(Equal(31)) for i, row := range metricsTable { colomns := strings.Split(row, "\t") if i == 0 { @@ -1880,77 +2341,30 @@ var _ = Describe("App-AutoScaler Commands", func() { } else { Expect(strings.Trim(colomns[0], " ")).To(Equal("memoryused")) Expect(strings.Trim(colomns[1], " ")).To(Equal("100MB")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*30*1E9)).Format(time.RFC3339))) + Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((29-(i-1))*30*1E9)).Format(time.RFC3339))) } } Expect(session.ExitCode()).To(Equal(0)) }) - }) - - Context("specifiy endtime only ", func() { - - BeforeEach(func() { - //simulate the asc response from api server - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 30, - TotalPages: 3, - Page: 1, - Metrics: metrics[0:10], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=asc&page=1&end-time=%v", lowPrecisionNowInNano+int64(29*30*1E9)), - ), - ), - ) - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 30, - TotalPages: 3, - Page: 2, - Metrics: metrics[10:20], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=asc&page=2&end-time=%v", lowPrecisionNowInNano+int64(29*30*1E9)), - ), - ), - ) - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ - TotalResults: 30, - TotalPages: 3, - Page: 3, - Metrics: metrics[20:30], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=asc&page=3&end-time=%v", lowPrecisionNowInNano+int64(29*30*1E9)), - ), - ), - ) - - }) - - It("Succeed to print limited number of the metrics to stdout with asc order ", func() { + It("Succeed to print all pages of the metrics to stdout with specified desc order", func() { args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, - "--number", "15", - "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*30*1E9)).Format(time.RFC3339)} + "--start", now.Format(time.RFC3339), + "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*30*1E9)).Format(time.RFC3339), + "--desc", + } session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() Expect(session.Out).To(gbytes.Say(ui.ShowAggregatedMetricsHint, metricName, fakeAppName)) + Expect(session.Out).To(gbytes.Say(ui.DeprecatedDescWarning)) metricsRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowAggregatedMetricsHint+"\n", metricName, fakeAppName))) + metricsRaw = bytes.TrimSuffix(metricsRaw, []byte(ui.DeprecatedDescWarning+"\n")) metricsTable := strings.Split(string(bytes.TrimRight(metricsRaw, "\n")), "\n") - Expect(len(metricsTable)).To(Equal(16)) + Expect(len(metricsTable)).To(Equal(31)) for i, row := range metricsTable { colomns := strings.Split(row, "\t") if i == 0 { @@ -1960,28 +2374,27 @@ var _ = Describe("App-AutoScaler Commands", func() { } else { Expect(strings.Trim(colomns[0], " ")).To(Equal("memoryused")) Expect(strings.Trim(colomns[1], " ")).To(Equal("100MB")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*30*1E9)).Format(time.RFC3339))) + Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((29-(i-1))*30*1E9)).Format(time.RFC3339))) } } Expect(session.ExitCode()).To(Equal(0)) }) - }) - Context("specifiy start time and endtime ", func() { + Context("specifiy --end only ", func() { BeforeEach(func() { - //simulate the asc response from api server + //simulate the desc response from api server apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ TotalResults: 30, TotalPages: 3, Page: 1, - Metrics: metrics[0:10], + Metrics: reversedMetrics[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=asc&page=1&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), + fmt.Sprintf("order=desc&page=1&end-time=%v", lowPrecisionNowInNano+int64(29*30*1E9)), ), ), ) @@ -1991,11 +2404,11 @@ var _ = Describe("App-AutoScaler Commands", func() { TotalResults: 30, TotalPages: 3, Page: 2, - Metrics: metrics[10:20], + Metrics: reversedMetrics[10:20], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=asc&page=2&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), + fmt.Sprintf("order=desc&page=2&end-time=%v", lowPrecisionNowInNano+int64(29*30*1E9)), ), ), ) @@ -2005,22 +2418,20 @@ var _ = Describe("App-AutoScaler Commands", func() { TotalResults: 30, TotalPages: 3, Page: 3, - Metrics: metrics[20:30], + Metrics: reversedMetrics[20:30], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, - fmt.Sprintf("order=asc&page=3&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), + fmt.Sprintf("order=desc&page=3&end-time=%v", lowPrecisionNowInNano+int64(29*30*1E9)), ), ), ) }) - It("Succeed to ignore the record number limit and print all pages of the metrics in specified duration to stdout with asc order ", func() { + It("Succeed to print all pages of the metrics to stdout with desc order", func() { args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, - "--number", "15", - "--start", now.Format(time.RFC3339), "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*30*1E9)).Format(time.RFC3339)} session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) @@ -2040,13 +2451,94 @@ var _ = Describe("App-AutoScaler Commands", func() { } else { Expect(strings.Trim(colomns[0], " ")).To(Equal("memoryused")) Expect(strings.Trim(colomns[1], " ")).To(Equal("100MB")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*30*1E9)).Format(time.RFC3339))) + Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((29-(i-1))*30*1E9)).Format(time.RFC3339))) } } Expect(session.ExitCode()).To(Equal(0)) }) }) + }) + + Context("Query multiple pages with asc order ", func() { + + BeforeEach(func() { + //simulate the asc response from api server + apiServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ + TotalResults: 30, + TotalPages: 3, + Page: 1, + Metrics: metrics[0:10], + }), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, + fmt.Sprintf("order=asc&page=1&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), + ), + ), + ) + apiServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ + TotalResults: 30, + TotalPages: 3, + Page: 2, + Metrics: metrics[10:20], + }), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, + fmt.Sprintf("order=asc&page=2&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), + ), + ), + ) + apiServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.RespondWithJSONEncoded(http.StatusOK, &AggregatedMetricsResults{ + TotalResults: 30, + TotalPages: 3, + Page: 3, + Metrics: metrics[20:30], + }), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", aggregatedMetricsUrlPath, + fmt.Sprintf("order=asc&page=3&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*30*1E9)), + ), + ), + ) + }) + + It("Succeed to print all pages of the metrics to stdout with asc order", func() { + + args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, + "--start", now.Format(time.RFC3339), + "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*30*1E9)).Format(time.RFC3339), + "--asc", + } + + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session.Out).To(gbytes.Say(ui.ShowAggregatedMetricsHint, metricName, fakeAppName)) + metricsRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowAggregatedMetricsHint+"\n", metricName, fakeAppName))) + metricsTable := strings.Split(string(bytes.TrimRight(metricsRaw, "\n")), "\n") + Expect(len(metricsTable)).To(Equal(31)) + for i, row := range metricsTable { + colomns := strings.Split(row, "\t") + if i == 0 { + Expect(strings.Trim(colomns[0], " ")).To(Equal("Metrics Name")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("Value")) + Expect(strings.Trim(colomns[2], " ")).To(Equal("Timestamp")) + } else { + //use "29-(i-1)" to simulate the expected output in asc order + Expect(strings.Trim(colomns[0], " ")).To(Equal("memoryused")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("100MB")) + Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*30*1E9)).Format(time.RFC3339))) + } + } + Expect(session.ExitCode()).To(Equal(0)) + }) }) Context(" Print the output to a file", func() { @@ -2058,7 +2550,7 @@ var _ = Describe("App-AutoScaler Commands", func() { TotalResults: 10, TotalPages: 1, Page: 1, - Metrics: metrics[0:10], + Metrics: reversedMetrics[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), ), @@ -2066,7 +2558,7 @@ var _ = Describe("App-AutoScaler Commands", func() { }) - It("Succeed to print the metrics to stdout with asc order", func() { + It("Succeed to print the metrics to stdout with desc order", func() { args = []string{ts.Port(), "autoscaling-metrics", fakeAppName, metricName, "--output", outputFile} @@ -2091,7 +2583,7 @@ var _ = Describe("App-AutoScaler Commands", func() { } else { Expect(strings.Trim(colomns[0], " ")).To(Equal("memoryused")) Expect(strings.Trim(colomns[1], " ")).To(Equal("100MB")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*30*1E9)).Format(time.RFC3339))) + Expect(strings.Trim(colomns[2], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((29-(i-1))*30*1E9)).Format(time.RFC3339))) } } Expect(session.ExitCode()).To(Equal(0)) @@ -2179,33 +2671,23 @@ var _ = Describe("App-AutoScaler Commands", func() { Expect(session.ExitCode()).To(Equal(1)) }) - It("Failed when record number is not an integer", func() { - By("long") - args = []string{ts.Port(), "autoscaling-history", fakeAppName, "--number", "not-integer"} + It("Failed when --asc is wrong spelled", func() { + args = []string{ts.Port(), "autoscaling-history", fakeAppName, "--aaasc"} session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() - Expect(session).To(gbytes.Say(fmt.Sprintf(ui.InvalidRecordNumber, "not-integer"))) - Expect(session.ExitCode()).To(Equal(1)) - - By("short") - args = []string{ts.Port(), "autoscaling-history", fakeAppName, "-n", "0"} - session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() - - Expect(session).To(gbytes.Say(fmt.Sprintf(ui.InvalidRecordNumber, "0"))) + Expect(session).To(gbytes.Say("unknown flag")) Expect(session.ExitCode()).To(Equal(1)) }) - It("Failed when --desc is wrong spelled", func() { - args = []string{ts.Port(), "autoscaling-history", fakeAppName, "--dddesc"} - session, err := gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + It("Failed when --desc and --asc are used at the same time", func() { + args = []string{ts.Port(), "autoscaling-history", fakeAppName, "--asc", "--desc"} + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() - Expect(session).To(gbytes.Say("unknown flag")) + Expect(session).To(gbytes.Say(ui.DeprecatedDescWarning)) Expect(session.ExitCode()).To(Equal(1)) }) @@ -2433,236 +2915,48 @@ var _ = Describe("App-AutoScaler Commands", func() { ScalingType: 1, //scheduled Status: 1, //failed OldInstances: i + 1, - NewInstances: i + 2, - Reason: "fakeReason", - Message: "", - Error: "fakeError", - }) - } - - for i := 0; i < 30; i++ { - reversedHistories = append(reversedHistories, histories[len(histories)-1-i]) - } - - }) - Context("Query with default options ", func() { - - BeforeEach(func() { - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ - TotalResults: 20, - TotalPages: 2, - Page: 1, - Histories: histories[0:10], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ), - ) - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ - TotalResults: 20, - TotalPages: 2, - Page: 2, - Histories: histories[10:20], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ), - ) - }) - - It("Succeed to print first page of the histories to stdout with asc order", func() { - - args = []string{ts.Port(), "autoscaling-history", fakeAppName} - - session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() - - Expect(session.Out).To(gbytes.Say(ui.ShowHistoryHint, fakeAppName)) - historyRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowHistoryHint+"\n", fakeAppName))) - historyTable := strings.Split(string(bytes.TrimRight(historyRaw, "\n")), "\n") - Expect(len(historyTable)).To(Equal(11)) - for i, row := range historyTable { - colomns := strings.Split(row, "\t") - if i == 0 { - Expect(strings.Trim(colomns[0], " ")).To(Equal("Scaling Type")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("Status")) - Expect(strings.Trim(colomns[2], " ")).To(Equal("Instance Changes")) - Expect(strings.Trim(colomns[3], " ")).To(Equal("Time")) - Expect(strings.Trim(colomns[4], " ")).To(Equal("Action")) - Expect(strings.Trim(colomns[5], " ")).To(Equal("Error")) - - } else { - Expect(strings.Trim(colomns[0], " ")).To(Equal("dynamic")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) - Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*120*1E9)).Format(time.RFC3339))) - Expect(strings.Trim(colomns[4], " ")).To(Equal("fakeReason")) - Expect(strings.Trim(colomns[5], " ")).To(Equal("fakeError")) - } - } - Expect(session.ExitCode()).To(Equal(0)) - }) - - }) - - Context("Query multiple pages with asc order ", func() { - - BeforeEach(func() { - //simulate the asc response from api server - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ - TotalResults: 30, - TotalPages: 3, - Page: 1, - Histories: histories[0:10], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=asc&page=1&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), - ), - ), - ) - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ - TotalResults: 30, - TotalPages: 3, - Page: 2, - Histories: histories[10:20], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=asc&page=2&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), - ), - ), - ) - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ - TotalResults: 30, - TotalPages: 3, - Page: 3, - Histories: histories[20:30], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=asc&page=3&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), - ), - ), - ) - - }) - - It("Succeed to print all pages of the history to stdout with asc order", func() { - - args = []string{ts.Port(), "autoscaling-history", fakeAppName, - "--start", now.Format(time.RFC3339), - "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*120*1E9)).Format(time.RFC3339)} - - session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) - Expect(err).NotTo(HaveOccurred()) - session.Wait() - - Expect(session.Out).To(gbytes.Say(ui.ShowHistoryHint, fakeAppName)) - historyRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowHistoryHint+"\n", fakeAppName))) - historyTable := strings.Split(string(bytes.TrimRight(historyRaw, "\n")), "\n") - Expect(len(historyTable)).To(Equal(31)) - for i, row := range historyTable { - colomns := strings.Split(row, "\t") - if i == 0 { - Expect(strings.Trim(colomns[0], " ")).To(Equal("Scaling Type")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("Status")) - Expect(strings.Trim(colomns[2], " ")).To(Equal("Instance Changes")) - Expect(strings.Trim(colomns[3], " ")).To(Equal("Time")) - Expect(strings.Trim(colomns[4], " ")).To(Equal("Action")) - Expect(strings.Trim(colomns[5], " ")).To(Equal("Error")) - //header line - } else { - //use (i-1) to skip header - Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*120*1E9)).Format(time.RFC3339))) - Expect(strings.Trim(colomns[4], " ")).To(Equal("fakeReason")) - Expect(strings.Trim(colomns[5], " ")).To(Equal("fakeError")) - - if i < 11 { - Expect(strings.Trim(colomns[0], " ")).To(Equal("dynamic")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) - } else if i < 21 { - Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) - } else { - Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("failed")) - Expect(strings.Trim(colomns[2], " ")).To(Equal("")) - } - } - } - Expect(session.ExitCode()).To(Equal(0)) - }) + NewInstances: i + 2, + Reason: "fakeReason", + Message: "", + Error: "fakeError", + }) + } - }) + for i := 0; i < 30; i++ { + reversedHistories = append(reversedHistories, histories[len(histories)-1-i]) + } - Context("Query multiple pages with desc order ", func() { + }) + Context("Query with default options ", func() { BeforeEach(func() { - //simulate the desc response from api server apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ - TotalResults: 30, - TotalPages: 3, + TotalResults: 20, + TotalPages: 2, Page: 1, Histories: reversedHistories[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=desc&page=1&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), - ), ), ) apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ - TotalResults: 30, - TotalPages: 3, + TotalResults: 20, + TotalPages: 2, Page: 2, Histories: reversedHistories[10:20], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=desc&page=2&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), - ), - ), - ) - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ - TotalResults: 30, - TotalPages: 3, - Page: 3, - Histories: reversedHistories[20:30], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=desc&page=3&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), - ), ), ) - }) - It("Succeed to print all pages of the history to stdout with desc order", func() { + It("Succeed to print first page of the histories to stdout with desc order", func() { - args = []string{ts.Port(), "autoscaling-history", fakeAppName, - "--start", now.Format(time.RFC3339), - "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*120*1E9)).Format(time.RFC3339), - "--desc", - } + args = []string{ts.Port(), "autoscaling-history", fakeAppName} session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) @@ -2671,7 +2965,7 @@ var _ = Describe("App-AutoScaler Commands", func() { Expect(session.Out).To(gbytes.Say(ui.ShowHistoryHint, fakeAppName)) historyRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowHistoryHint+"\n", fakeAppName))) historyTable := strings.Split(string(bytes.TrimRight(historyRaw, "\n")), "\n") - Expect(len(historyTable)).To(Equal(31)) + Expect(len(historyTable)).To(Equal(12)) for i, row := range historyTable { colomns := strings.Split(row, "\t") if i == 0 { @@ -2681,24 +2975,14 @@ var _ = Describe("App-AutoScaler Commands", func() { Expect(strings.Trim(colomns[3], " ")).To(Equal("Time")) Expect(strings.Trim(colomns[4], " ")).To(Equal("Action")) Expect(strings.Trim(colomns[5], " ")).To(Equal("Error")) - } else { - //use "29-(i-1)" to simulate the expected output in desc order + + } else if i != len(historyTable)-1 { + Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("failed")) + Expect(strings.Trim(colomns[2], " ")).To(Equal("")) Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((29-(i-1))*120*1E9)).Format(time.RFC3339))) Expect(strings.Trim(colomns[4], " ")).To(Equal("fakeReason")) Expect(strings.Trim(colomns[5], " ")).To(Equal("fakeError")) - if i < 11 { - Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("failed")) - Expect(strings.Trim(colomns[2], " ")).To(Equal("")) - } else if i < 21 { - Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(29-(i-1)+1) + "->" + strconv.Itoa(29-(i-1)+2))) - } else { - Expect(strings.Trim(colomns[0], " ")).To(Equal("dynamic")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(29-(i-1)+1) + "->" + strconv.Itoa(29-(i-1)+2))) - } } } Expect(session.ExitCode()).To(Equal(0)) @@ -2706,21 +2990,23 @@ var _ = Describe("App-AutoScaler Commands", func() { }) - Context("Query multiple pages with specified record number ", func() { - - Context("not specify starttime and entime ", func() { + Context("Query multiple pages with desc order ", func() { + Context("specifiy --start and --end both ", func() { BeforeEach(func() { - //simulate the asc response from api server + //simulate the desc response from api server apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ TotalResults: 30, TotalPages: 3, Page: 1, - Histories: histories[0:10], + Histories: reversedHistories[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", urlpath, + fmt.Sprintf("order=desc&page=1&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), + ), ), ) apiServer.AppendHandlers( @@ -2729,9 +3015,12 @@ var _ = Describe("App-AutoScaler Commands", func() { TotalResults: 30, TotalPages: 3, Page: 2, - Histories: histories[10:20], + Histories: reversedHistories[10:20], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", urlpath, + fmt.Sprintf("order=desc&page=2&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), + ), ), ) apiServer.AppendHandlers( @@ -2740,18 +3029,22 @@ var _ = Describe("App-AutoScaler Commands", func() { TotalResults: 30, TotalPages: 3, Page: 3, - Histories: histories[20:30], + Histories: reversedHistories[20:30], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", urlpath, + fmt.Sprintf("order=desc&page=3&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), + ), ), ) }) - It("Succeed to print limited number of the history to stdout with asc order ", func() { + It("Succeed to print all pages of the history to stdout", func() { args = []string{ts.Port(), "autoscaling-history", fakeAppName, - "--number", "15"} + "--start", now.Format(time.RFC3339), + "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*120*1E9)).Format(time.RFC3339)} session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) @@ -2760,7 +3053,7 @@ var _ = Describe("App-AutoScaler Commands", func() { Expect(session.Out).To(gbytes.Say(ui.ShowHistoryHint, fakeAppName)) historyRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowHistoryHint+"\n", fakeAppName))) historyTable := strings.Split(string(bytes.TrimRight(historyRaw, "\n")), "\n") - Expect(len(historyTable)).To(Equal(16)) + Expect(len(historyTable)).To(Equal(31)) for i, row := range historyTable { colomns := strings.Split(row, "\t") if i == 0 { @@ -2773,96 +3066,46 @@ var _ = Describe("App-AutoScaler Commands", func() { //header line } else { //use (i-1) to skip header - Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*120*1E9)).Format(time.RFC3339))) + Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((29-(i-1))*120*1E9)).Format(time.RFC3339))) Expect(strings.Trim(colomns[4], " ")).To(Equal("fakeReason")) Expect(strings.Trim(colomns[5], " ")).To(Equal("fakeError")) if i < 11 { - Expect(strings.Trim(colomns[0], " ")).To(Equal("dynamic")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) - + Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("failed")) + Expect(strings.Trim(colomns[2], " ")).To(Equal("")) } else if i < 21 { Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) - + Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(29-(i-1)+1) + "->" + strconv.Itoa(29-(i-1)+2))) } else { - Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("failed")) - Expect(strings.Trim(colomns[2], " ")).To(Equal("")) - + Expect(strings.Trim(colomns[0], " ")).To(Equal("dynamic")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) + Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(29-(i-1)+1) + "->" + strconv.Itoa(29-(i-1)+2))) } } } Expect(session.ExitCode()).To(Equal(0)) }) - }) - - Context("specifiy endtime only ", func() { - - BeforeEach(func() { - //simulate the asc response from api server - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ - TotalResults: 30, - TotalPages: 3, - Page: 1, - Histories: histories[0:10], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=asc&page=1&end-time=%v", lowPrecisionNowInNano), - ), - ), - ) - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ - TotalResults: 30, - TotalPages: 3, - Page: 2, - Histories: histories[10:20], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=asc&page=2&end-time=%v", lowPrecisionNowInNano), - ), - ), - ) - apiServer.AppendHandlers( - ghttp.CombineHandlers( - ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ - TotalResults: 30, - TotalPages: 3, - Page: 3, - Histories: histories[20:30], - }), - ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), - ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=asc&page=3&end-time=%v", lowPrecisionNowInNano), - ), - ), - ) - - }) - - It("Succeed to print limited number of the history to stdout with asc order ", func() { + It("Succeed to print all pages of the history with specified desc to stdout", func() { args = []string{ts.Port(), "autoscaling-history", fakeAppName, - "--number", "15", - "--end", time.Unix(0, lowPrecisionNowInNano).Format(time.RFC3339)} + "--start", now.Format(time.RFC3339), + "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*120*1E9)).Format(time.RFC3339), + "--desc", + } session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) session.Wait() Expect(session.Out).To(gbytes.Say(ui.ShowHistoryHint, fakeAppName)) + Expect(session.Out).To(gbytes.Say(ui.DeprecatedDescWarning)) historyRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowHistoryHint+"\n", fakeAppName))) + historyRaw = bytes.TrimSuffix(historyRaw, []byte(ui.DeprecatedDescWarning+"\n")) historyTable := strings.Split(string(bytes.TrimRight(historyRaw, "\n")), "\n") - Expect(len(historyTable)).To(Equal(16)) + Expect(len(historyTable)).To(Equal(31)) for i, row := range historyTable { colomns := strings.Split(row, "\t") if i == 0 { @@ -2875,47 +3118,43 @@ var _ = Describe("App-AutoScaler Commands", func() { //header line } else { //use (i-1) to skip header - Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*120*1E9)).Format(time.RFC3339))) + Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((29-(i-1))*120*1E9)).Format(time.RFC3339))) Expect(strings.Trim(colomns[4], " ")).To(Equal("fakeReason")) Expect(strings.Trim(colomns[5], " ")).To(Equal("fakeError")) if i < 11 { - Expect(strings.Trim(colomns[0], " ")).To(Equal("dynamic")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) - + Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("failed")) + Expect(strings.Trim(colomns[2], " ")).To(Equal("")) } else if i < 21 { Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) - + Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(29-(i-1)+1) + "->" + strconv.Itoa(29-(i-1)+2))) } else { - Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("failed")) - Expect(strings.Trim(colomns[2], " ")).To(Equal("")) - + Expect(strings.Trim(colomns[0], " ")).To(Equal("dynamic")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) + Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(29-(i-1)+1) + "->" + strconv.Itoa(29-(i-1)+2))) } } } Expect(session.ExitCode()).To(Equal(0)) }) - }) - Context("specifiy start time and endtime ", func() { + Context("specify --end only ", func() { BeforeEach(func() { - //simulate the asc response from api server + //simulate the desc response from api server apiServer.AppendHandlers( ghttp.CombineHandlers( ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ TotalResults: 30, TotalPages: 3, Page: 1, - Histories: histories[0:10], + Histories: reversedHistories[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=asc&page=1&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), + fmt.Sprintf("order=desc&page=1&end-time=%v", lowPrecisionNowInNano+int64(29*120*1E9)), ), ), ) @@ -2925,11 +3164,11 @@ var _ = Describe("App-AutoScaler Commands", func() { TotalResults: 30, TotalPages: 3, Page: 2, - Histories: histories[10:20], + Histories: reversedHistories[10:20], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=asc&page=2&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), + fmt.Sprintf("order=desc&page=2&end-time=%v", lowPrecisionNowInNano+int64(29*120*1E9)), ), ), ) @@ -2939,22 +3178,20 @@ var _ = Describe("App-AutoScaler Commands", func() { TotalResults: 30, TotalPages: 3, Page: 3, - Histories: histories[20:30], + Histories: reversedHistories[20:30], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), ghttp.VerifyRequest("GET", urlpath, - fmt.Sprintf("order=asc&page=3&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), + fmt.Sprintf("order=desc&page=3&end-time=%v", lowPrecisionNowInNano+int64(29*120*1E9)), ), ), ) }) - It("Succeed to ignore the record number limit and print all pages of the history in specified duration to stdout with asc order ", func() { + It("Succeed to print all pages of the history to stdout", func() { args = []string{ts.Port(), "autoscaling-history", fakeAppName, - "--number", "15", - "--start", now.Format(time.RFC3339), "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*120*1E9)).Format(time.RFC3339)} session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) @@ -2977,25 +3214,22 @@ var _ = Describe("App-AutoScaler Commands", func() { //header line } else { //use (i-1) to skip header - Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*120*1E9)).Format(time.RFC3339))) + Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((29-(i-1))*120*1E9)).Format(time.RFC3339))) Expect(strings.Trim(colomns[4], " ")).To(Equal("fakeReason")) Expect(strings.Trim(colomns[5], " ")).To(Equal("fakeError")) if i < 11 { - Expect(strings.Trim(colomns[0], " ")).To(Equal("dynamic")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) - + Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("failed")) + Expect(strings.Trim(colomns[2], " ")).To(Equal("")) } else if i < 21 { Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) - + Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(29-(i-1)+1) + "->" + strconv.Itoa(29-(i-1)+2))) } else { - Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("failed")) - Expect(strings.Trim(colomns[2], " ")).To(Equal("")) - + Expect(strings.Trim(colomns[0], " ")).To(Equal("dynamic")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) + Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(29-(i-1)+1) + "->" + strconv.Itoa(29-(i-1)+2))) } } } @@ -3005,6 +3239,105 @@ var _ = Describe("App-AutoScaler Commands", func() { }) + Context("Query multiple pages with asc order ", func() { + + BeforeEach(func() { + //simulate the asc response from api server + apiServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ + TotalResults: 30, + TotalPages: 3, + Page: 1, + Histories: histories[0:10], + }), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", urlpath, + fmt.Sprintf("order=asc&page=1&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), + ), + ), + ) + apiServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ + TotalResults: 30, + TotalPages: 3, + Page: 2, + Histories: histories[10:20], + }), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", urlpath, + fmt.Sprintf("order=asc&page=2&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), + ), + ), + ) + apiServer.AppendHandlers( + ghttp.CombineHandlers( + ghttp.RespondWithJSONEncoded(http.StatusOK, &HistoryResults{ + TotalResults: 30, + TotalPages: 3, + Page: 3, + Histories: histories[20:30], + }), + ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), + ghttp.VerifyRequest("GET", urlpath, + fmt.Sprintf("order=asc&page=3&start-time=%v&end-time=%v", lowPrecisionNowInNano, lowPrecisionNowInNano+int64(29*120*1E9)), + ), + ), + ) + + }) + + It("Succeed to print all pages of the history to stdout with asc order", func() { + + args = []string{ts.Port(), "autoscaling-history", fakeAppName, + "--start", now.Format(time.RFC3339), + "--end", time.Unix(0, lowPrecisionNowInNano+int64(29*120*1E9)).Format(time.RFC3339), + "--asc", + } + + session, err = gexec.Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + session.Wait() + + Expect(session.Out).To(gbytes.Say(ui.ShowHistoryHint, fakeAppName)) + historyRaw := bytes.TrimPrefix(session.Out.Contents(), []byte(fmt.Sprintf(ui.ShowHistoryHint+"\n", fakeAppName))) + historyTable := strings.Split(string(bytes.TrimRight(historyRaw, "\n")), "\n") + Expect(len(historyTable)).To(Equal(31)) + for i, row := range historyTable { + colomns := strings.Split(row, "\t") + if i == 0 { + Expect(strings.Trim(colomns[0], " ")).To(Equal("Scaling Type")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("Status")) + Expect(strings.Trim(colomns[2], " ")).To(Equal("Instance Changes")) + Expect(strings.Trim(colomns[3], " ")).To(Equal("Time")) + Expect(strings.Trim(colomns[4], " ")).To(Equal("Action")) + Expect(strings.Trim(colomns[5], " ")).To(Equal("Error")) + } else { + //use "29-(i-1)" to simulate the expected output in asc order + Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*120*1E9)).Format(time.RFC3339))) + Expect(strings.Trim(colomns[4], " ")).To(Equal("fakeReason")) + Expect(strings.Trim(colomns[5], " ")).To(Equal("fakeError")) + if i < 11 { + Expect(strings.Trim(colomns[0], " ")).To(Equal("dynamic")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) + Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) + } else if i < 21 { + Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) + Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) + } else { + Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("failed")) + Expect(strings.Trim(colomns[2], " ")).To(Equal("")) + } + } + } + Expect(session.ExitCode()).To(Equal(0)) + }) + + }) + Context(" Print the output to a file", func() { BeforeEach(func() { @@ -3014,7 +3347,7 @@ var _ = Describe("App-AutoScaler Commands", func() { TotalResults: 10, TotalPages: 1, Page: 1, - Histories: histories[0:10], + Histories: reversedHistories[0:10], }), ghttp.VerifyHeaderKV("Authorization", fakeAccessToken), ), @@ -3022,7 +3355,7 @@ var _ = Describe("App-AutoScaler Commands", func() { }) - It("Succeed to print the history to a file with asc order", func() { + It("Succeed to print the history to a file with desc order", func() { args = []string{ts.Port(), "autoscaling-history", fakeAppName, "--output", outputFile} @@ -3048,10 +3381,10 @@ var _ = Describe("App-AutoScaler Commands", func() { Expect(strings.Trim(colomns[4], " ")).To(Equal("Action")) Expect(strings.Trim(colomns[5], " ")).To(Equal("Error")) } else { - Expect(strings.Trim(colomns[0], " ")).To(Equal("dynamic")) - Expect(strings.Trim(colomns[1], " ")).To(Equal("succeeded")) - Expect(strings.Trim(colomns[2], " ")).To(Equal(strconv.Itoa(i-1+1) + "->" + strconv.Itoa(i-1+2))) - Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((i-1)*120*1E9)).Format(time.RFC3339))) + Expect(strings.Trim(colomns[0], " ")).To(Equal("scheduled")) + Expect(strings.Trim(colomns[1], " ")).To(Equal("failed")) + Expect(strings.Trim(colomns[2], " ")).To(Equal("")) + Expect(strings.Trim(colomns[3], " ")).To(Equal(time.Unix(0, now.UnixNano()+int64((29-(i-1))*120*1E9)).Format(time.RFC3339))) Expect(strings.Trim(colomns[4], " ")).To(Equal("fakeReason")) Expect(strings.Trim(colomns[5], " ")).To(Equal("fakeError")) } diff --git a/src/cli/models/models.go b/src/cli/models/models.go index 732872a..baf23da 100644 --- a/src/cli/models/models.go +++ b/src/cli/models/models.go @@ -79,3 +79,14 @@ type HistoryResults struct { Page uint16 `json:"page"` Histories []*AppScalingHistory `json:"resources"` } + +type Credential struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type CredentialResponse struct { + AppId string `json:"app_id"` + *Credential + Url string `json:"url"` +} diff --git a/src/cli/ui/message.go b/src/cli/ui/message.go index 2cfc805..726dba1 100644 --- a/src/cli/ui/message.go +++ b/src/cli/ui/message.go @@ -24,18 +24,30 @@ const ( AttachPolicyHint = "Attaching policy for app %s..." DetachPolicyHint = "Detaching policy for app %s..." + CreateCredentialHint = "Creating custom metric credential for app %s..." + DeleteCredentialHint = "Deleting custom metric credential for app %s..." + ShowAggregatedMetricsHint = "Retrieving aggregated %s metrics for app %s..." ShowHistoryHint = "Retrieving scaling event history for app %s..." SavePolicyHint = "Saving policy for app %s to %s... " + SaveCredentialHint = "Saving new created credential for app %s to %s..." SaveAggregatedMetricHint = "Saving aggregated metrics for app %s to %s... " SaveHistoryHint = "Saving scaling event history for app %s to %s... " - InvalidRecordNumber = "Invalid record number: %s. A positive integer is expected." UnrecognizedTimeFormat = "Unrecognized date time format: %s. \nSupported formats are yyyy-MM-ddTHH:mm:ss+/-hhmm, yyyy-MM-ddTHH:mm:ssZ with an input later than 1970-01-01T00:00:00Z." - UnrecognizedMetricName = "Unrecognized metric name: %s. \nSupported value: memoryused, memoryutil, responsetime, throughput, cpu." + UnrecognizedMetricName = "Unrecognized metric name: %s. \nSupported value: memoryused, memoryutil, responsetime, throughput, cpu or custom metric names built with letters, numbers or underlines \"_\"." InvalidTimeRange = "Invalid time range. The start time %s is greater than the end time %s." AggregatedMetricsNotFound = "No aggregated %s metrics were found for app %s." HistoryNotFound = "No event history were found for app %s." + + InvalidCredentialUsername = "Both USERNAME and PASSWORD need to be provided for user-defined credential." + InvalidCredentialPassword = "Both USERNAME and PASSWORD need to be provided for user-defined credential." + InvalidCredential = "Invalid credential definition: %v." + + MoreRecordsWarning = "TIP: More records available. Please re-run the command with --start or --end option to fetch more." + DeprecatedDescWarning = "TIP: The default order is set to descending now. Please remove the DEPRECATED flag '--desc'." + CreateCredentialWarning = "TIP: A new credential generated. Please update the credential setting of your application, and use 'cf restart %s' to ensure your env variable changes take effect." + DeleteCredentialWarning = "TIP: The credential removed. Please remove the credential setting from your application, and use 'cf restart %s' to ensure your env variable changes take effect." ) diff --git a/src/cli/ui/ui.go b/src/cli/ui/ui.go index 16cfa52..feb9946 100644 --- a/src/cli/ui/ui.go +++ b/src/cli/ui/ui.go @@ -19,3 +19,8 @@ func SayFailed() { func SayMessage(message string, args ...interface{}) { fmt.Printf(message+"\n", args...) } + +func SayWarningMessage(message string, args ...interface{}) { + c := color.New(color.FgYellow).Add(color.Bold) + c.Printf(message+"\n", args...) +}