From 71847a3c68f39d6907f8e1c65f0ee6422b133439 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 16 Aug 2018 16:56:28 -0700 Subject: [PATCH 01/60] add new resource example that has async operations - this example will be used to perform tests against to when adding async operations support in the open api terraform provider - refactored the code so some code can be shared across the two resources exposed cdns and lbs --- examples/swaggercodegen/api/api/auth.go | 17 ++ examples/swaggercodegen/api/api/cdn.go | 86 ++------- .../api/api/content_delivery_network.go | 28 +++ examples/swaggercodegen/api/api/default.go | 13 -- .../swaggercodegen/api/api/default_api.go | 35 ++++ examples/swaggercodegen/api/api/http_utils.go | 41 +++++ examples/swaggercodegen/api/api/lb_api.go | 35 ++++ examples/swaggercodegen/api/api/lbv1.go | 20 +++ examples/swaggercodegen/api/api/routers.go | 70 +++++--- .../swaggercodegen/api/resources/swagger.yaml | 167 ++++++++++++++++-- 10 files changed, 392 insertions(+), 120 deletions(-) create mode 100644 examples/swaggercodegen/api/api/auth.go create mode 100644 examples/swaggercodegen/api/api/content_delivery_network.go delete mode 100644 examples/swaggercodegen/api/api/default.go create mode 100644 examples/swaggercodegen/api/api/default_api.go create mode 100644 examples/swaggercodegen/api/api/http_utils.go create mode 100644 examples/swaggercodegen/api/api/lb_api.go create mode 100644 examples/swaggercodegen/api/api/lbv1.go diff --git a/examples/swaggercodegen/api/api/auth.go b/examples/swaggercodegen/api/api/auth.go new file mode 100644 index 000000000..a3ddd118e --- /dev/null +++ b/examples/swaggercodegen/api/api/auth.go @@ -0,0 +1,17 @@ +package api + +import ( + "net/http" + "fmt" + "errors" +) + +func AuthenticateRequest(r *http.Request, w http.ResponseWriter) error { + apiKey := r.Header.Get("Authorization") + if apiKey == "" || apiKey != "apiKeyValue" { + msg := fmt.Sprintf("unauthorized user") + sendErrorResponse(http.StatusUnauthorized, msg, w) + return errors.New(msg) + } + return nil +} \ No newline at end of file diff --git a/examples/swaggercodegen/api/api/cdn.go b/examples/swaggercodegen/api/api/cdn.go index 4e7f1513b..eda261f78 100644 --- a/examples/swaggercodegen/api/api/cdn.go +++ b/examples/swaggercodegen/api/api/cdn.go @@ -1,34 +1,24 @@ package api import ( - "encoding/json" "fmt" - "io/ioutil" "net/http" "strings" - "errors" - "github.com/pborman/uuid" "log" ) -type ContentDeliveryNetwork struct { - Id string `json:"id"` - Label string `json:"label"` - Ips []string `json:"ips"` - Hostnames []string `json:"hostnames"` -} - var db = map[string]*ContentDeliveryNetwork{} func ContentDeliveryNetworkCreateV1(w http.ResponseWriter, r *http.Request) { - if authenticateRequest(r, w) != nil { + if AuthenticateRequest(r, w) != nil { return } xRequestID := r.Header.Get("X-Request-ID") log.Printf("Header [X-Request-ID]: %s", xRequestID) - cdn, err := readRequest(r) + cdn := &ContentDeliveryNetwork{} + err := readRequest(r, cdn) if err != nil { sendErrorResponse(http.StatusBadRequest, err.Error(), w) return @@ -39,46 +29,47 @@ func ContentDeliveryNetworkCreateV1(w http.ResponseWriter, r *http.Request) { } func ContentDeliveryNetworkGetV1(w http.ResponseWriter, r *http.Request) { - if authenticateRequest(r, w) != nil { + if AuthenticateRequest(r, w) != nil { return } - a, err := retrieveCdn(r) + cdn, err := retrieveCdn(r) if err != nil { sendErrorResponse(http.StatusNotFound, err.Error(), w) return } - sendResponse(http.StatusOK, w, a) + sendResponse(http.StatusOK, w, cdn) } func ContentDeliveryNetworkUpdateV1(w http.ResponseWriter, r *http.Request) { - if authenticateRequest(r, w) != nil { + if AuthenticateRequest(r, w) != nil { return } - a, err := retrieveCdn(r) + cdn, err := retrieveCdn(r) if err != nil { sendErrorResponse(http.StatusNotFound, err.Error(), w) return } - cdn, err := readRequest(r) + newCDN := &ContentDeliveryNetwork{} + err = readRequest(r, newCDN) if err != nil { sendErrorResponse(http.StatusBadRequest, err.Error(), w) return } - cdn.Id = a.Id - db[cdn.Id] = cdn - sendResponse(http.StatusOK, w, cdn) + cdn.Id = cdn.Id + db[cdn.Id] = newCDN + sendResponse(http.StatusOK, w, newCDN) } func ContentDeliveryNetworkDeleteV1(w http.ResponseWriter, r *http.Request) { - if authenticateRequest(r, w) != nil { + if AuthenticateRequest(r, w) != nil { return } - a, err := retrieveCdn(r) + cdn, err := retrieveCdn(r) if err != nil { sendErrorResponse(http.StatusNotFound, err.Error(), w) return } - delete(db, a.Id) + delete(db, cdn.Id) updateResponseHeaders(http.StatusNoContent, w) } @@ -92,47 +83,4 @@ func retrieveCdn(r *http.Request) (*ContentDeliveryNetwork, error) { return nil, fmt.Errorf("cdn id '%s' not found", id) } return cdn, nil -} - -func readRequest(r *http.Request) (*ContentDeliveryNetwork, error) { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - return nil, fmt.Errorf("failed to read request body - %s", err) - } - cdn := &ContentDeliveryNetwork{} - if err := json.Unmarshal(body, cdn); err != nil { - return nil, fmt.Errorf("payload does not match cdn spec - %s", err) - } - return cdn, nil -} - -func authenticateRequest(r *http.Request, w http.ResponseWriter) error { - apiKey := r.Header.Get("Authorization") - if apiKey == "" || apiKey != "apiKeyValue" { - msg := fmt.Sprintf("unauthorized user") - sendErrorResponse(http.StatusUnauthorized, msg, w) - return errors.New(msg) - } - return nil -} - -func sendResponse(httpResponseStatusCode int, w http.ResponseWriter, cdn *ContentDeliveryNetwork) { - var resBody []byte - var err error - if resBody, err = json.Marshal(cdn); err != nil { - msg := fmt.Sprintf("internal server error - %s", err) - sendErrorResponse(http.StatusInternalServerError, msg, w) - } - w.WriteHeader(httpResponseStatusCode) - w.Write(resBody) -} - -func sendErrorResponse(httpStatusCode int, message string, w http.ResponseWriter) { - updateResponseHeaders(httpStatusCode, w) - w.Write([]byte(fmt.Sprintf(`{"code":"%d", "message": "%s"}`, httpStatusCode, message))) -} - -func updateResponseHeaders(httpStatusCode int, w http.ResponseWriter) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(httpStatusCode) -} +} \ No newline at end of file diff --git a/examples/swaggercodegen/api/api/content_delivery_network.go b/examples/swaggercodegen/api/api/content_delivery_network.go new file mode 100644 index 000000000..97a46137b --- /dev/null +++ b/examples/swaggercodegen/api/api/content_delivery_network.go @@ -0,0 +1,28 @@ +/* + * Service Provider (swaggercodegen) + * + * This service provider allows the creation of fake 'cdns' resources + * + * API version: 1.0.0 + * Contact: apiteam@serviceprovider.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ + +package api + +type ContentDeliveryNetwork struct { + + Id string `json:"id,omitempty"` + + Label string `json:"label"` + + Ips []string `json:"ips"` + + Hostnames []string `json:"hostnames"` + + ExampleInt int32 `json:"example_int,omitempty"` + + ExampleNumber float32 `json:"example_number,omitempty"` + + ExampleBoolean bool `json:"example_boolean,omitempty"` +} diff --git a/examples/swaggercodegen/api/api/default.go b/examples/swaggercodegen/api/api/default.go deleted file mode 100644 index 02a14b0c7..000000000 --- a/examples/swaggercodegen/api/api/default.go +++ /dev/null @@ -1,13 +0,0 @@ -package api - -import ( - "net/http" -) - -type Default struct { -} - -func GetVersion(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) -} diff --git a/examples/swaggercodegen/api/api/default_api.go b/examples/swaggercodegen/api/api/default_api.go new file mode 100644 index 000000000..9e88b861f --- /dev/null +++ b/examples/swaggercodegen/api/api/default_api.go @@ -0,0 +1,35 @@ +/* + * Service Provider (swaggercodegen) + * + * This service provider allows the creation of fake 'cdns' resources + * + * API version: 1.0.0 + * Contact: apiteam@serviceprovider.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ + +package api + +import ( + "net/http" + "io/ioutil" + "os" + "fmt" +) + +func ApiDiscovery(w http.ResponseWriter, r *http.Request) { + pwd, _ := os.Getwd() + b, err := ioutil.ReadFile(pwd + "/resources/swagger.yaml") + if err != nil { + fmt.Print(err) + return + } + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + w.Write(b) +} + +func GetVersion(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) +} diff --git a/examples/swaggercodegen/api/api/http_utils.go b/examples/swaggercodegen/api/api/http_utils.go new file mode 100644 index 000000000..76b55bbcf --- /dev/null +++ b/examples/swaggercodegen/api/api/http_utils.go @@ -0,0 +1,41 @@ +package api + +import ( + "net/http" + "encoding/json" + "fmt" + "io/ioutil" +) + +func readRequest(r *http.Request, in interface{}) error { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return fmt.Errorf("failed to read request body - %s", err) + } + if err := json.Unmarshal(body, in); err != nil { + return fmt.Errorf("payload does not match cdn spec - %s", err) + } + return nil +} + +func sendResponse(httpResponseStatusCode int, w http.ResponseWriter, out interface{}) { + var resBody []byte + var err error + if resBody, err = json.Marshal(out); err != nil { + msg := fmt.Sprintf("internal server error - %s", err) + sendErrorResponse(http.StatusInternalServerError, msg, w) + } + w.WriteHeader(httpResponseStatusCode) + w.Write(resBody) +} + +func sendErrorResponse(httpStatusCode int, message string, w http.ResponseWriter) { + updateResponseHeaders(httpStatusCode, w) + w.Write([]byte(fmt.Sprintf(`{"code":"%d", "message": "%s"}`, httpStatusCode, message))) +} + +func updateResponseHeaders(httpStatusCode int, w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(httpStatusCode) +} + diff --git a/examples/swaggercodegen/api/api/lb_api.go b/examples/swaggercodegen/api/api/lb_api.go new file mode 100644 index 000000000..9678ed41a --- /dev/null +++ b/examples/swaggercodegen/api/api/lb_api.go @@ -0,0 +1,35 @@ +/* + * Dummy Service Provider generated using 'swaggercodegen' that has two resources 'cdns' and 'lbs' which are terraform compliant + * + * This service provider allows the creation of fake 'cdns' and 'lbs' resources + * + * API version: 1.0.0 + * Contact: apiteam@serviceprovider.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ + +package api + +import ( + "net/http" +) + +func LBCreateV1(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) +} + +func LBDeleteV1(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) +} + +func LBGetV1(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) +} + +func LBUpdateV1(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) +} diff --git a/examples/swaggercodegen/api/api/lbv1.go b/examples/swaggercodegen/api/api/lbv1.go new file mode 100644 index 000000000..e888be9ef --- /dev/null +++ b/examples/swaggercodegen/api/api/lbv1.go @@ -0,0 +1,20 @@ +/* + * Dummy Service Provider generated using 'swaggercodegen' that has two resources 'cdns' and 'lbs' which are terraform compliant + * + * This service provider allows the creation of fake 'cdns' and 'lbs' resources + * + * API version: 1.0.0 + * Contact: apiteam@serviceprovider.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ + +package api + +type Lbv1 struct { + + Id string `json:"id,omitempty"` + + Name string `json:"name"` + + Backends []string `json:"backends"` +} diff --git a/examples/swaggercodegen/api/api/routers.go b/examples/swaggercodegen/api/api/routers.go index f79b6c19e..6a8660016 100644 --- a/examples/swaggercodegen/api/api/routers.go +++ b/examples/swaggercodegen/api/api/routers.go @@ -2,11 +2,10 @@ package api import ( "fmt" - "io/ioutil" "net/http" - "os" "github.com/gorilla/mux" + "strings" ) type Route struct { @@ -35,58 +34,87 @@ func NewRouter() *mux.Router { return router } -func Discovery(w http.ResponseWriter, r *http.Request) { - pwd, _ := os.Getwd() - b, err := ioutil.ReadFile(pwd + "/resources/swagger.yaml") - if err != nil { - fmt.Print(err) - return - } - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - w.Write(b) + + +func Index(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello World!") } var routes = Routes{ Route{ - "ApiDiscovery", + "Index", "GET", - "/swagger.yaml", - Discovery, + "/", + Index, }, Route{ "ContentDeliveryNetworkCreateV1", - "POST", + strings.ToUpper("Post"), "/v1/cdns", ContentDeliveryNetworkCreateV1, }, Route{ "ContentDeliveryNetworkDeleteV1", - "DELETE", + strings.ToUpper("Delete"), "/v1/cdns/{id}", ContentDeliveryNetworkDeleteV1, }, Route{ "ContentDeliveryNetworkGetV1", - "GET", + strings.ToUpper("Get"), "/v1/cdns/{id}", ContentDeliveryNetworkGetV1, }, Route{ "ContentDeliveryNetworkUpdateV1", - "PUT", + strings.ToUpper("Put"), "/v1/cdns/{id}", ContentDeliveryNetworkUpdateV1, }, + Route{ + "LBCreateV1", + strings.ToUpper("Post"), + "/v1/lbs", + LBCreateV1, + }, + + Route{ + "LBDeleteV1", + strings.ToUpper("Delete"), + "/v1/lbs/{id}", + LBDeleteV1, + }, + + Route{ + "LBGetV1", + strings.ToUpper("Get"), + "/v1/lbs/{id}", + LBGetV1, + }, + + Route{ + "LBUpdateV1", + strings.ToUpper("Put"), + "/v1/lbs/{id}", + LBUpdateV1, + }, + + Route{ + "ApiDiscovery", + strings.ToUpper("Get"), + "/swagger.yaml", + ApiDiscovery, + }, + Route{ "GetVersion", - "GET", + strings.ToUpper("Get"), "/version", GetVersion, }, -} +} \ No newline at end of file diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 4651b75e1..33350ae46 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -1,19 +1,24 @@ swagger: "2.0" info: - description: "This service provider allows the creation of fake 'cdns' resources" + description: "This service provider allows the creation of fake 'cdns' and 'lbs' resources" version: "1.0.0" - title: "Service Provider (swaggercodegen)" + title: "Dummy Service Provider generated using 'swaggercodegen' that has two resources 'cdns' and 'lbs' which are terraform compliant" contact: email: "apiteam@serviceprovider.io" #host: "localhost:8443" If host is not specified, it is assumed to be the same host where the API documentation is being served. -basePath: "/" +#basePath: "" tags: - name: "cdn" description: "Operations about cdns" externalDocs: description: "Find out more about cdn api" - url: "https://github.com/dikhan/terraform-provider-api/service_provider_example" + url: "https://github.com/dikhan/terraform-provider-openapi/tree/master/examples/swaggercodegen" +- name: "lb" + description: "Operations about lbs" + externalDocs: + description: "Find out more about lb api" + url: "https://github.com/dikhan/terraform-provider-openapi/tree/master/examples/swaggercodegen" schemes: - "http" - "https" @@ -41,6 +46,11 @@ paths: responses: 200: description: "successful operation" + + ###################### + #### CDN Resource #### + ###################### + /v1/cdns: post: tags: @@ -53,7 +63,7 @@ paths: description: "Created CDN" required: true schema: - $ref: "#/definitions/ContentDeliveryNetwork" + $ref: "#/definitions/ContentDeliveryNetworkV1" - in: "header" x-terraform-header: x_request_id name: "X-Request-ID" @@ -63,11 +73,11 @@ paths: 201: description: "successful operation" schema: - $ref: "#/definitions/ContentDeliveryNetwork" + $ref: "#/definitions/ContentDeliveryNetworkV1" default: description: "generic error response" schema: - $ref: "#/definitions/error" + $ref: "#/definitions/Error" #security: For the sake of the example, this POST operation will use the global security schemes # - apikey_auth: [] /v1/cdns/{id}: @@ -87,7 +97,7 @@ paths: 200: description: "successful operation" schema: - $ref: "#/definitions/ContentDeliveryNetwork" + $ref: "#/definitions/ContentDeliveryNetworkV1" 400: description: "Invalid cdn id supplied" 404: @@ -110,12 +120,12 @@ paths: description: "Updated cdn object" required: true schema: - $ref: "#/definitions/ContentDeliveryNetwork" + $ref: "#/definitions/ContentDeliveryNetworkV1" responses: 200: description: "successful operation" schema: - $ref: "#/definitions/ContentDeliveryNetwork" + $ref: "#/definitions/ContentDeliveryNetworkV1" 400: description: "Invalid cdn id supplied" 404: @@ -137,12 +147,105 @@ paths: 204: description: "successful operation, no content is returned" 400: - description: "Invalid cdn id supplied" + $ref: "#/responses/Unauthorized" 404: - description: "cdn not found" + $ref: "#/responses/NotFound" security: - apikey_auth: [] + ###################### + ##### LB Resource #### + ###################### + + /v1/lbs: + post: + tags: + - "lb" + summary: "Create lb v1" + operationId: "LBCreateV1" + parameters: + - in: "body" + name: "body" + description: "LB v1 payload object to be posted as part of the POST request" + required: true + schema: + $ref: "#/definitions/LBV1" + responses: + 202: + description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" + schema: + $ref: "#/definitions/LBV1" + default: + description: "generic error response" + schema: + $ref: "#/definitions/Error" + /v1/lbs/{id}: + get: + tags: + - "lb" + summary: "Get lb v1 by id" + description: "" + operationId: "LBGetV1" + parameters: + - name: "id" + in: "path" + description: "The lb v1 id that needs to be fetched." + required: true + type: "string" + responses: + 200: + description: "successful operation" + schema: + $ref: "#/definitions/LBV1" + 400: + description: "Invalid lb id supplied" + 404: + description: "LB not found" + put: + tags: + - "lb" + summary: "Updated cdn" + operationId: "LBUpdateV1" + parameters: + - name: "id" + in: "path" + description: "lb v1 that needs to be updated" + required: true + type: "string" + - in: "body" + name: "body" + description: "Updated cdn object" + required: true + schema: + $ref: "#/definitions/LBV1" + responses: + 202: + description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" + schema: + $ref: "#/definitions/LBV1" + 400: + description: "Invalid lb id supplied" + 404: + description: "LB v1 not found" + delete: + tags: + - "lb" + summary: "Delete lb v1" + operationId: "LBDeleteV1" + parameters: + - name: "id" + in: "path" + description: "The lb v1 that needs to be deleted" + required: true + type: "string" + responses: + 202: + description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" + 400: + $ref: "#/responses/Unauthorized" + 404: + $ref: "#/responses/NotFound" + securityDefinitions: apikey_auth: type: "apiKey" @@ -150,7 +253,7 @@ securityDefinitions: in: "header" definitions: - ContentDeliveryNetwork: + ContentDeliveryNetworkV1: type: "object" required: - label @@ -179,14 +282,44 @@ definitions: type: number example_boolean: type: boolean - error: + + LBV1: + type: "object" + required: + - name + - backends + properties: + id: + type: "string" + readOnly: true # This property will not be considered when creating a new resource, however, it is expected to + # to be returned from the api, and will be saved as computed value in the terraform state file + name: + type: "string" + backends: + type: "array" + x-terraform-force-new: true # when this value changes terraform will force the creation of a new resource + items: + type: "string" + + # Schema for error response body + Error: type: object required: - code - message properties: code: - type: integer - format: int64 + type: string message: - type: string \ No newline at end of file + type: string + +# Descriptions of common responses +responses: + NotFound: + description: The specified resource was not found + schema: + $ref: "#/definitions/Error" + Unauthorized: + description: Unauthorized + schema: + $ref: "#/definitions/Error" \ No newline at end of file From ac2fb5e7427113e9a8bd240e1ab197f75a4d52ec Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 30 Aug 2018 11:21:51 -0700 Subject: [PATCH 02/60] update swagger example including extensions to support async operations --- .../swaggercodegen/api/resources/swagger.yaml | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index a0c1cf228..fb246258a 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -165,8 +165,16 @@ paths: required: true schema: $ref: "#/definitions/LBV1" + x-terraform-resource-timeout: "30s" # The amount of time to wait before timeout (applicable to both sync and async operations) responses: - 202: + 202: # Accepted + x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' + x-terraform-resource-poll-delay: "30s" # [type (string)] - Wait this time before quering the resource state + x-terraform-resource-poll-min-timeout: "10s" # [type (string)] - Smallest time to wait before refreshes + x-terraform-resource-poll-poll-interval: "60s" # [type (string)] - Override MinTimeout/backoff and only poll this often + x-terraform-resource-poll-not-found-checks: 2 # [type (int)] - Number of times to allow not found + x-terraform-resource-poll-target-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed + x-terraform-resource-poll-pending-statuses: "deploy_pending, deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" schema: $ref: "#/definitions/LBV1" @@ -296,6 +304,19 @@ definitions: x-terraform-force-new: true # when this value changes terraform will force the creation of a new resource items: type: "string" + status: + x-terraform-field-status: true # identifies the field that should be used as status for async operations. This is handy when the field name is not status but some other name the service provider might have chosen and enables the provider to identify the field as the status field that will be used to track progress for the async operations + description: lb resource status + type: string + enum: + - deploy_pending + - deploy_in_progress + - deploy_failed + - deployed + - delete_pending + - delete_in_progress + - delete_failed + - deleted # Schema for error response body Error: From d17cda42acee4807be96452154e37a2932fc8448 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 31 Aug 2018 11:54:43 -0700 Subject: [PATCH 03/60] add support for x-terraform-field-status extension --- openapi/resource_info.go | 48 +++++++- openapi/resource_info_test.go | 217 +++++++++++++++++++++++++++++++++- 2 files changed, 258 insertions(+), 7 deletions(-) diff --git a/openapi/resource_info.go b/openapi/resource_info.go index ffdb4d368..4b874cdb3 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -16,8 +16,12 @@ const extTfForceNew = "x-terraform-force-new" const extTfSensitive = "x-terraform-sensitive" const extTfExcludeResource = "x-terraform-exclude-resource" const extTfFieldName = "x-terraform-field-name" +const extTfFieldStatus = "x-terraform-field-status" const extTfID = "x-terraform-id" +const idDefaultPropertyName = "id" +const statusDefaultPropertyName = "status" + type resourcesInfo map[string]resourceInfo // resourceInfo serves as translator between swagger definitions and terraform schemas @@ -209,7 +213,7 @@ func (r resourceInfo) getResourceIDURL(id string) (string, error) { // getResourceIdentifier returns the property name that is supposed to be used as the identifier. The resource id // is selected as follows: -// 1.If the given schema definition contains a property configured with metadata 'x-terraform-id' set to true, that property value +// 1.If the given schema definition contains a property configured with metadata 'x-terraform-id' set to true, that property // will be used to set the state ID of the resource. Additionally, the value will be used when performing GET/PUT/DELETE requests to // identify the resource in question. // 2. If none of the properties of the given schema definition contain such metadata, it is expected that the payload @@ -237,6 +241,38 @@ func (r resourceInfo) getResourceIdentifier() (string, error) { return identifierProperty, nil } +// getStatusIdentifier returns the property name that is supposed to be used as the status field. The status field +// is selected as follows: +// 1.If the given schema definition contains a property configured with metadata 'x-terraform-field-status' set to true, that property +// will be used to check the different statues for the asynchronous pooling mechanism. +// 2. If none of the properties of the given schema definition contain such metadata, it is expected that the payload +// will have a property named 'status' +// 3. If none of the above requirements is met, an error will be returned +func (r resourceInfo) getStatusIdentifier() (string, error) { + statusProperty := "" + for propertyName, property := range r.schemaDefinition.Properties { + if r.isIDProperty(propertyName) { + continue + } + if r.isStatusProperty(propertyName) { + statusProperty = propertyName + continue + } + // field with extTfFieldStatus metadata takes preference over 'status' fields as the service provider is the one acknowledging + // the fact that this field should be used as identifier of the resource + if terraformID, ok := property.Extensions.GetBool(extTfFieldStatus); ok && terraformID { + statusProperty = propertyName + break + } + } + // if the id field is missing and there isn't any properties set with extTfFieldStatus, there is not way for the resource + // to be identified and therefore an error is returned + if statusProperty == "" { + return "", fmt.Errorf("could not find any status property in the resource swagger definition. Please make sure the resource definition has either one property named 'status' or one property that contains %s metadata", extTfFieldStatus) + } + return statusProperty, nil +} + // shouldIgnoreResource checks whether the POST operation for a given resource as the 'x-terraform-exclude-resource' extension // defined with true value. If so, the resource will not be exposed to the OpenAPI Terraform provder; otherwise it will // be exposed and users will be able to manage such resource via terraform. @@ -248,5 +284,13 @@ func (r resourceInfo) shouldIgnoreResource() bool { } func (r resourceInfo) isIDProperty(propertyName string) bool { - return terraformutils.ConvertToTerraformCompliantName(propertyName) == "id" + return r.propertyNameMatchesDefaultName(propertyName, idDefaultPropertyName) +} + +func (r resourceInfo) isStatusProperty(propertyName string) bool { + return r.propertyNameMatchesDefaultName(propertyName, statusDefaultPropertyName) +} + +func (r resourceInfo) propertyNameMatchesDefaultName(propertyName, expectedPropertyName string) bool { + return terraformutils.ConvertToTerraformCompliantName(propertyName) == expectedPropertyName } diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 9d572d225..94f35d1f1 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -997,7 +997,7 @@ func TestGetResourceIdentifier(t *testing.T) { schemaDefinition: spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ - "id": { + idDefaultPropertyName: { VendorExtensible: spec.VendorExtensible{}, SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -1013,14 +1013,14 @@ func TestGetResourceIdentifier(t *testing.T) { So(err, ShouldBeNil) }) Convey("Then the value returned should be 'id'", func() { - So(id, ShouldEqual, "id") + So(id, ShouldEqual, idDefaultPropertyName) }) }) }) Convey("Given a swagger schema definition that DOES NOT have an 'id' property but has a property configured with x-terraform-id set to TRUE", t, func() { extensions := spec.Extensions{} - extensions.Add("x-terraform-id", true) + extensions.Add(extTfID, true) r := resourceInfo{ schemaDefinition: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1048,7 +1048,7 @@ func TestGetResourceIdentifier(t *testing.T) { Convey("Given a swagger schema definition that HAS BOTH an 'id' property AND ALSO a property configured with x-terraform-id set to true", t, func() { extensions := spec.Extensions{} - extensions.Add("x-terraform-id", true) + extensions.Add(extTfID, true) r := resourceInfo{ schemaDefinition: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1082,7 +1082,7 @@ func TestGetResourceIdentifier(t *testing.T) { Convey("Given a swagger schema definition that DOES NOT have an 'id' property but has a property configured with x-terraform-id set to FALSE", t, func() { extensions := spec.Extensions{} - extensions.Add("x-terraform-id", false) + extensions.Add(extTfID, false) r := resourceInfo{ schemaDefinition: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -1135,6 +1135,213 @@ func TestGetResourceIdentifier(t *testing.T) { }) } +func TestGetStatusIdentifier(t *testing.T) { + Convey("Given a swagger schema definition that has an status property", t, func() { + r := resourceInfo{ + schemaDefinition: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + statusDefaultPropertyName: { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + status, err := r.getStatusIdentifier() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should be 'status'", func() { + So(status, ShouldEqual, statusDefaultPropertyName) + }) + }) + }) + + Convey("Given a swagger schema definition that DOES NOT have an 'status' property but has a property configured with x-terraform-field-status set to TRUE", t, func() { + extensions := spec.Extensions{} + extensions.Add(extTfFieldStatus, true) + expectedStatusProperty := "some-other-property-holding-status" + r := resourceInfo{ + schemaDefinition: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + expectedStatusProperty: { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + id, err := r.getStatusIdentifier() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should be 'some-other-property-holding-status'", func() { + So(id, ShouldEqual, expectedStatusProperty) + }) + }) + }) + + Convey("Given a swagger schema definition that HAS BOTH an 'status' property AND ALSO a property configured with 'x-terraform-field-status' set to true", t, func() { + extensions := spec.Extensions{} + extensions.Add(extTfFieldStatus, true) + expectedStatusProperty := "some-other-property-holding-status" + r := resourceInfo{ + schemaDefinition: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "status": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + expectedStatusProperty: { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + id, err := r.getStatusIdentifier() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should be 'some-other-property-holding-status' as it takes preference over the default 'status' property", func() { + So(id, ShouldEqual, expectedStatusProperty) + }) + }) + }) + + Convey("Given a swagger schema definition that DOES NOT have an 'status' property but has a property configured with 'x-terraform-field-status' set to FALSE", t, func() { + extensions := spec.Extensions{} + extensions.Add(extTfFieldStatus, false) + expectedStatusProperty := "some-other-property-holding-status" + r := resourceInfo{ + schemaDefinition: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + expectedStatusProperty: { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + _, err := r.getStatusIdentifier() + Convey("Then the error returned should not be nil", func() { + So(err, ShouldNotBeNil) + }) + }) + }) + + Convey("Given a swagger schema definition that NEITHER HAS an 'status' property NOR a property configured with 'x-terraform-field-status' set to true", t, func() { + r := resourceInfo{ + schemaDefinition: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "prop-that-is-not-status": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "prop-that-is-not-status-and-does-not-have-status-metadata-either": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + _, err := r.getStatusIdentifier() + Convey("Then the error returned should NOT be nil", func() { + So(err, ShouldNotBeNil) + }) + }) + }) +} + +func TestIsIDProperty(t *testing.T) { + Convey("Given a swagger schema definition", t, func() { + r := resourceInfo{} + Convey("When isIDProperty method is called with property named 'id'", func() { + isIDProperty := r.isIDProperty("id") + Convey("Then the error returned should be nil", func() { + So(isIDProperty, ShouldBeTrue) + }) + }) + Convey("When isIDProperty method is called with property NOT named 'id'", func() { + isIDProperty := r.isIDProperty("something_not_id") + Convey("Then the error returned should be nil", func() { + So(isIDProperty, ShouldBeFalse) + }) + }) + }) +} + +func TestIsStatusProperty(t *testing.T) { + Convey("Given a swagger schema definition", t, func() { + r := resourceInfo{} + Convey("When isStatusProperty method is called with property named 'status'", func() { + isStatusProperty := r.isStatusProperty("status") + Convey("Then the error returned should be nil", func() { + So(isStatusProperty, ShouldBeTrue) + }) + }) + Convey("When isStatusProperty method is called with property NOT named 'status'", func() { + isStatusProperty := r.isStatusProperty("something_not_status") + Convey("Then the error returned should be nil", func() { + So(isStatusProperty, ShouldBeFalse) + }) + }) + }) +} + +func TestPropertyNameMatchesDefaultName(t *testing.T) { + Convey("Given a swagger schema definition", t, func() { + r := resourceInfo{} + Convey("When propertyNameMatchesDefaultName method is called with property named 'status' and an expected name matching the property property name", func() { + propertyNameMatchesDefaultName := r.propertyNameMatchesDefaultName("status", "status") + Convey("Then the error returned should be nil", func() { + So(propertyNameMatchesDefaultName, ShouldBeTrue) + }) + }) + Convey("When propertyNameMatchesDefaultName method is called with property named 'ID' which is not terraform compliant name and an expected property name", func() { + propertyNameMatchesDefaultName := r.propertyNameMatchesDefaultName("ID", "id") + Convey("Then the error returned should be nil", func() { + So(propertyNameMatchesDefaultName, ShouldBeTrue) + }) + }) + Convey("When propertyNameMatchesDefaultName method is called with property NOT matching the expected property name", func() { + propertyNameMatchesDefaultName := r.propertyNameMatchesDefaultName("something_not_status", "") + Convey("Then the error returned should be nil", func() { + So(propertyNameMatchesDefaultName, ShouldBeFalse) + }) + }) + }) +} + func TestShouldIgnoreResource(t *testing.T) { Convey("Given a terraform compliant resource that has a POST operation containing the x-terraform-exclude-resource with value true", t, func() { r := resourceInfo{ From 7aa6ba74812133d447fef5b52df33f452b9ca80e Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 31 Aug 2018 12:05:48 -0700 Subject: [PATCH 04/60] update documentation adding info about x-terraform-field-status --- docs/how_to.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/how_to.md b/docs/how_to.md index b68b80877..b47ab545f 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -395,6 +395,7 @@ x-terraform-force-new | boolean | If the value of this property is updated; ter x-terraform-sensitive | boolean | If this meta attribute is present in an object definition property, it will be considered sensitive as far as terraform is concerned, meaning that its value will not be disclosed in the TF state file x-terraform-id | boolean | If this meta attribute is present in an object definition property, the value will be used as the resource identifier when performing the read, update and delete API operations. The value will also be stored in the ID field of the local state file. x-terraform-field-name | string | This enables service providers to override the schema definition property name with a different one which will be the property name used in the terraform configuration file. This is mostly used to expose the internal property to a more user friendly name. If the extension is not present and the property name is not terraform compliant, an automatic conversion will be performed by the OpenAPI Terraform provider to make the name compliant (following Terraform's field name convention to be snake_case) +x-terraform-field-status | boolean | If this meta attribute is present in an object definition property, the value will be used as the status identifier when executing the polling mechanism on eligible async operations such as POST/PUT/DELETE. ##### Full Example From 8ce75d83cbadf53043517fad26dc9e1c552b0a51 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 31 Aug 2018 14:09:21 -0700 Subject: [PATCH 05/60] add backend for lbs - added timetoprocess to the model which will allow to specify the time the resource takes to perform the operations (handy for testing puroses) - added status to the model --- examples/swaggercodegen/api/api/cdn.go | 2 +- examples/swaggercodegen/api/api/http_utils.go | 14 ++-- examples/swaggercodegen/api/api/lb_api.go | 66 ++++++++++++++++--- examples/swaggercodegen/api/api/lbv1.go | 5 ++ 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/examples/swaggercodegen/api/api/cdn.go b/examples/swaggercodegen/api/api/cdn.go index 66e0b55d0..709dd02e5 100644 --- a/examples/swaggercodegen/api/api/cdn.go +++ b/examples/swaggercodegen/api/api/cdn.go @@ -74,7 +74,7 @@ func ContentDeliveryNetworkDeleteV1(w http.ResponseWriter, r *http.Request) { } delete(db, cdn.Id) log.Printf("DELETE [%s]", cdn.Id) - updateResponseHeaders(http.StatusNoContent, w) + sendResponse(http.StatusNoContent, w, nil) } func retrieveCdn(r *http.Request) (*ContentDeliveryNetwork, error) { diff --git a/examples/swaggercodegen/api/api/http_utils.go b/examples/swaggercodegen/api/api/http_utils.go index 76b55bbcf..4b1b14342 100644 --- a/examples/swaggercodegen/api/api/http_utils.go +++ b/examples/swaggercodegen/api/api/http_utils.go @@ -21,12 +21,16 @@ func readRequest(r *http.Request, in interface{}) error { func sendResponse(httpResponseStatusCode int, w http.ResponseWriter, out interface{}) { var resBody []byte var err error - if resBody, err = json.Marshal(out); err != nil { - msg := fmt.Sprintf("internal server error - %s", err) - sendErrorResponse(http.StatusInternalServerError, msg, w) + if out != nil { + if resBody, err = json.Marshal(out); err != nil { + msg := fmt.Sprintf("internal server error - %s", err) + sendErrorResponse(http.StatusInternalServerError, msg, w) + } + } + updateResponseHeaders(httpResponseStatusCode, w) + if len(resBody) > 0 { + w.Write(resBody) } - w.WriteHeader(httpResponseStatusCode) - w.Write(resBody) } func sendErrorResponse(httpStatusCode int, message string, w http.ResponseWriter) { diff --git a/examples/swaggercodegen/api/api/lb_api.go b/examples/swaggercodegen/api/api/lb_api.go index 9678ed41a..a5c7141e8 100644 --- a/examples/swaggercodegen/api/api/lb_api.go +++ b/examples/swaggercodegen/api/api/lb_api.go @@ -12,24 +12,74 @@ package api import ( "net/http" + "github.com/pborman/uuid" + "log" + "strings" + "fmt" ) +var lbsDB = map[string]*Lbv1{} + func LBCreateV1(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) + lb := &Lbv1{} + err := readRequest(r, lb) + if err != nil { + sendErrorResponse(http.StatusBadRequest, err.Error(), w) + return + } + lb.Id = uuid.New() + lbsDB[lb.Id] = lb + log.Printf("POST [%+v\n]", lb) + sendResponse(http.StatusCreated, w, lb) } func LBDeleteV1(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) + lb, err := retrieveLB(r) + if err != nil { + sendErrorResponse(http.StatusNotFound, err.Error(), w) + return + } + delete(db, lb.Id) + log.Printf("DELETE [%s]", lb.Id) + sendResponse(http.StatusNoContent, w, nil) } func LBGetV1(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) + lb, err := retrieveLB(r) + log.Printf("GET [%+v\n]", lb) + if err != nil { + sendErrorResponse(http.StatusNotFound, err.Error(), w) + return + } + sendResponse(http.StatusOK, w, lb) } func LBUpdateV1(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) + lb, err := retrieveLB(r) + if err != nil { + sendErrorResponse(http.StatusNotFound, err.Error(), w) + return + } + newLB := &Lbv1{} + err = readRequest(r, newLB) + if err != nil { + sendErrorResponse(http.StatusBadRequest, err.Error(), w) + return + } + log.Printf("UPDATE [%+v\n]", newLB) + lb.Id = lb.Id + lbsDB[lb.Id] = newLB + sendResponse(http.StatusOK, w, newLB) } + +func retrieveLB(r *http.Request) (*Lbv1, error) { + id := strings.TrimPrefix(r.URL.Path, "/v1/lbs/") + if id == "" { + return nil, fmt.Errorf("lb id path param not provided") + } + lb := lbsDB[id] + if lb == nil { + return nil, fmt.Errorf("lb id '%s' not found", id) + } + return lb, nil +} \ No newline at end of file diff --git a/examples/swaggercodegen/api/api/lbv1.go b/examples/swaggercodegen/api/api/lbv1.go index e888be9ef..f006b7f83 100644 --- a/examples/swaggercodegen/api/api/lbv1.go +++ b/examples/swaggercodegen/api/api/lbv1.go @@ -17,4 +17,9 @@ type Lbv1 struct { Name string `json:"name"` Backends []string `json:"backends"` + + TimeToProcess int32 `json:"timeToProcess"` + + // lb resource status + Status string `json:"status,omitempty"` } From db43dccaa41d7773e84f34056de0aaa631650dd8 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 31 Aug 2018 16:31:40 -0700 Subject: [PATCH 06/60] add default timeout (will be applicable to all CRUD operations) - Support for a more granular configuration is on the roadmap, where providers are able to specify an extension like 'x-terraform-resource-timeout' per resource operation --- openapi/resource_factory.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index 17c7f6c19..8e559e9f0 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -12,6 +12,7 @@ import ( "io/ioutil" "log" "strconv" + "time" ) type resourceFactory struct { @@ -20,6 +21,8 @@ type resourceFactory struct { apiAuthenticator apiAuthenticator } +var defaultTimeout = time.Duration(60 * time.Second) + func (r resourceFactory) createSchemaResource() (*schema.Resource, error) { s, err := r.resourceInfo.createTerraformResourceSchema() if err != nil { @@ -31,6 +34,9 @@ func (r resourceFactory) createSchemaResource() (*schema.Resource, error) { Read: r.read, Delete: r.delete, Update: r.update, + Timeouts: &schema.ResourceTimeout{ + Default: &defaultTimeout, + }, }, nil } From 91711cfce6d73a0d484145d2f6473b721ef935c5 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 31 Aug 2018 16:32:28 -0700 Subject: [PATCH 07/60] update lb example resource to behave as an async resource --- examples/swaggercodegen/api/api/cdn.go | 2 +- examples/swaggercodegen/api/api/lb_api.go | 101 ++++++++++++++---- examples/swaggercodegen/api/api/lbv1.go | 2 + .../swaggercodegen/api/resources/swagger.yaml | 6 ++ examples/swaggercodegen/main.tf | 7 ++ 5 files changed, 95 insertions(+), 23 deletions(-) diff --git a/examples/swaggercodegen/api/api/cdn.go b/examples/swaggercodegen/api/api/cdn.go index 709dd02e5..104063cf7 100644 --- a/examples/swaggercodegen/api/api/cdn.go +++ b/examples/swaggercodegen/api/api/cdn.go @@ -57,8 +57,8 @@ func ContentDeliveryNetworkUpdateV1(w http.ResponseWriter, r *http.Request) { sendErrorResponse(http.StatusBadRequest, err.Error(), w) return } + newCDN.Id = cdn.Id log.Printf("UPDATE [%+v\n]", newCDN) - cdn.Id = cdn.Id db[cdn.Id] = newCDN sendResponse(http.StatusOK, w, newCDN) } diff --git a/examples/swaggercodegen/api/api/lb_api.go b/examples/swaggercodegen/api/api/lb_api.go index a5c7141e8..901618539 100644 --- a/examples/swaggercodegen/api/api/lb_api.go +++ b/examples/swaggercodegen/api/api/lb_api.go @@ -16,10 +16,37 @@ import ( "log" "strings" "fmt" + "time" ) +var defaultTimeToProcess int32 = 30 // 30 seconds var lbsDB = map[string]*Lbv1{} +type status string +const( + deployPending status = "deploy_pending" + deployInProgress status = "deploy_in_progress" + deployFailed status = "deploy_failed" + deployed status = "deployed" + deletePending status = "delete_pending" + deleteInProgress status = "delete_in_progress" + deleteFailed status = "delete_failed" + deleted status = "deleted" +) + +var deployPendingStatuses = []status{deployInProgress} +var deletePendingStatuses = []status{deleteInProgress} + +func LBGetV1(w http.ResponseWriter, r *http.Request) { + lb, err := retrieveLB(r) + log.Printf("GET [%+v\n]", lb) + if err != nil { + sendErrorResponse(http.StatusNotFound, err.Error(), w) + return + } + sendResponse(http.StatusOK, w, lb) +} + func LBCreateV1(w http.ResponseWriter, r *http.Request) { lb := &Lbv1{} err := readRequest(r, lb) @@ -28,48 +55,78 @@ func LBCreateV1(w http.ResponseWriter, r *http.Request) { return } lb.Id = uuid.New() + updateLBStatus(lb, deployPending) lbsDB[lb.Id] = lb log.Printf("POST [%+v\n]", lb) - sendResponse(http.StatusCreated, w, lb) + + go pretendResourceOperationIsProcessing(lb, deployPendingStatuses, deployed, deployFailed) + + sendResponse(http.StatusAccepted, w, lb) } -func LBDeleteV1(w http.ResponseWriter, r *http.Request) { +func LBUpdateV1(w http.ResponseWriter, r *http.Request) { lb, err := retrieveLB(r) if err != nil { sendErrorResponse(http.StatusNotFound, err.Error(), w) return } - delete(db, lb.Id) - log.Printf("DELETE [%s]", lb.Id) - sendResponse(http.StatusNoContent, w, nil) -} - -func LBGetV1(w http.ResponseWriter, r *http.Request) { - lb, err := retrieveLB(r) - log.Printf("GET [%+v\n]", lb) + newLB := &Lbv1{} + err = readRequest(r, newLB) if err != nil { - sendErrorResponse(http.StatusNotFound, err.Error(), w) + sendErrorResponse(http.StatusBadRequest, err.Error(), w) return } - sendResponse(http.StatusOK, w, lb) + newLB.Id = lb.Id + updateLBStatus(newLB, deployPending) + lbsDB[newLB.Id] = newLB + log.Printf("UPDATE [%+v\n]", newLB) + + go pretendResourceOperationIsProcessing(lb, deployPendingStatuses, deployed, deployFailed) + + sendResponse(http.StatusAccepted, w, newLB) } -func LBUpdateV1(w http.ResponseWriter, r *http.Request) { +func LBDeleteV1(w http.ResponseWriter, r *http.Request) { lb, err := retrieveLB(r) if err != nil { sendErrorResponse(http.StatusNotFound, err.Error(), w) return } - newLB := &Lbv1{} - err = readRequest(r, newLB) - if err != nil { - sendErrorResponse(http.StatusBadRequest, err.Error(), w) - return + updateLBStatus(lb, deletePending) + delete(db, lb.Id) + log.Printf("DELETE [%s]", lb.Id) + + go pretendResourceOperationIsProcessing(lb, deletePendingStatuses, deleted, deleteFailed) + + sendResponse(http.StatusAccepted, w, nil) +} + +func pretendResourceOperationIsProcessing(lb *Lbv1, pendingStatues []status, completed status, failureStatus status) { + var timeToProcess = defaultTimeToProcess + // Override default wait time if it is configured in the lb + if lb.TimeToProcess > 0 { + timeToProcess = lb.TimeToProcess } - log.Printf("UPDATE [%+v\n]", newLB) - lb.Id = lb.Id - lbsDB[lb.Id] = newLB - sendResponse(http.StatusOK, w, newLB) + if lb.SimulateFailure { + log.Printf("Simulating failure - timeToProcess = %d", timeToProcess) + time.Sleep(time.Duration(timeToProcess) * time.Second) + updateLBStatus(lb, failureStatus) + } else { + waitTimePerPendingStatus := timeToProcess / int32(len(pendingStatues)) + timeToProcessPerStatusDuration := time.Duration(waitTimePerPendingStatus) * time.Second + for _, newStatus := range pendingStatues { + log.Printf("Precessing resource [%s] [%s] - timeToProcess = %ds", lb.Id, newStatus, waitTimePerPendingStatus) + time.Sleep(timeToProcessPerStatusDuration) + updateLBStatus(lb, newStatus) + } + updateLBStatus(lb, completed) + } +} + +func updateLBStatus(lb *Lbv1, newStatus status) { + oldStatus := lb.Status + lb.Status = string(newStatus) + log.Printf("LB [%s] status updated '%s' => '%s'", lb.Id, oldStatus, newStatus) } func retrieveLB(r *http.Request) (*Lbv1, error) { diff --git a/examples/swaggercodegen/api/api/lbv1.go b/examples/swaggercodegen/api/api/lbv1.go index f006b7f83..3f3c0747c 100644 --- a/examples/swaggercodegen/api/api/lbv1.go +++ b/examples/swaggercodegen/api/api/lbv1.go @@ -20,6 +20,8 @@ type Lbv1 struct { TimeToProcess int32 `json:"timeToProcess"` + SimulateFailure bool `json:"simulate_failure"` + // lb resource status Status string `json:"status,omitempty"` } diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index fb246258a..de856678b 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -308,6 +308,7 @@ definitions: x-terraform-field-status: true # identifies the field that should be used as status for async operations. This is handy when the field name is not status but some other name the service provider might have chosen and enables the provider to identify the field as the status field that will be used to track progress for the async operations description: lb resource status type: string + readOnly: true enum: - deploy_pending - deploy_in_progress @@ -317,6 +318,11 @@ definitions: - delete_in_progress - delete_failed - deleted + timeToProcess: # time that the resource will take to be processed in seconds + type: integer + default: 60 # it will take two minute to process the resource operation (POST/PUT/READ/DELETE) + simulate_failure: # allows user to set it to true and force an error on the API when the given operation (POST/PUT/READ/DELETE) is being performed + type: boolean # Schema for error response body Error: diff --git a/examples/swaggercodegen/main.tf b/examples/swaggercodegen/main.tf index a86052947..3960d6bd0 100644 --- a/examples/swaggercodegen/main.tf +++ b/examples/swaggercodegen/main.tf @@ -13,4 +13,11 @@ resource "swaggercodegen_cdns_v1" "my_cdn" { example_int = 12 better_example_number_field_name = 1.12 example_boolean = true +} + +resource "swaggercodegen_lbs_v1" "my_lb" { + name = "some_name" + backends = ["backend1.com"] + time_to_process = 30 + simulate_failure = false } \ No newline at end of file From 5a299f74f8b8faac06bc3b22232b06619dbc0cd0 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 31 Aug 2018 16:45:39 -0700 Subject: [PATCH 08/60] adding some comments to terraform config file for reference purposes --- examples/swaggercodegen/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/swaggercodegen/main.tf b/examples/swaggercodegen/main.tf index 3960d6bd0..6860f2346 100644 --- a/examples/swaggercodegen/main.tf +++ b/examples/swaggercodegen/main.tf @@ -18,6 +18,6 @@ resource "swaggercodegen_cdns_v1" "my_cdn" { resource "swaggercodegen_lbs_v1" "my_lb" { name = "some_name" backends = ["backend1.com"] - time_to_process = 30 - simulate_failure = false + time_to_process = 30 # the operation (post,update,delete) will take 30s in the API to complete + simulate_failure = false # no failures wished now ;) (post,update,delete) } \ No newline at end of file From f4aa43d861c33e94fcdc628eaad318f1bd184526 Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 4 Sep 2018 10:38:56 -0700 Subject: [PATCH 09/60] add support for x-terraform-resource-poll-enabled extension - added unit tests - updated documentation --- docs/how_to.md | 76 +++++++++++++++++++++++++++++++++++ openapi/resource_info.go | 20 ++++++++- openapi/resource_info_test.go | 59 +++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) diff --git a/docs/how_to.md b/docs/how_to.md index b47ab545f..739c1843b 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -263,6 +263,7 @@ Extension Name | Type | Description ---|:---:|--- [x-terraform-exclude-resource](#xTerraformExcludeResource) | bool | Only available in resource root's POST operation. Defines whether a given terraform compliant resource should be exposed to the OpenAPI Terraform provider or ignored. [x-terraform-header](#xTerraformHeader) | string | Only available in operation level parameters at the moment. Defines that he given header should be passed as part of the request. +[x-terraform-resource-poll-enabled](#xTerraformResourcePollEnabled) | bool | Only supported in operation response (202). Defines that if the API responds with the given HTTP Status code (202), the polling mechanism will be enabled. This allows the OpenAPI Terraform provider to perform read calls to the remote API and check the resource state. The polling mechanism finalises if the remote resource state arrives at completion, failure state or times-out (60s) ###### x-terraform-exclude-resource @@ -332,6 +333,66 @@ the header will be the one specified in the terraform configuration ```request h *Note: Currently, parameters of type 'header' are only supported on an operation level* +###### x-terraform-resource-poll-enabled + +This extension allows the service provider to enable the polling mechanism in the OpenAPI Terraform provider for asynchronous +operations. In order for this to work, the following must be met: + +- The resource definition must have a read-only field that defines the status of the resource. By default, if a string field caThis can be a field called +called 'status' is present in the resource schema definition that field will be used to track the different statues of the resource. Alternatively, +a field can be marked to serve as the status field adding the 'x-terraform-field-status'. This field will be used as the status +field even if there is another field named 'status'. This gives service providers flexibility to name their status field the +way they desire. More details about the 'x-terraform-field-status' extension can be found in the [Attribute details](#attributeDetails) section. +- The polling mechanism required two more extensions to work which define the expected 'status' values for both target and +pending statuses. These are: + + - **x-terraform-resource-poll-target-statuses**: Defines the statuses on which the resource state will be considered 'completed' + - **x-terraform-resource-poll-pending-statuses**: Defines the statuses on which the resource state will be considered 'in progress'. +Any other state returned that returned but is not part of this list will be considered as a failure and the polling mechanism +will stop its execution accordingly. + +In the example below, the response with HTTP status code 202 has the extension defined with value 'true' meaning +that the OpenAPI Terraform provider will treat this response as asynchronous. Therefore, the provider will perform +continues calls to the resource's instance GET operation and will use the value from the resource 'status' property to +determine the state of the resource: + +```` + /v1/lbs: + post: + ... + responses: + 202: # Accepted + x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' + x-terraform-resource-poll-target-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed + x-terraform-resource-poll-pending-statuses: "deploy_pending, deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying + schema: + $ref: "#/definitions/LBV1" +definitions: + LBV1: + type: "object" + required: + - name + - backends + properties: + ... + status: + x-terraform-field-status: true # identifies the field that should be used as status for async operations. This is handy when the field name is not status but some other name the service provider might have chosen and enables the provider to identify the field as the status field that will be used to track progress for the async operations + description: lb resource status + type: string + readOnly: true + enum: + - deploy_pending + - deploy_in_progress + - deploy_failed + - deployed + - delete_pending + - delete_in_progress + - delete_failed + - deleted +```` + +*Note: This extension is only supported at the response level.* + #### Definitions - **Field Name:** definitions @@ -450,6 +511,21 @@ definitions: someNonUserFriendlyPropertyName: # If this property did not have the 'x-terraform-field-name' extension, the property name will be automatically converted by the OpenAPI Terraform provider into a name that is Terraform field name compliant. The result will be: some_non_user_friendly_propertyName type: string x-terraform-field-name: property_name_more_user_friendly + + status: + x-terraform-field-status: true # identifies the field that should be used as status for async operations. This is handy when the field name is not 'status' but some other name the service provider might have chosen and enables the provider to identify the field as the status field that will be used to track progress for the async operations + type: string + readOnly: true + enum: # this is jsut for documentation purposes and to let the consumer know what statues should be expected + - deploy_pending + - deploy_in_progress + - deploy_failed + - deployed + - delete_pending + - delete_in_progress + - delete_failed + - deleted + ``` diff --git a/openapi/resource_info.go b/openapi/resource_info.go index 4b874cdb3..a4df1a19e 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -11,14 +11,18 @@ import ( "github.com/hashicorp/terraform/helper/schema" ) +// Definition level extensions const extTfImmutable = "x-terraform-immutable" const extTfForceNew = "x-terraform-force-new" const extTfSensitive = "x-terraform-sensitive" -const extTfExcludeResource = "x-terraform-exclude-resource" const extTfFieldName = "x-terraform-field-name" const extTfFieldStatus = "x-terraform-field-status" const extTfID = "x-terraform-id" +// Operation level extensions +const extTfExcludeResource = "x-terraform-exclude-resource" +const extTfResourcePollEnabled= "x-terraform-resource-poll-enabled" + const idDefaultPropertyName = "id" const statusDefaultPropertyName = "status" @@ -283,6 +287,20 @@ func (r resourceInfo) shouldIgnoreResource() bool { return false } +// isResourcePollingEnabled checks whether there is any response code defined for the given responseStatusCode and if so +// whether that response contains the extension 'x-terraform-resource-poll-enabled' set to true returning true; +// otherwise false is returned +func (r resourceInfo) isResourcePollingEnabled(responses spec.Responses, responseStatusCode int) bool { + response, exists := responses.StatusCodeResponses[responseStatusCode] + if !exists { + return false + } + if isResourcePollEnabled, ok := response.Extensions.GetBool(extTfResourcePollEnabled); ok && isResourcePollEnabled { + return true + } + return false +} + func (r resourceInfo) isIDProperty(propertyName string) bool { return r.propertyNameMatchesDefaultName(propertyName, idDefaultPropertyName) } diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 94f35d1f1..bea844841 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -7,6 +7,7 @@ import ( . "github.com/smartystreets/goconvey/convey" "reflect" "testing" + "net/http" ) func TestGetResourceURL(t *testing.T) { @@ -1342,6 +1343,64 @@ func TestPropertyNameMatchesDefaultName(t *testing.T) { }) } +func TestIsResourcePollingEnabled(t *testing.T) { + Convey("Given a resourceInfo", t, func() { + r := resourceInfo{} + Convey("When isResourcePollingEnabled method is called with a list of responses where one of the reponses matches the response status received and has the 'x-terraform-resource-poll-enabled' extension set to true", func() { + extensions := spec.Extensions{} + extensions.Add(extTfResourcePollEnabled, true) + responses := spec.Responses{ + ResponsesProps: spec.ResponsesProps{ + StatusCodeResponses: map[int]spec.Response{ + http.StatusAccepted: { + VendorExtensible: spec.VendorExtensible{ + Extensions: extensions, + }, + }, + }, + }, + } + isResourcePollingEnabled := r.isResourcePollingEnabled(responses, http.StatusAccepted) + Convey("Then the bool returned should be true", func() { + So(isResourcePollingEnabled, ShouldBeTrue) + }) + }) + Convey("When isResourcePollingEnabled method is called with a list of responses where one of the reponses matches the response status received and has the 'x-terraform-resource-poll-enabled' extension set to false", func() { + extensions := spec.Extensions{} + extensions.Add(extTfResourcePollEnabled, false) + responses := spec.Responses{ + ResponsesProps: spec.ResponsesProps{ + StatusCodeResponses: map[int]spec.Response{ + http.StatusAccepted: { + VendorExtensible: spec.VendorExtensible{ + Extensions: extensions, + }, + }, + }, + }, + } + isResourcePollingEnabled := r.isResourcePollingEnabled(responses, http.StatusAccepted) + Convey("Then the bool returned should be false", func() { + So(isResourcePollingEnabled, ShouldBeFalse) + }) + }) + Convey("When isResourcePollingEnabled method is called with list of responses where non of the codes match the given response http code", func() { + responses := spec.Responses{ + ResponsesProps: spec.ResponsesProps{ + StatusCodeResponses: map[int]spec.Response{ + http.StatusOK: { + }, + }, + }, + } + isResourcePollingEnabled := r.isResourcePollingEnabled(responses, http.StatusAccepted) + Convey("Then bool returned should be false", func() { + So(isResourcePollingEnabled, ShouldBeFalse) + }) + }) + }) +} + func TestShouldIgnoreResource(t *testing.T) { Convey("Given a terraform compliant resource that has a POST operation containing the x-terraform-exclude-resource with value true", t, func() { r := resourceInfo{ From fa9ab1392e96727be4c14ec6d9a4c948e320c1f2 Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 4 Sep 2018 11:55:41 -0700 Subject: [PATCH 10/60] add support for x-terraform-resource-poll-target-status - isResourcePollingEnabled now returns response along with verdict, this will help consumer to just get the response and use it accordingly instead of having to query the responses again afterwards. - added unit test - updated documentation --- docs/how_to.md | 4 +- openapi/resource_info.go | 23 +++++-- openapi/resource_info_test.go | 110 +++++++++++++++++++++++++++++++++- 3 files changed, 128 insertions(+), 9 deletions(-) diff --git a/docs/how_to.md b/docs/how_to.md index 739c1843b..f41f5d4eb 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -346,8 +346,8 @@ way they desire. More details about the 'x-terraform-field-status' extension can - The polling mechanism required two more extensions to work which define the expected 'status' values for both target and pending statuses. These are: - - **x-terraform-resource-poll-target-statuses**: Defines the statuses on which the resource state will be considered 'completed' - - **x-terraform-resource-poll-pending-statuses**: Defines the statuses on which the resource state will be considered 'in progress'. + - **x-terraform-resource-poll-target-statuses**: (type: string) Comma separated values - Defines the statuses on which the resource state will be considered 'completed' + - **x-terraform-resource-poll-pending-statuses**: (type: string) Comma separated values - Defines the statuses on which the resource state will be considered 'in progress'. Any other state returned that returned but is not part of this list will be considered as a failure and the polling mechanism will stop its execution accordingly. diff --git a/openapi/resource_info.go b/openapi/resource_info.go index a4df1a19e..d6b5d7b0a 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -22,6 +22,7 @@ const extTfID = "x-terraform-id" // Operation level extensions const extTfExcludeResource = "x-terraform-exclude-resource" const extTfResourcePollEnabled= "x-terraform-resource-poll-enabled" +const extTfResourcePollTargetStatuses= "x-terraform-resource-poll-target-statuses" const idDefaultPropertyName = "id" const statusDefaultPropertyName = "status" @@ -290,15 +291,29 @@ func (r resourceInfo) shouldIgnoreResource() bool { // isResourcePollingEnabled checks whether there is any response code defined for the given responseStatusCode and if so // whether that response contains the extension 'x-terraform-resource-poll-enabled' set to true returning true; // otherwise false is returned -func (r resourceInfo) isResourcePollingEnabled(responses spec.Responses, responseStatusCode int) bool { +func (r resourceInfo) isResourcePollingEnabled(responses spec.Responses, responseStatusCode int) (bool, *spec.Response) { response, exists := responses.StatusCodeResponses[responseStatusCode] if !exists { - return false + return false, nil } if isResourcePollEnabled, ok := response.Extensions.GetBool(extTfResourcePollEnabled); ok && isResourcePollEnabled { - return true + return true, &response } - return false + return false, nil +} + +func (r resourceInfo) getResourcePollTargetStatuses(response spec.Response) ([]string, error) { + return r.getPollingStatuses(response, extTfResourcePollTargetStatuses) +} + +func (r resourceInfo) getPollingStatuses(response spec.Response, extension string) ([]string, error) { + statuses := []string{} + if resourcePollTargets, exists := response.Extensions.GetString(extension); exists { + statuses = strings.Split(resourcePollTargets, ",") + } else { + return nil, fmt.Errorf("response missing required extension '%s' for the polling mechanism to work", extension) + } + return statuses, nil } func (r resourceInfo) isIDProperty(propertyName string) bool { diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index bea844841..f1bc23541 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -8,6 +8,7 @@ import ( "reflect" "testing" "net/http" + "strings" ) func TestGetResourceURL(t *testing.T) { @@ -1360,7 +1361,7 @@ func TestIsResourcePollingEnabled(t *testing.T) { }, }, } - isResourcePollingEnabled := r.isResourcePollingEnabled(responses, http.StatusAccepted) + isResourcePollingEnabled, _ := r.isResourcePollingEnabled(responses, http.StatusAccepted) Convey("Then the bool returned should be true", func() { So(isResourcePollingEnabled, ShouldBeTrue) }) @@ -1379,7 +1380,7 @@ func TestIsResourcePollingEnabled(t *testing.T) { }, }, } - isResourcePollingEnabled := r.isResourcePollingEnabled(responses, http.StatusAccepted) + isResourcePollingEnabled, _ := r.isResourcePollingEnabled(responses, http.StatusAccepted) Convey("Then the bool returned should be false", func() { So(isResourcePollingEnabled, ShouldBeFalse) }) @@ -1393,7 +1394,7 @@ func TestIsResourcePollingEnabled(t *testing.T) { }, }, } - isResourcePollingEnabled := r.isResourcePollingEnabled(responses, http.StatusAccepted) + isResourcePollingEnabled, _ := r.isResourcePollingEnabled(responses, http.StatusAccepted) Convey("Then bool returned should be false", func() { So(isResourcePollingEnabled, ShouldBeFalse) }) @@ -1401,6 +1402,109 @@ func TestIsResourcePollingEnabled(t *testing.T) { }) } +func TestGetResourcePollTargetStatuses(t *testing.T) { + Convey("Given a resourceInfo", t, func() { + r := resourceInfo{} + Convey("When getResourcePollTargetStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-target-statuses'", func() { + expectedTarget := "deployed" + extensions := spec.Extensions{} + extensions.Add(extTfResourcePollTargetStatuses, expectedTarget) + responses := spec.Responses{ + ResponsesProps: spec.ResponsesProps{ + StatusCodeResponses: map[int]spec.Response{ + http.StatusAccepted: { + VendorExtensible: spec.VendorExtensible{ + Extensions: extensions, + }, + }, + }, + }, + } + statuses, err := r.getResourcePollTargetStatuses(responses.StatusCodeResponses[http.StatusAccepted]) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the status returned should contain", func() { + So(statuses, ShouldContain, expectedTarget) + }) + }) + }) +} + +func TestGetPollingStatuses(t *testing.T) { + Convey("Given a resourceInfo", t, func() { + r := resourceInfo{} + Convey("When getPollingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-target-statuses'", func() { + expectedTarget := "deployed" + extensions := spec.Extensions{} + extensions.Add(extTfResourcePollTargetStatuses, expectedTarget) + responses := spec.Responses{ + ResponsesProps: spec.ResponsesProps{ + StatusCodeResponses: map[int]spec.Response{ + http.StatusAccepted: { + VendorExtensible: spec.VendorExtensible{ + Extensions: extensions, + }, + }, + }, + }, + } + statuses, err := r.getPollingStatuses(responses.StatusCodeResponses[http.StatusAccepted], extTfResourcePollTargetStatuses) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the statuses returned should contain", func() { + So(statuses, ShouldContain, expectedTarget) + }) + }) + + Convey("When getPollingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-target-statuses' containing multiple targets", func() { + expectedTargets := "deployed, completed, done" + extensions := spec.Extensions{} + extensions.Add(extTfResourcePollTargetStatuses, expectedTargets) + responses := spec.Responses{ + ResponsesProps: spec.ResponsesProps{ + StatusCodeResponses: map[int]spec.Response{ + http.StatusAccepted: { + VendorExtensible: spec.VendorExtensible{ + Extensions: extensions, + }, + }, + }, + }, + } + statuses, err := r.getPollingStatuses(responses.StatusCodeResponses[http.StatusAccepted], extTfResourcePollTargetStatuses) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the statuses returned should contain expected targets", func() { + for _, expectedTarget := range strings.Split(expectedTargets, ",") { + So(statuses, ShouldContain, expectedTarget) + } + }) + }) + + Convey("When getPollingStatuses method is called with a response that has does not have a given extension 'x-terraform-resource-poll-target-statuses'", func() { + responses := spec.Responses{ + ResponsesProps: spec.ResponsesProps{ + StatusCodeResponses: map[int]spec.Response{ + http.StatusAccepted: { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{}, + }, + }, + }, + }, + } + _, err := r.getPollingStatuses(responses.StatusCodeResponses[http.StatusAccepted], extTfResourcePollTargetStatuses) + Convey("Then the error returned should NOT be nil", func() { + So(err, ShouldNotBeNil) + }) + }) + }) +} + + func TestShouldIgnoreResource(t *testing.T) { Convey("Given a terraform compliant resource that has a POST operation containing the x-terraform-exclude-resource with value true", t, func() { r := resourceInfo{ From 8fec507dc641d8df49603fb467b042728b77b14f Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 4 Sep 2018 12:01:09 -0700 Subject: [PATCH 11/60] add support for x-terraform-resource-poll-pending-statuses - added unit test --- openapi/resource_info.go | 5 +++++ openapi/resource_info_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/openapi/resource_info.go b/openapi/resource_info.go index d6b5d7b0a..387238984 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -23,6 +23,7 @@ const extTfID = "x-terraform-id" const extTfExcludeResource = "x-terraform-exclude-resource" const extTfResourcePollEnabled= "x-terraform-resource-poll-enabled" const extTfResourcePollTargetStatuses= "x-terraform-resource-poll-target-statuses" +const extTfResourcePollPendingStatuses= "x-terraform-resource-poll-pending-statuses" const idDefaultPropertyName = "id" const statusDefaultPropertyName = "status" @@ -306,6 +307,10 @@ func (r resourceInfo) getResourcePollTargetStatuses(response spec.Response) ([]s return r.getPollingStatuses(response, extTfResourcePollTargetStatuses) } +func (r resourceInfo) getResourcePollPendingStatuses(response spec.Response) ([]string, error) { + return r.getPollingStatuses(response, extTfResourcePollPendingStatuses) +} + func (r resourceInfo) getPollingStatuses(response spec.Response, extension string) ([]string, error) { statuses := []string{} if resourcePollTargets, exists := response.Extensions.GetString(extension); exists { diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index f1bc23541..8401bb29a 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -1431,6 +1431,35 @@ func TestGetResourcePollTargetStatuses(t *testing.T) { }) } +func TestGetResourcePollPendingStatuses(t *testing.T) { + Convey("Given a resourceInfo", t, func() { + r := resourceInfo{} + Convey("When getResourcePollPendingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-pending-statuses'", func() { + expectedStatus := "deploy_pending" + extensions := spec.Extensions{} + extensions.Add(extTfResourcePollPendingStatuses, expectedStatus) + responses := spec.Responses{ + ResponsesProps: spec.ResponsesProps{ + StatusCodeResponses: map[int]spec.Response{ + http.StatusAccepted: { + VendorExtensible: spec.VendorExtensible{ + Extensions: extensions, + }, + }, + }, + }, + } + statuses, err := r.getResourcePollPendingStatuses(responses.StatusCodeResponses[http.StatusAccepted]) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the status returned should contain", func() { + So(statuses, ShouldContain, expectedStatus) + }) + }) + }) +} + func TestGetPollingStatuses(t *testing.T) { Convey("Given a resourceInfo", t, func() { r := resourceInfo{} From 0be9bedeb1a1a43dd3d87e7974b528f01439e34d Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 4 Sep 2018 17:57:35 -0700 Subject: [PATCH 12/60] add support for async create operations - create operation alaways calls read operation after POST. This allows for code re-usability - refactor api lb example to handle better wait times between different statuses and allow for better testing - dep ensure --add github.com/hashicorp/logutils - handle list of target/pending statuses with comma separated values containing spaces and with no spaces --- Gopkg.lock | 17 +- Gopkg.toml | 4 + examples/swaggercodegen/api/api/lb_api.go | 28 +- .../swaggercodegen/api/resources/swagger.yaml | 2 +- examples/swaggercodegen/main.tf | 2 +- openapi/resource_factory.go | 71 +- openapi/resource_info.go | 11 +- openapi/resource_info_test.go | 49 +- vendor/github.com/davecgh/go-spew/.gitignore | 22 + vendor/github.com/davecgh/go-spew/.travis.yml | 28 + vendor/github.com/davecgh/go-spew/LICENSE | 15 + vendor/github.com/davecgh/go-spew/README.md | 201 +++ .../github.com/davecgh/go-spew/cov_report.sh | 22 + .../github.com/davecgh/go-spew/spew/bypass.go | 145 ++ .../davecgh/go-spew/spew/bypasssafe.go | 38 + .../github.com/davecgh/go-spew/spew/common.go | 341 ++++ .../davecgh/go-spew/spew/common_test.go | 298 ++++ .../github.com/davecgh/go-spew/spew/config.go | 306 ++++ vendor/github.com/davecgh/go-spew/spew/doc.go | 211 +++ .../github.com/davecgh/go-spew/spew/dump.go | 509 ++++++ .../davecgh/go-spew/spew/dump_test.go | 1042 +++++++++++ .../davecgh/go-spew/spew/dumpcgo_test.go | 101 ++ .../davecgh/go-spew/spew/dumpnocgo_test.go | 26 + .../davecgh/go-spew/spew/example_test.go | 226 +++ .../github.com/davecgh/go-spew/spew/format.go | 419 +++++ .../davecgh/go-spew/spew/format_test.go | 1558 +++++++++++++++++ .../davecgh/go-spew/spew/internal_test.go | 84 + .../go-spew/spew/internalunsafe_test.go | 101 ++ .../github.com/davecgh/go-spew/spew/spew.go | 148 ++ .../davecgh/go-spew/spew/spew_test.go | 320 ++++ .../davecgh/go-spew/spew/testdata/dumpcgo.go | 82 + .../davecgh/go-spew/test_coverage.txt | 61 + .../github.com/hashicorp/logutils/.gitignore | 22 + vendor/github.com/hashicorp/logutils/LICENSE | 354 ++++ .../github.com/hashicorp/logutils/README.md | 36 + vendor/github.com/hashicorp/logutils/go.mod | 1 + vendor/github.com/hashicorp/logutils/level.go | 81 + .../logutils/level_benchmark_test.go | 37 + .../hashicorp/logutils/level_test.go | 94 + 39 files changed, 7081 insertions(+), 32 deletions(-) create mode 100644 vendor/github.com/davecgh/go-spew/.gitignore create mode 100644 vendor/github.com/davecgh/go-spew/.travis.yml create mode 100644 vendor/github.com/davecgh/go-spew/LICENSE create mode 100644 vendor/github.com/davecgh/go-spew/README.md create mode 100644 vendor/github.com/davecgh/go-spew/cov_report.sh create mode 100644 vendor/github.com/davecgh/go-spew/spew/bypass.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/bypasssafe.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/common.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/common_test.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/config.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/doc.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/dump.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/dump_test.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/dumpcgo_test.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/dumpnocgo_test.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/example_test.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/format.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/format_test.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/internal_test.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/internalunsafe_test.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/spew.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/spew_test.go create mode 100644 vendor/github.com/davecgh/go-spew/spew/testdata/dumpcgo.go create mode 100644 vendor/github.com/davecgh/go-spew/test_coverage.txt create mode 100644 vendor/github.com/hashicorp/logutils/.gitignore create mode 100644 vendor/github.com/hashicorp/logutils/LICENSE create mode 100644 vendor/github.com/hashicorp/logutils/README.md create mode 100644 vendor/github.com/hashicorp/logutils/go.mod create mode 100644 vendor/github.com/hashicorp/logutils/level.go create mode 100644 vendor/github.com/hashicorp/logutils/level_benchmark_test.go create mode 100644 vendor/github.com/hashicorp/logutils/level_test.go diff --git a/Gopkg.lock b/Gopkg.lock index 51a0b2b67..8d66b377f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -99,6 +99,12 @@ revision = "2ee87856327ba09384cabd113bc6b5d174e9ec0f" version = "v3.5.1" +[[projects]] + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" + version = "v1.1.1" + [[projects]] branch = "master" name = "github.com/dikhan/http_goclient" @@ -325,6 +331,12 @@ ] revision = "fa9f258a92500514cc8e9c67020487709df92432" +[[projects]] + name = "github.com/hashicorp/logutils" + packages = ["."] + revision = "a335183dfd075f638afcc820c90591ca3c97eba6" + version = "v1.0.0" + [[projects]] name = "github.com/hashicorp/terraform" packages = [ @@ -334,8 +346,11 @@ "config/module", "dag", "flatmap", + "helper/config", "helper/hashcode", "helper/hilmapstructure", + "helper/logging", + "helper/resource", "helper/schema", "httpclient", "moduledeps", @@ -669,6 +684,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "7ef5221e2869a4ab9782908fa3914192148adc21f31562551b15a2c703902224" + inputs-digest = "c4a49e8a626665d817bd0e3193b0b252ce7f1e345e2c38e2808bce88400c58dc" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index c3d46b2ff..4470c6b35 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -72,3 +72,7 @@ [[constraint]] branch = "master" name = "github.com/iancoleman/strcase" + +[[constraint]] + name = "github.com/hashicorp/logutils" + version = "1.0.0" diff --git a/examples/swaggercodegen/api/api/lb_api.go b/examples/swaggercodegen/api/api/lb_api.go index 901618539..990d6ac1f 100644 --- a/examples/swaggercodegen/api/api/lb_api.go +++ b/examples/swaggercodegen/api/api/lb_api.go @@ -107,20 +107,26 @@ func pretendResourceOperationIsProcessing(lb *Lbv1, pendingStatues []status, com if lb.TimeToProcess > 0 { timeToProcess = lb.TimeToProcess } + var finalStatus status + var inProgressStatuses []status if lb.SimulateFailure { - log.Printf("Simulating failure - timeToProcess = %d", timeToProcess) - time.Sleep(time.Duration(timeToProcess) * time.Second) - updateLBStatus(lb, failureStatus) + log.Println("Simulating failure...") + inProgressStatuses = []status{failureStatus} + finalStatus = failureStatus } else { - waitTimePerPendingStatus := timeToProcess / int32(len(pendingStatues)) - timeToProcessPerStatusDuration := time.Duration(waitTimePerPendingStatus) * time.Second - for _, newStatus := range pendingStatues { - log.Printf("Precessing resource [%s] [%s] - timeToProcess = %ds", lb.Id, newStatus, waitTimePerPendingStatus) - time.Sleep(timeToProcessPerStatusDuration) - updateLBStatus(lb, newStatus) - } - updateLBStatus(lb, completed) + inProgressStatuses = pendingStatues + finalStatus = completed } + waitTimePerPendingStatus := timeToProcess / int32(len(inProgressStatuses) + 1) + timeToProcessPerStatusDuration := time.Duration(waitTimePerPendingStatus) * time.Second + for _, newStatus := range inProgressStatuses { + log.Printf("Precessing resource [%s] [%s => %s] - timeToProcess = %ds", lb.Id, lb.Status, newStatus, waitTimePerPendingStatus) + time.Sleep(timeToProcessPerStatusDuration) + updateLBStatus(lb, newStatus) + } + log.Printf("Precessing resource final status [%s] [%s => %s] - timeToProcess = %ds", lb.Id, lb.Status, finalStatus, waitTimePerPendingStatus) + time.Sleep(timeToProcessPerStatusDuration) + updateLBStatus(lb, finalStatus) } func updateLBStatus(lb *Lbv1, newStatus status) { diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index de856678b..e6f8ea2c5 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -174,7 +174,7 @@ paths: x-terraform-resource-poll-poll-interval: "60s" # [type (string)] - Override MinTimeout/backoff and only poll this often x-terraform-resource-poll-not-found-checks: 2 # [type (int)] - Number of times to allow not found x-terraform-resource-poll-target-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed - x-terraform-resource-poll-pending-statuses: "deploy_pending, deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying + x-terraform-resource-poll-pending-statuses: "deploy_pending,deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" schema: $ref: "#/definitions/LBV1" diff --git a/examples/swaggercodegen/main.tf b/examples/swaggercodegen/main.tf index 6860f2346..69eb8a3b7 100644 --- a/examples/swaggercodegen/main.tf +++ b/examples/swaggercodegen/main.tf @@ -18,6 +18,6 @@ resource "swaggercodegen_cdns_v1" "my_cdn" { resource "swaggercodegen_lbs_v1" "my_lb" { name = "some_name" backends = ["backend1.com"] - time_to_process = 30 # the operation (post,update,delete) will take 30s in the API to complete + time_to_process = 15 # the operation (post,update,delete) will take 15s in the API to complete simulate_failure = false # no failures wished now ;) (post,update,delete) } \ No newline at end of file diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index 8e559e9f0..4be181269 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -8,6 +8,7 @@ import ( "github.com/dikhan/http_goclient" "github.com/dikhan/terraform-provider-openapi/openapi/openapiutils" "github.com/go-openapi/spec" + "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "io/ioutil" "log" @@ -67,7 +68,75 @@ func (r resourceFactory) create(resourceLocalData *schema.ResourceData, i interf if err := r.checkHTTPStatusCode(res, []int{http.StatusOK, http.StatusCreated, http.StatusAccepted}); err != nil { return fmt.Errorf("POST %s failed: %s", resourceURL, err) } - return r.updateLocalState(resourceLocalData, responsePayload) + + err = r.setStateID(resourceLocalData, responsePayload) + if err != nil { + return err + } + log.Printf("[INFO] Resource '%s' ID: %s", r.resourceInfo.name, resourceLocalData.Id()) + + err = r.handlePollingIfConfigured(resourceLocalData, providerConfig, operation.Responses, res.StatusCode) + if err != nil { + return fmt.Errorf("polling mechanism failed after POST %s call with response status code (%d): %s", resourceURL, res.StatusCode, err) + } + + return r.read(resourceLocalData, i) +} + +func (r resourceFactory) handlePollingIfConfigured(resourceLocalData *schema.ResourceData, providerConfig providerConfig, responses *spec.Responses, responseStatusCode int) error { + if pollingEnabled, response := r.resourceInfo.isResourcePollingEnabled(responses, responseStatusCode); pollingEnabled { + targetStatuses, err := r.resourceInfo.getResourcePollTargetStatuses(*response) + if err != nil { + return err + } + pendingStatuses, err := r.resourceInfo.getResourcePollPendingStatuses(*response) + if err != nil { + return err + } + + log.Printf("[DEBUG] target statuses (%s); pending statuses (%s)", targetStatuses, pendingStatuses) + log.Printf("[INFO] Waiting for resource '%s' to reach a completion status (%s)", r.resourceInfo.name, targetStatuses) + + stateConf := &resource.StateChangeConf{ + Pending: pendingStatuses, + Target: targetStatuses, + Refresh: r.resourceStateRefreshFunc(resourceLocalData, providerConfig), + Timeout: resourceLocalData.Timeout(schema.TimeoutCreate), + PollInterval: 5 * time.Second, + MinTimeout: 10 * time.Second, + Delay: 1 * time.Second, + } + + // Wait, catching any errors + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("error waiting for resource to reach a completion status (%s) [valid pending statuses (%s)]: %s", targetStatuses, pendingStatuses, err) + } + } + return nil +} + +func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.ResourceData, providerConfig providerConfig) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + remoteData, err := r.readRemote(resourceLocalData.Id(), providerConfig) + if err != nil { + return nil, "", err + } + + statusIdentifier, err := r.resourceInfo.getStatusIdentifier() + if err != nil { + log.Printf("[WARN] Error on retrieving resource '%s' (%s) when waiting: %s", r.resourceInfo.name, resourceLocalData.Id(), err) + return nil, "", err + } + + value, statusIdentifierPresentInResponse := remoteData[statusIdentifier] + if !statusIdentifierPresentInResponse { + return nil, "", fmt.Errorf("response payload received from GET /%s/%s missing the status identifier field", r.resourceInfo.path, resourceLocalData.Id()) + } + newStatus := value.(string) + log.Printf("[DEBUG] resource '%s' status (%s): %s", r.resourceInfo.name, resourceLocalData.Id(), newStatus) + return remoteData, newStatus, nil + } } func (r resourceFactory) read(resourceLocalData *schema.ResourceData, i interface{}) error { diff --git a/openapi/resource_info.go b/openapi/resource_info.go index 387238984..20e05521e 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -21,9 +21,9 @@ const extTfID = "x-terraform-id" // Operation level extensions const extTfExcludeResource = "x-terraform-exclude-resource" -const extTfResourcePollEnabled= "x-terraform-resource-poll-enabled" -const extTfResourcePollTargetStatuses= "x-terraform-resource-poll-target-statuses" -const extTfResourcePollPendingStatuses= "x-terraform-resource-poll-pending-statuses" +const extTfResourcePollEnabled = "x-terraform-resource-poll-enabled" +const extTfResourcePollTargetStatuses = "x-terraform-resource-poll-target-statuses" +const extTfResourcePollPendingStatuses = "x-terraform-resource-poll-pending-statuses" const idDefaultPropertyName = "id" const statusDefaultPropertyName = "status" @@ -292,7 +292,7 @@ func (r resourceInfo) shouldIgnoreResource() bool { // isResourcePollingEnabled checks whether there is any response code defined for the given responseStatusCode and if so // whether that response contains the extension 'x-terraform-resource-poll-enabled' set to true returning true; // otherwise false is returned -func (r resourceInfo) isResourcePollingEnabled(responses spec.Responses, responseStatusCode int) (bool, *spec.Response) { +func (r resourceInfo) isResourcePollingEnabled(responses *spec.Responses, responseStatusCode int) (bool, *spec.Response) { response, exists := responses.StatusCodeResponses[responseStatusCode] if !exists { return false, nil @@ -314,7 +314,8 @@ func (r resourceInfo) getResourcePollPendingStatuses(response spec.Response) ([] func (r resourceInfo) getPollingStatuses(response spec.Response, extension string) ([]string, error) { statuses := []string{} if resourcePollTargets, exists := response.Extensions.GetString(extension); exists { - statuses = strings.Split(resourcePollTargets, ",") + spaceTrimmedTargerts := strings.Replace(resourcePollTargets, " ", "", -1) + statuses = strings.Split(spaceTrimmedTargerts, ",") } else { return nil, fmt.Errorf("response missing required extension '%s' for the polling mechanism to work", extension) } diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 8401bb29a..7ba724777 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -5,10 +5,10 @@ import ( "github.com/go-openapi/spec" "github.com/hashicorp/terraform/helper/schema" . "github.com/smartystreets/goconvey/convey" - "reflect" - "testing" "net/http" + "reflect" "strings" + "testing" ) func TestGetResourceURL(t *testing.T) { @@ -1350,7 +1350,7 @@ func TestIsResourcePollingEnabled(t *testing.T) { Convey("When isResourcePollingEnabled method is called with a list of responses where one of the reponses matches the response status received and has the 'x-terraform-resource-poll-enabled' extension set to true", func() { extensions := spec.Extensions{} extensions.Add(extTfResourcePollEnabled, true) - responses := spec.Responses{ + responses := &spec.Responses{ ResponsesProps: spec.ResponsesProps{ StatusCodeResponses: map[int]spec.Response{ http.StatusAccepted: { @@ -1369,7 +1369,7 @@ func TestIsResourcePollingEnabled(t *testing.T) { Convey("When isResourcePollingEnabled method is called with a list of responses where one of the reponses matches the response status received and has the 'x-terraform-resource-poll-enabled' extension set to false", func() { extensions := spec.Extensions{} extensions.Add(extTfResourcePollEnabled, false) - responses := spec.Responses{ + responses := &spec.Responses{ ResponsesProps: spec.ResponsesProps{ StatusCodeResponses: map[int]spec.Response{ http.StatusAccepted: { @@ -1380,21 +1380,20 @@ func TestIsResourcePollingEnabled(t *testing.T) { }, }, } - isResourcePollingEnabled, _ := r.isResourcePollingEnabled(responses, http.StatusAccepted) + isResourcePollingEnabled, _ := r.isResourcePollingEnabled(responses, http.StatusAccepted) Convey("Then the bool returned should be false", func() { So(isResourcePollingEnabled, ShouldBeFalse) }) }) Convey("When isResourcePollingEnabled method is called with list of responses where non of the codes match the given response http code", func() { - responses := spec.Responses{ + responses := &spec.Responses{ ResponsesProps: spec.ResponsesProps{ StatusCodeResponses: map[int]spec.Response{ - http.StatusOK: { - }, + http.StatusOK: {}, }, }, } - isResourcePollingEnabled, _ := r.isResourcePollingEnabled(responses, http.StatusAccepted) + isResourcePollingEnabled, _ := r.isResourcePollingEnabled(responses, http.StatusAccepted) Convey("Then bool returned should be false", func() { So(isResourcePollingEnabled, ShouldBeFalse) }) @@ -1409,7 +1408,7 @@ func TestGetResourcePollTargetStatuses(t *testing.T) { expectedTarget := "deployed" extensions := spec.Extensions{} extensions.Add(extTfResourcePollTargetStatuses, expectedTarget) - responses := spec.Responses{ + responses := &spec.Responses{ ResponsesProps: spec.ResponsesProps{ StatusCodeResponses: map[int]spec.Response{ http.StatusAccepted: { @@ -1487,7 +1486,7 @@ func TestGetPollingStatuses(t *testing.T) { }) }) - Convey("When getPollingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-target-statuses' containing multiple targets", func() { + Convey("When getPollingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-target-statuses' containing multiple targets (comma separated with spaces)", func() { expectedTargets := "deployed, completed, done" extensions := spec.Extensions{} extensions.Add(extTfResourcePollTargetStatuses, expectedTargets) @@ -1506,6 +1505,33 @@ func TestGetPollingStatuses(t *testing.T) { Convey("Then the error returned should be nil", func() { So(err, ShouldBeNil) }) + Convey("Then the statuses returned should contain expected targets", func() { + // the expected Targets are a list of targets with no spaces whatsoever, hence why the removal of spaces + for _, expectedTarget := range strings.Split(strings.Replace(expectedTargets, " ", "", -1), ",") { + So(statuses, ShouldContain, expectedTarget) + } + }) + }) + + Convey("When getPollingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-target-statuses' containing multiple targets (comma separated with no spaces)", func() { + expectedTargets := "deployed,completed,done" + extensions := spec.Extensions{} + extensions.Add(extTfResourcePollTargetStatuses, expectedTargets) + responses := spec.Responses{ + ResponsesProps: spec.ResponsesProps{ + StatusCodeResponses: map[int]spec.Response{ + http.StatusAccepted: { + VendorExtensible: spec.VendorExtensible{ + Extensions: extensions, + }, + }, + }, + }, + } + statuses, err := r.getPollingStatuses(responses.StatusCodeResponses[http.StatusAccepted], extTfResourcePollTargetStatuses) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) Convey("Then the statuses returned should contain expected targets", func() { for _, expectedTarget := range strings.Split(expectedTargets, ",") { So(statuses, ShouldContain, expectedTarget) @@ -1533,7 +1559,6 @@ func TestGetPollingStatuses(t *testing.T) { }) } - func TestShouldIgnoreResource(t *testing.T) { Convey("Given a terraform compliant resource that has a POST operation containing the x-terraform-exclude-resource with value true", t, func() { r := resourceInfo{ diff --git a/vendor/github.com/davecgh/go-spew/.gitignore b/vendor/github.com/davecgh/go-spew/.gitignore new file mode 100644 index 000000000..00268614f --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/github.com/davecgh/go-spew/.travis.yml b/vendor/github.com/davecgh/go-spew/.travis.yml new file mode 100644 index 000000000..1f4cbf542 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/.travis.yml @@ -0,0 +1,28 @@ +language: go +go_import_path: github.com/davecgh/go-spew +go: + - 1.6.x + - 1.7.x + - 1.8.x + - 1.9.x + - 1.10.x + - tip +sudo: false +install: + - go get -v github.com/alecthomas/gometalinter + - gometalinter --install +script: + - export PATH=$PATH:$HOME/gopath/bin + - export GORACE="halt_on_error=1" + - test -z "$(gometalinter --disable-all + --enable=gofmt + --enable=golint + --enable=vet + --enable=gosimple + --enable=unconvert + --deadline=4m ./spew | tee /dev/stderr)" + - go test -v -race -tags safe ./spew + - go test -v -race -tags testcgo ./spew -covermode=atomic -coverprofile=profile.cov +after_success: + - go get -v github.com/mattn/goveralls + - goveralls -coverprofile=profile.cov -service=travis-ci diff --git a/vendor/github.com/davecgh/go-spew/LICENSE b/vendor/github.com/davecgh/go-spew/LICENSE new file mode 100644 index 000000000..bc52e96f2 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2012-2016 Dave Collins + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/vendor/github.com/davecgh/go-spew/README.md b/vendor/github.com/davecgh/go-spew/README.md new file mode 100644 index 000000000..f6ed02c3b --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/README.md @@ -0,0 +1,201 @@ +go-spew +======= + +[![Build Status](https://img.shields.io/travis/davecgh/go-spew.svg)](https://travis-ci.org/davecgh/go-spew) +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![Coverage Status](https://img.shields.io/coveralls/davecgh/go-spew.svg)](https://coveralls.io/r/davecgh/go-spew?branch=master) + +Go-spew implements a deep pretty printer for Go data structures to aid in +debugging. A comprehensive suite of tests with 100% test coverage is provided +to ensure proper functionality. See `test_coverage.txt` for the gocov coverage +report. Go-spew is licensed under the liberal ISC license, so it may be used in +open source or commercial projects. + +If you're interested in reading about how this package came to life and some +of the challenges involved in providing a deep pretty printer, there is a blog +post about it +[here](https://web.archive.org/web/20160304013555/https://blog.cyphertite.com/go-spew-a-journey-into-dumping-go-data-structures/). + +## Documentation + +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/davecgh/go-spew/spew) + +Full `go doc` style documentation for the project can be viewed online without +installing this package by using the excellent GoDoc site here: +http://godoc.org/github.com/davecgh/go-spew/spew + +You can also view the documentation locally once the package is installed with +the `godoc` tool by running `godoc -http=":6060"` and pointing your browser to +http://localhost:6060/pkg/github.com/davecgh/go-spew/spew + +## Installation + +```bash +$ go get -u github.com/davecgh/go-spew/spew +``` + +## Quick Start + +Add this import line to the file you're working in: + +```Go +import "github.com/davecgh/go-spew/spew" +``` + +To dump a variable with full newlines, indentation, type, and pointer +information use Dump, Fdump, or Sdump: + +```Go +spew.Dump(myVar1, myVar2, ...) +spew.Fdump(someWriter, myVar1, myVar2, ...) +str := spew.Sdump(myVar1, myVar2, ...) +``` + +Alternatively, if you would prefer to use format strings with a compacted inline +printing style, use the convenience wrappers Printf, Fprintf, etc with %v (most +compact), %+v (adds pointer addresses), %#v (adds types), or %#+v (adds types +and pointer addresses): + +```Go +spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) +spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) +spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) +spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) +``` + +## Debugging a Web Application Example + +Here is an example of how you can use `spew.Sdump()` to help debug a web application. Please be sure to wrap your output using the `html.EscapeString()` function for safety reasons. You should also only use this debugging technique in a development environment, never in production. + +```Go +package main + +import ( + "fmt" + "html" + "net/http" + + "github.com/davecgh/go-spew/spew" +) + +func handler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, "Hi there, %s!", r.URL.Path[1:]) + fmt.Fprintf(w, "") +} + +func main() { + http.HandleFunc("/", handler) + http.ListenAndServe(":8080", nil) +} +``` + +## Sample Dump Output + +``` +(main.Foo) { + unexportedField: (*main.Bar)(0xf84002e210)({ + flag: (main.Flag) flagTwo, + data: (uintptr) + }), + ExportedField: (map[interface {}]interface {}) { + (string) "one": (bool) true + } +} +([]uint8) { + 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | + 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| + 00000020 31 32 |12| +} +``` + +## Sample Formatter Output + +Double pointer to a uint8: +``` + %v: <**>5 + %+v: <**>(0xf8400420d0->0xf8400420c8)5 + %#v: (**uint8)5 + %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 +``` + +Pointer to circular struct with a uint8 field and a pointer to itself: +``` + %v: <*>{1 <*>} + %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)} + %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)} + %#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)} +``` + +## Configuration Options + +Configuration of spew is handled by fields in the ConfigState type. For +convenience, all of the top-level functions use a global state available via the +spew.Config global. + +It is also possible to create a ConfigState instance that provides methods +equivalent to the top-level functions. This allows concurrent configuration +options. See the ConfigState documentation for more details. + +``` +* Indent + String to use for each indentation level for Dump functions. + It is a single space by default. A popular alternative is "\t". + +* MaxDepth + Maximum number of levels to descend into nested data structures. + There is no limit by default. + +* DisableMethods + Disables invocation of error and Stringer interface methods. + Method invocation is enabled by default. + +* DisablePointerMethods + Disables invocation of error and Stringer interface methods on types + which only accept pointer receivers from non-pointer variables. This option + relies on access to the unsafe package, so it will not have any effect when + running in environments without access to the unsafe package such as Google + App Engine or with the "safe" build tag specified. + Pointer method invocation is enabled by default. + +* DisablePointerAddresses + DisablePointerAddresses specifies whether to disable the printing of + pointer addresses. This is useful when diffing data structures in tests. + +* DisableCapacities + DisableCapacities specifies whether to disable the printing of capacities + for arrays, slices, maps and channels. This is useful when diffing data + structures in tests. + +* ContinueOnMethod + Enables recursion into types after invoking error and Stringer interface + methods. Recursion after method invocation is disabled by default. + +* SortKeys + Specifies map keys should be sorted before being printed. Use + this to have a more deterministic, diffable output. Note that + only native types (bool, int, uint, floats, uintptr and string) + and types which implement error or Stringer interfaces are supported, + with other types sorted according to the reflect.Value.String() output + which guarantees display stability. Natural map order is used by + default. + +* SpewKeys + SpewKeys specifies that, as a last resort attempt, map keys should be + spewed to strings and sorted by those strings. This is only considered + if SortKeys is true. + +``` + +## Unsafe Package Dependency + +This package relies on the unsafe package to perform some of the more advanced +features, however it also supports a "limited" mode which allows it to work in +environments where the unsafe package is not available. By default, it will +operate in this mode on Google App Engine and when compiled with GopherJS. The +"safe" build tag may also be specified to force the package to build without +using the unsafe package. + +## License + +Go-spew is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/vendor/github.com/davecgh/go-spew/cov_report.sh b/vendor/github.com/davecgh/go-spew/cov_report.sh new file mode 100644 index 000000000..9579497e4 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/cov_report.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# This script uses gocov to generate a test coverage report. +# The gocov tool my be obtained with the following command: +# go get github.com/axw/gocov/gocov +# +# It will be installed to $GOPATH/bin, so ensure that location is in your $PATH. + +# Check for gocov. +if ! type gocov >/dev/null 2>&1; then + echo >&2 "This script requires the gocov tool." + echo >&2 "You may obtain it with the following command:" + echo >&2 "go get github.com/axw/gocov/gocov" + exit 1 +fi + +# Only run the cgo tests if gcc is installed. +if type gcc >/dev/null 2>&1; then + (cd spew && gocov test -tags testcgo | gocov report) +else + (cd spew && gocov test | gocov report) +fi diff --git a/vendor/github.com/davecgh/go-spew/spew/bypass.go b/vendor/github.com/davecgh/go-spew/spew/bypass.go new file mode 100644 index 000000000..792994785 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/bypass.go @@ -0,0 +1,145 @@ +// Copyright (c) 2015-2016 Dave Collins +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when the code is not running on Google App Engine, compiled by GopherJS, and +// "-tags safe" is not added to the go build command line. The "disableunsafe" +// tag is deprecated and thus should not be used. +// Go versions prior to 1.4 are disabled because they use a different layout +// for interfaces which make the implementation of unsafeReflectValue more complex. +// +build !js,!appengine,!safe,!disableunsafe,go1.4 + +package spew + +import ( + "reflect" + "unsafe" +) + +const ( + // UnsafeDisabled is a build-time constant which specifies whether or + // not access to the unsafe package is available. + UnsafeDisabled = false + + // ptrSize is the size of a pointer on the current arch. + ptrSize = unsafe.Sizeof((*byte)(nil)) +) + +type flag uintptr + +var ( + // flagRO indicates whether the value field of a reflect.Value + // is read-only. + flagRO flag + + // flagAddr indicates whether the address of the reflect.Value's + // value may be taken. + flagAddr flag +) + +// flagKindMask holds the bits that make up the kind +// part of the flags field. In all the supported versions, +// it is in the lower 5 bits. +const flagKindMask = flag(0x1f) + +// Different versions of Go have used different +// bit layouts for the flags type. This table +// records the known combinations. +var okFlags = []struct { + ro, addr flag +}{{ + // From Go 1.4 to 1.5 + ro: 1 << 5, + addr: 1 << 7, +}, { + // Up to Go tip. + ro: 1<<5 | 1<<6, + addr: 1 << 8, +}} + +var flagValOffset = func() uintptr { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") + } + return field.Offset +}() + +// flagField returns a pointer to the flag field of a reflect.Value. +func flagField(v *reflect.Value) *flag { + return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset)) +} + +// unsafeReflectValue converts the passed reflect.Value into a one that bypasses +// the typical safety restrictions preventing access to unaddressable and +// unexported data. It works by digging the raw pointer to the underlying +// value out of the protected value and generating a new unprotected (unsafe) +// reflect.Value to it. +// +// This allows us to check for implementations of the Stringer and error +// interfaces to be used for pretty printing ordinarily unaddressable and +// inaccessible values such as unexported struct fields. +func unsafeReflectValue(v reflect.Value) reflect.Value { + if !v.IsValid() || (v.CanInterface() && v.CanAddr()) { + return v + } + flagFieldPtr := flagField(&v) + *flagFieldPtr &^= flagRO + *flagFieldPtr |= flagAddr + return v +} + +// Sanity checks against future reflect package changes +// to the type or semantics of the Value.flag field. +func init() { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") + } + if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() { + panic("reflect.Value flag field has changed kind") + } + type t0 int + var t struct { + A t0 + // t0 will have flagEmbedRO set. + t0 + // a will have flagStickyRO set + a t0 + } + vA := reflect.ValueOf(t).FieldByName("A") + va := reflect.ValueOf(t).FieldByName("a") + vt0 := reflect.ValueOf(t).FieldByName("t0") + + // Infer flagRO from the difference between the flags + // for the (otherwise identical) fields in t. + flagPublic := *flagField(&vA) + flagWithRO := *flagField(&va) | *flagField(&vt0) + flagRO = flagPublic ^ flagWithRO + + // Infer flagAddr from the difference between a value + // taken from a pointer and not. + vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A") + flagNoPtr := *flagField(&vA) + flagPtr := *flagField(&vPtrA) + flagAddr = flagNoPtr ^ flagPtr + + // Check that the inferred flags tally with one of the known versions. + for _, f := range okFlags { + if flagRO == f.ro && flagAddr == f.addr { + return + } + } + panic("reflect.Value read-only flag has changed semantics") +} diff --git a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go new file mode 100644 index 000000000..205c28d68 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go @@ -0,0 +1,38 @@ +// Copyright (c) 2015-2016 Dave Collins +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when the code is running on Google App Engine, compiled by GopherJS, or +// "-tags safe" is added to the go build command line. The "disableunsafe" +// tag is deprecated and thus should not be used. +// +build js appengine safe disableunsafe !go1.4 + +package spew + +import "reflect" + +const ( + // UnsafeDisabled is a build-time constant which specifies whether or + // not access to the unsafe package is available. + UnsafeDisabled = true +) + +// unsafeReflectValue typically converts the passed reflect.Value into a one +// that bypasses the typical safety restrictions preventing access to +// unaddressable and unexported data. However, doing this relies on access to +// the unsafe package. This is a stub version which simply returns the passed +// reflect.Value when the unsafe package is not available. +func unsafeReflectValue(v reflect.Value) reflect.Value { + return v +} diff --git a/vendor/github.com/davecgh/go-spew/spew/common.go b/vendor/github.com/davecgh/go-spew/spew/common.go new file mode 100644 index 000000000..1be8ce945 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/common.go @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "io" + "reflect" + "sort" + "strconv" +) + +// Some constants in the form of bytes to avoid string overhead. This mirrors +// the technique used in the fmt package. +var ( + panicBytes = []byte("(PANIC=") + plusBytes = []byte("+") + iBytes = []byte("i") + trueBytes = []byte("true") + falseBytes = []byte("false") + interfaceBytes = []byte("(interface {})") + commaNewlineBytes = []byte(",\n") + newlineBytes = []byte("\n") + openBraceBytes = []byte("{") + openBraceNewlineBytes = []byte("{\n") + closeBraceBytes = []byte("}") + asteriskBytes = []byte("*") + colonBytes = []byte(":") + colonSpaceBytes = []byte(": ") + openParenBytes = []byte("(") + closeParenBytes = []byte(")") + spaceBytes = []byte(" ") + pointerChainBytes = []byte("->") + nilAngleBytes = []byte("") + maxNewlineBytes = []byte("\n") + maxShortBytes = []byte("") + circularBytes = []byte("") + circularShortBytes = []byte("") + invalidAngleBytes = []byte("") + openBracketBytes = []byte("[") + closeBracketBytes = []byte("]") + percentBytes = []byte("%") + precisionBytes = []byte(".") + openAngleBytes = []byte("<") + closeAngleBytes = []byte(">") + openMapBytes = []byte("map[") + closeMapBytes = []byte("]") + lenEqualsBytes = []byte("len=") + capEqualsBytes = []byte("cap=") +) + +// hexDigits is used to map a decimal value to a hex digit. +var hexDigits = "0123456789abcdef" + +// catchPanic handles any panics that might occur during the handleMethods +// calls. +func catchPanic(w io.Writer, v reflect.Value) { + if err := recover(); err != nil { + w.Write(panicBytes) + fmt.Fprintf(w, "%v", err) + w.Write(closeParenBytes) + } +} + +// handleMethods attempts to call the Error and String methods on the underlying +// type the passed reflect.Value represents and outputes the result to Writer w. +// +// It handles panics in any called methods by catching and displaying the error +// as the formatted value. +func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) { + // We need an interface to check if the type implements the error or + // Stringer interface. However, the reflect package won't give us an + // interface on certain things like unexported struct fields in order + // to enforce visibility rules. We use unsafe, when it's available, + // to bypass these restrictions since this package does not mutate the + // values. + if !v.CanInterface() { + if UnsafeDisabled { + return false + } + + v = unsafeReflectValue(v) + } + + // Choose whether or not to do error and Stringer interface lookups against + // the base type or a pointer to the base type depending on settings. + // Technically calling one of these methods with a pointer receiver can + // mutate the value, however, types which choose to satisify an error or + // Stringer interface with a pointer receiver should not be mutating their + // state inside these interface methods. + if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() { + v = unsafeReflectValue(v) + } + if v.CanAddr() { + v = v.Addr() + } + + // Is it an error or Stringer? + switch iface := v.Interface().(type) { + case error: + defer catchPanic(w, v) + if cs.ContinueOnMethod { + w.Write(openParenBytes) + w.Write([]byte(iface.Error())) + w.Write(closeParenBytes) + w.Write(spaceBytes) + return false + } + + w.Write([]byte(iface.Error())) + return true + + case fmt.Stringer: + defer catchPanic(w, v) + if cs.ContinueOnMethod { + w.Write(openParenBytes) + w.Write([]byte(iface.String())) + w.Write(closeParenBytes) + w.Write(spaceBytes) + return false + } + w.Write([]byte(iface.String())) + return true + } + return false +} + +// printBool outputs a boolean value as true or false to Writer w. +func printBool(w io.Writer, val bool) { + if val { + w.Write(trueBytes) + } else { + w.Write(falseBytes) + } +} + +// printInt outputs a signed integer value to Writer w. +func printInt(w io.Writer, val int64, base int) { + w.Write([]byte(strconv.FormatInt(val, base))) +} + +// printUint outputs an unsigned integer value to Writer w. +func printUint(w io.Writer, val uint64, base int) { + w.Write([]byte(strconv.FormatUint(val, base))) +} + +// printFloat outputs a floating point value using the specified precision, +// which is expected to be 32 or 64bit, to Writer w. +func printFloat(w io.Writer, val float64, precision int) { + w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision))) +} + +// printComplex outputs a complex value using the specified float precision +// for the real and imaginary parts to Writer w. +func printComplex(w io.Writer, c complex128, floatPrecision int) { + r := real(c) + w.Write(openParenBytes) + w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision))) + i := imag(c) + if i >= 0 { + w.Write(plusBytes) + } + w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision))) + w.Write(iBytes) + w.Write(closeParenBytes) +} + +// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x' +// prefix to Writer w. +func printHexPtr(w io.Writer, p uintptr) { + // Null pointer. + num := uint64(p) + if num == 0 { + w.Write(nilAngleBytes) + return + } + + // Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix + buf := make([]byte, 18) + + // It's simpler to construct the hex string right to left. + base := uint64(16) + i := len(buf) - 1 + for num >= base { + buf[i] = hexDigits[num%base] + num /= base + i-- + } + buf[i] = hexDigits[num] + + // Add '0x' prefix. + i-- + buf[i] = 'x' + i-- + buf[i] = '0' + + // Strip unused leading bytes. + buf = buf[i:] + w.Write(buf) +} + +// valuesSorter implements sort.Interface to allow a slice of reflect.Value +// elements to be sorted. +type valuesSorter struct { + values []reflect.Value + strings []string // either nil or same len and values + cs *ConfigState +} + +// newValuesSorter initializes a valuesSorter instance, which holds a set of +// surrogate keys on which the data should be sorted. It uses flags in +// ConfigState to decide if and how to populate those surrogate keys. +func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface { + vs := &valuesSorter{values: values, cs: cs} + if canSortSimply(vs.values[0].Kind()) { + return vs + } + if !cs.DisableMethods { + vs.strings = make([]string, len(values)) + for i := range vs.values { + b := bytes.Buffer{} + if !handleMethods(cs, &b, vs.values[i]) { + vs.strings = nil + break + } + vs.strings[i] = b.String() + } + } + if vs.strings == nil && cs.SpewKeys { + vs.strings = make([]string, len(values)) + for i := range vs.values { + vs.strings[i] = Sprintf("%#v", vs.values[i].Interface()) + } + } + return vs +} + +// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted +// directly, or whether it should be considered for sorting by surrogate keys +// (if the ConfigState allows it). +func canSortSimply(kind reflect.Kind) bool { + // This switch parallels valueSortLess, except for the default case. + switch kind { + case reflect.Bool: + return true + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return true + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return true + case reflect.Float32, reflect.Float64: + return true + case reflect.String: + return true + case reflect.Uintptr: + return true + case reflect.Array: + return true + } + return false +} + +// Len returns the number of values in the slice. It is part of the +// sort.Interface implementation. +func (s *valuesSorter) Len() int { + return len(s.values) +} + +// Swap swaps the values at the passed indices. It is part of the +// sort.Interface implementation. +func (s *valuesSorter) Swap(i, j int) { + s.values[i], s.values[j] = s.values[j], s.values[i] + if s.strings != nil { + s.strings[i], s.strings[j] = s.strings[j], s.strings[i] + } +} + +// valueSortLess returns whether the first value should sort before the second +// value. It is used by valueSorter.Less as part of the sort.Interface +// implementation. +func valueSortLess(a, b reflect.Value) bool { + switch a.Kind() { + case reflect.Bool: + return !a.Bool() && b.Bool() + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return a.Int() < b.Int() + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return a.Uint() < b.Uint() + case reflect.Float32, reflect.Float64: + return a.Float() < b.Float() + case reflect.String: + return a.String() < b.String() + case reflect.Uintptr: + return a.Uint() < b.Uint() + case reflect.Array: + // Compare the contents of both arrays. + l := a.Len() + for i := 0; i < l; i++ { + av := a.Index(i) + bv := b.Index(i) + if av.Interface() == bv.Interface() { + continue + } + return valueSortLess(av, bv) + } + } + return a.String() < b.String() +} + +// Less returns whether the value at index i should sort before the +// value at index j. It is part of the sort.Interface implementation. +func (s *valuesSorter) Less(i, j int) bool { + if s.strings == nil { + return valueSortLess(s.values[i], s.values[j]) + } + return s.strings[i] < s.strings[j] +} + +// sortValues is a sort function that handles both native types and any type that +// can be converted to error or Stringer. Other inputs are sorted according to +// their Value.String() value to ensure display stability. +func sortValues(values []reflect.Value, cs *ConfigState) { + if len(values) == 0 { + return + } + sort.Sort(newValuesSorter(values, cs)) +} diff --git a/vendor/github.com/davecgh/go-spew/spew/common_test.go b/vendor/github.com/davecgh/go-spew/spew/common_test.go new file mode 100644 index 000000000..0f5ce47dc --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/common_test.go @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew_test + +import ( + "fmt" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" +) + +// custom type to test Stinger interface on non-pointer receiver. +type stringer string + +// String implements the Stringer interface for testing invocation of custom +// stringers on types with non-pointer receivers. +func (s stringer) String() string { + return "stringer " + string(s) +} + +// custom type to test Stinger interface on pointer receiver. +type pstringer string + +// String implements the Stringer interface for testing invocation of custom +// stringers on types with only pointer receivers. +func (s *pstringer) String() string { + return "stringer " + string(*s) +} + +// xref1 and xref2 are cross referencing structs for testing circular reference +// detection. +type xref1 struct { + ps2 *xref2 +} +type xref2 struct { + ps1 *xref1 +} + +// indirCir1, indirCir2, and indirCir3 are used to generate an indirect circular +// reference for testing detection. +type indirCir1 struct { + ps2 *indirCir2 +} +type indirCir2 struct { + ps3 *indirCir3 +} +type indirCir3 struct { + ps1 *indirCir1 +} + +// embed is used to test embedded structures. +type embed struct { + a string +} + +// embedwrap is used to test embedded structures. +type embedwrap struct { + *embed + e *embed +} + +// panicer is used to intentionally cause a panic for testing spew properly +// handles them +type panicer int + +func (p panicer) String() string { + panic("test panic") +} + +// customError is used to test custom error interface invocation. +type customError int + +func (e customError) Error() string { + return fmt.Sprintf("error: %d", int(e)) +} + +// stringizeWants converts a slice of wanted test output into a format suitable +// for a test error message. +func stringizeWants(wants []string) string { + s := "" + for i, want := range wants { + if i > 0 { + s += fmt.Sprintf("want%d: %s", i+1, want) + } else { + s += "want: " + want + } + } + return s +} + +// testFailed returns whether or not a test failed by checking if the result +// of the test is in the slice of wanted strings. +func testFailed(result string, wants []string) bool { + for _, want := range wants { + if result == want { + return false + } + } + return true +} + +type sortableStruct struct { + x int +} + +func (ss sortableStruct) String() string { + return fmt.Sprintf("ss.%d", ss.x) +} + +type unsortableStruct struct { + x int +} + +type sortTestCase struct { + input []reflect.Value + expected []reflect.Value +} + +func helpTestSortValues(tests []sortTestCase, cs *spew.ConfigState, t *testing.T) { + getInterfaces := func(values []reflect.Value) []interface{} { + interfaces := []interface{}{} + for _, v := range values { + interfaces = append(interfaces, v.Interface()) + } + return interfaces + } + + for _, test := range tests { + spew.SortValues(test.input, cs) + // reflect.DeepEqual cannot really make sense of reflect.Value, + // probably because of all the pointer tricks. For instance, + // v(2.0) != v(2.0) on a 32-bits system. Turn them into interface{} + // instead. + input := getInterfaces(test.input) + expected := getInterfaces(test.expected) + if !reflect.DeepEqual(input, expected) { + t.Errorf("Sort mismatch:\n %v != %v", input, expected) + } + } +} + +// TestSortValues ensures the sort functionality for relect.Value based sorting +// works as intended. +func TestSortValues(t *testing.T) { + v := reflect.ValueOf + + a := v("a") + b := v("b") + c := v("c") + embedA := v(embed{"a"}) + embedB := v(embed{"b"}) + embedC := v(embed{"c"}) + tests := []sortTestCase{ + // No values. + { + []reflect.Value{}, + []reflect.Value{}, + }, + // Bools. + { + []reflect.Value{v(false), v(true), v(false)}, + []reflect.Value{v(false), v(false), v(true)}, + }, + // Ints. + { + []reflect.Value{v(2), v(1), v(3)}, + []reflect.Value{v(1), v(2), v(3)}, + }, + // Uints. + { + []reflect.Value{v(uint8(2)), v(uint8(1)), v(uint8(3))}, + []reflect.Value{v(uint8(1)), v(uint8(2)), v(uint8(3))}, + }, + // Floats. + { + []reflect.Value{v(2.0), v(1.0), v(3.0)}, + []reflect.Value{v(1.0), v(2.0), v(3.0)}, + }, + // Strings. + { + []reflect.Value{b, a, c}, + []reflect.Value{a, b, c}, + }, + // Array + { + []reflect.Value{v([3]int{3, 2, 1}), v([3]int{1, 3, 2}), v([3]int{1, 2, 3})}, + []reflect.Value{v([3]int{1, 2, 3}), v([3]int{1, 3, 2}), v([3]int{3, 2, 1})}, + }, + // Uintptrs. + { + []reflect.Value{v(uintptr(2)), v(uintptr(1)), v(uintptr(3))}, + []reflect.Value{v(uintptr(1)), v(uintptr(2)), v(uintptr(3))}, + }, + // SortableStructs. + { + // Note: not sorted - DisableMethods is set. + []reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})}, + []reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})}, + }, + // UnsortableStructs. + { + // Note: not sorted - SpewKeys is false. + []reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})}, + []reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})}, + }, + // Invalid. + { + []reflect.Value{embedB, embedA, embedC}, + []reflect.Value{embedB, embedA, embedC}, + }, + } + cs := spew.ConfigState{DisableMethods: true, SpewKeys: false} + helpTestSortValues(tests, &cs, t) +} + +// TestSortValuesWithMethods ensures the sort functionality for relect.Value +// based sorting works as intended when using string methods. +func TestSortValuesWithMethods(t *testing.T) { + v := reflect.ValueOf + + a := v("a") + b := v("b") + c := v("c") + tests := []sortTestCase{ + // Ints. + { + []reflect.Value{v(2), v(1), v(3)}, + []reflect.Value{v(1), v(2), v(3)}, + }, + // Strings. + { + []reflect.Value{b, a, c}, + []reflect.Value{a, b, c}, + }, + // SortableStructs. + { + []reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})}, + []reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})}, + }, + // UnsortableStructs. + { + // Note: not sorted - SpewKeys is false. + []reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})}, + []reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})}, + }, + } + cs := spew.ConfigState{DisableMethods: false, SpewKeys: false} + helpTestSortValues(tests, &cs, t) +} + +// TestSortValuesWithSpew ensures the sort functionality for relect.Value +// based sorting works as intended when using spew to stringify keys. +func TestSortValuesWithSpew(t *testing.T) { + v := reflect.ValueOf + + a := v("a") + b := v("b") + c := v("c") + tests := []sortTestCase{ + // Ints. + { + []reflect.Value{v(2), v(1), v(3)}, + []reflect.Value{v(1), v(2), v(3)}, + }, + // Strings. + { + []reflect.Value{b, a, c}, + []reflect.Value{a, b, c}, + }, + // SortableStructs. + { + []reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})}, + []reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})}, + }, + // UnsortableStructs. + { + []reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})}, + []reflect.Value{v(unsortableStruct{1}), v(unsortableStruct{2}), v(unsortableStruct{3})}, + }, + } + cs := spew.ConfigState{DisableMethods: true, SpewKeys: true} + helpTestSortValues(tests, &cs, t) +} diff --git a/vendor/github.com/davecgh/go-spew/spew/config.go b/vendor/github.com/davecgh/go-spew/spew/config.go new file mode 100644 index 000000000..2e3d22f31 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/config.go @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "io" + "os" +) + +// ConfigState houses the configuration options used by spew to format and +// display values. There is a global instance, Config, that is used to control +// all top-level Formatter and Dump functionality. Each ConfigState instance +// provides methods equivalent to the top-level functions. +// +// The zero value for ConfigState provides no indentation. You would typically +// want to set it to a space or a tab. +// +// Alternatively, you can use NewDefaultConfig to get a ConfigState instance +// with default settings. See the documentation of NewDefaultConfig for default +// values. +type ConfigState struct { + // Indent specifies the string to use for each indentation level. The + // global config instance that all top-level functions use set this to a + // single space by default. If you would like more indentation, you might + // set this to a tab with "\t" or perhaps two spaces with " ". + Indent string + + // MaxDepth controls the maximum number of levels to descend into nested + // data structures. The default, 0, means there is no limit. + // + // NOTE: Circular data structures are properly detected, so it is not + // necessary to set this value unless you specifically want to limit deeply + // nested data structures. + MaxDepth int + + // DisableMethods specifies whether or not error and Stringer interfaces are + // invoked for types that implement them. + DisableMethods bool + + // DisablePointerMethods specifies whether or not to check for and invoke + // error and Stringer interfaces on types which only accept a pointer + // receiver when the current type is not a pointer. + // + // NOTE: This might be an unsafe action since calling one of these methods + // with a pointer receiver could technically mutate the value, however, + // in practice, types which choose to satisify an error or Stringer + // interface with a pointer receiver should not be mutating their state + // inside these interface methods. As a result, this option relies on + // access to the unsafe package, so it will not have any effect when + // running in environments without access to the unsafe package such as + // Google App Engine or with the "safe" build tag specified. + DisablePointerMethods bool + + // DisablePointerAddresses specifies whether to disable the printing of + // pointer addresses. This is useful when diffing data structures in tests. + DisablePointerAddresses bool + + // DisableCapacities specifies whether to disable the printing of capacities + // for arrays, slices, maps and channels. This is useful when diffing + // data structures in tests. + DisableCapacities bool + + // ContinueOnMethod specifies whether or not recursion should continue once + // a custom error or Stringer interface is invoked. The default, false, + // means it will print the results of invoking the custom error or Stringer + // interface and return immediately instead of continuing to recurse into + // the internals of the data type. + // + // NOTE: This flag does not have any effect if method invocation is disabled + // via the DisableMethods or DisablePointerMethods options. + ContinueOnMethod bool + + // SortKeys specifies map keys should be sorted before being printed. Use + // this to have a more deterministic, diffable output. Note that only + // native types (bool, int, uint, floats, uintptr and string) and types + // that support the error or Stringer interfaces (if methods are + // enabled) are supported, with other types sorted according to the + // reflect.Value.String() output which guarantees display stability. + SortKeys bool + + // SpewKeys specifies that, as a last resort attempt, map keys should + // be spewed to strings and sorted by those strings. This is only + // considered if SortKeys is true. + SpewKeys bool +} + +// Config is the active configuration of the top-level functions. +// The configuration can be changed by modifying the contents of spew.Config. +var Config = ConfigState{Indent: " "} + +// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the formatted string as a value that satisfies error. See NewFormatter +// for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) { + return fmt.Errorf(format, c.convertArgs(a)...) +} + +// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprint(w, c.convertArgs(a)...) +} + +// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, format, c.convertArgs(a)...) +} + +// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it +// passed with a Formatter interface returned by c.NewFormatter. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, c.convertArgs(a)...) +} + +// Print is a wrapper for fmt.Print that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Print(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Print(a ...interface{}) (n int, err error) { + return fmt.Print(c.convertArgs(a)...) +} + +// Printf is a wrapper for fmt.Printf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(format, c.convertArgs(a)...) +} + +// Println is a wrapper for fmt.Println that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Println(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Println(a ...interface{}) (n int, err error) { + return fmt.Println(c.convertArgs(a)...) +} + +// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprint(a ...interface{}) string { + return fmt.Sprint(c.convertArgs(a)...) +} + +// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, c.convertArgs(a)...) +} + +// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it +// were passed with a Formatter interface returned by c.NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprintln(a ...interface{}) string { + return fmt.Sprintln(c.convertArgs(a)...) +} + +/* +NewFormatter returns a custom formatter that satisfies the fmt.Formatter +interface. As a result, it integrates cleanly with standard fmt package +printing functions. The formatter is useful for inline printing of smaller data +types similar to the standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Typically this function shouldn't be called directly. It is much easier to make +use of the custom formatter by calling one of the convenience functions such as +c.Printf, c.Println, or c.Printf. +*/ +func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter { + return newFormatter(c, v) +} + +// Fdump formats and displays the passed arguments to io.Writer w. It formats +// exactly the same as Dump. +func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) { + fdump(c, w, a...) +} + +/* +Dump displays the passed parameters to standard out with newlines, customizable +indentation, and additional debug information such as complete types and all +pointer addresses used to indirect to the final value. It provides the +following features over the built-in printing facilities provided by the fmt +package: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output + +The configuration options are controlled by modifying the public members +of c. See ConfigState for options documentation. + +See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to +get the formatted result as a string. +*/ +func (c *ConfigState) Dump(a ...interface{}) { + fdump(c, os.Stdout, a...) +} + +// Sdump returns a string with the passed arguments formatted exactly the same +// as Dump. +func (c *ConfigState) Sdump(a ...interface{}) string { + var buf bytes.Buffer + fdump(c, &buf, a...) + return buf.String() +} + +// convertArgs accepts a slice of arguments and returns a slice of the same +// length with each argument converted to a spew Formatter interface using +// the ConfigState associated with s. +func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) { + formatters = make([]interface{}, len(args)) + for index, arg := range args { + formatters[index] = newFormatter(c, arg) + } + return formatters +} + +// NewDefaultConfig returns a ConfigState with the following default settings. +// +// Indent: " " +// MaxDepth: 0 +// DisableMethods: false +// DisablePointerMethods: false +// ContinueOnMethod: false +// SortKeys: false +func NewDefaultConfig() *ConfigState { + return &ConfigState{Indent: " "} +} diff --git a/vendor/github.com/davecgh/go-spew/spew/doc.go b/vendor/github.com/davecgh/go-spew/spew/doc.go new file mode 100644 index 000000000..aacaac6f1 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/doc.go @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +Package spew implements a deep pretty printer for Go data structures to aid in +debugging. + +A quick overview of the additional features spew provides over the built-in +printing facilities for Go data types are as follows: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output (only when using + Dump style) + +There are two different approaches spew allows for dumping Go data structures: + + * Dump style which prints with newlines, customizable indentation, + and additional debug information such as types and all pointer addresses + used to indirect to the final value + * A custom Formatter interface that integrates cleanly with the standard fmt + package and replaces %v, %+v, %#v, and %#+v to provide inline printing + similar to the default %v while providing the additional functionality + outlined above and passing unsupported format verbs such as %x and %q + along to fmt + +Quick Start + +This section demonstrates how to quickly get started with spew. See the +sections below for further details on formatting and configuration options. + +To dump a variable with full newlines, indentation, type, and pointer +information use Dump, Fdump, or Sdump: + spew.Dump(myVar1, myVar2, ...) + spew.Fdump(someWriter, myVar1, myVar2, ...) + str := spew.Sdump(myVar1, myVar2, ...) + +Alternatively, if you would prefer to use format strings with a compacted inline +printing style, use the convenience wrappers Printf, Fprintf, etc with +%v (most compact), %+v (adds pointer addresses), %#v (adds types), or +%#+v (adds types and pointer addresses): + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + +Configuration Options + +Configuration of spew is handled by fields in the ConfigState type. For +convenience, all of the top-level functions use a global state available +via the spew.Config global. + +It is also possible to create a ConfigState instance that provides methods +equivalent to the top-level functions. This allows concurrent configuration +options. See the ConfigState documentation for more details. + +The following configuration options are available: + * Indent + String to use for each indentation level for Dump functions. + It is a single space by default. A popular alternative is "\t". + + * MaxDepth + Maximum number of levels to descend into nested data structures. + There is no limit by default. + + * DisableMethods + Disables invocation of error and Stringer interface methods. + Method invocation is enabled by default. + + * DisablePointerMethods + Disables invocation of error and Stringer interface methods on types + which only accept pointer receivers from non-pointer variables. + Pointer method invocation is enabled by default. + + * DisablePointerAddresses + DisablePointerAddresses specifies whether to disable the printing of + pointer addresses. This is useful when diffing data structures in tests. + + * DisableCapacities + DisableCapacities specifies whether to disable the printing of + capacities for arrays, slices, maps and channels. This is useful when + diffing data structures in tests. + + * ContinueOnMethod + Enables recursion into types after invoking error and Stringer interface + methods. Recursion after method invocation is disabled by default. + + * SortKeys + Specifies map keys should be sorted before being printed. Use + this to have a more deterministic, diffable output. Note that + only native types (bool, int, uint, floats, uintptr and string) + and types which implement error or Stringer interfaces are + supported with other types sorted according to the + reflect.Value.String() output which guarantees display + stability. Natural map order is used by default. + + * SpewKeys + Specifies that, as a last resort attempt, map keys should be + spewed to strings and sorted by those strings. This is only + considered if SortKeys is true. + +Dump Usage + +Simply call spew.Dump with a list of variables you want to dump: + + spew.Dump(myVar1, myVar2, ...) + +You may also call spew.Fdump if you would prefer to output to an arbitrary +io.Writer. For example, to dump to standard error: + + spew.Fdump(os.Stderr, myVar1, myVar2, ...) + +A third option is to call spew.Sdump to get the formatted output as a string: + + str := spew.Sdump(myVar1, myVar2, ...) + +Sample Dump Output + +See the Dump example for details on the setup of the types and variables being +shown here. + + (main.Foo) { + unexportedField: (*main.Bar)(0xf84002e210)({ + flag: (main.Flag) flagTwo, + data: (uintptr) + }), + ExportedField: (map[interface {}]interface {}) (len=1) { + (string) (len=3) "one": (bool) true + } + } + +Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C +command as shown. + ([]uint8) (len=32 cap=32) { + 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | + 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| + 00000020 31 32 |12| + } + +Custom Formatter + +Spew provides a custom formatter that implements the fmt.Formatter interface +so that it integrates cleanly with standard fmt package printing functions. The +formatter is useful for inline printing of smaller data types similar to the +standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Custom Formatter Usage + +The simplest way to make use of the spew custom formatter is to call one of the +convenience functions such as spew.Printf, spew.Println, or spew.Printf. The +functions have syntax you are most likely already familiar with: + + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + spew.Println(myVar, myVar2) + spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + +See the Index for the full list convenience functions. + +Sample Formatter Output + +Double pointer to a uint8: + %v: <**>5 + %+v: <**>(0xf8400420d0->0xf8400420c8)5 + %#v: (**uint8)5 + %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 + +Pointer to circular struct with a uint8 field and a pointer to itself: + %v: <*>{1 <*>} + %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)} + %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)} + %#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)} + +See the Printf example for details on the setup of variables being shown +here. + +Errors + +Since it is possible for custom Stringer/error interfaces to panic, spew +detects them and handles them internally by printing the panic information +inline with the output. Since spew is intended to provide deep pretty printing +capabilities on structures, it intentionally does not return any errors. +*/ +package spew diff --git a/vendor/github.com/davecgh/go-spew/spew/dump.go b/vendor/github.com/davecgh/go-spew/spew/dump.go new file mode 100644 index 000000000..f78d89fc1 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/dump.go @@ -0,0 +1,509 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "os" + "reflect" + "regexp" + "strconv" + "strings" +) + +var ( + // uint8Type is a reflect.Type representing a uint8. It is used to + // convert cgo types to uint8 slices for hexdumping. + uint8Type = reflect.TypeOf(uint8(0)) + + // cCharRE is a regular expression that matches a cgo char. + // It is used to detect character arrays to hexdump them. + cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`) + + // cUnsignedCharRE is a regular expression that matches a cgo unsigned + // char. It is used to detect unsigned character arrays to hexdump + // them. + cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`) + + // cUint8tCharRE is a regular expression that matches a cgo uint8_t. + // It is used to detect uint8_t arrays to hexdump them. + cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`) +) + +// dumpState contains information about the state of a dump operation. +type dumpState struct { + w io.Writer + depth int + pointers map[uintptr]int + ignoreNextType bool + ignoreNextIndent bool + cs *ConfigState +} + +// indent performs indentation according to the depth level and cs.Indent +// option. +func (d *dumpState) indent() { + if d.ignoreNextIndent { + d.ignoreNextIndent = false + return + } + d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth)) +} + +// unpackValue returns values inside of non-nil interfaces when possible. +// This is useful for data types like structs, arrays, slices, and maps which +// can contain varying types packed inside an interface. +func (d *dumpState) unpackValue(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Interface && !v.IsNil() { + v = v.Elem() + } + return v +} + +// dumpPtr handles formatting of pointers by indirecting them as necessary. +func (d *dumpState) dumpPtr(v reflect.Value) { + // Remove pointers at or below the current depth from map used to detect + // circular refs. + for k, depth := range d.pointers { + if depth >= d.depth { + delete(d.pointers, k) + } + } + + // Keep list of all dereferenced pointers to show later. + pointerChain := make([]uintptr, 0) + + // Figure out how many levels of indirection there are by dereferencing + // pointers and unpacking interfaces down the chain while detecting circular + // references. + nilFound := false + cycleFound := false + indirects := 0 + ve := v + for ve.Kind() == reflect.Ptr { + if ve.IsNil() { + nilFound = true + break + } + indirects++ + addr := ve.Pointer() + pointerChain = append(pointerChain, addr) + if pd, ok := d.pointers[addr]; ok && pd < d.depth { + cycleFound = true + indirects-- + break + } + d.pointers[addr] = d.depth + + ve = ve.Elem() + if ve.Kind() == reflect.Interface { + if ve.IsNil() { + nilFound = true + break + } + ve = ve.Elem() + } + } + + // Display type information. + d.w.Write(openParenBytes) + d.w.Write(bytes.Repeat(asteriskBytes, indirects)) + d.w.Write([]byte(ve.Type().String())) + d.w.Write(closeParenBytes) + + // Display pointer information. + if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 { + d.w.Write(openParenBytes) + for i, addr := range pointerChain { + if i > 0 { + d.w.Write(pointerChainBytes) + } + printHexPtr(d.w, addr) + } + d.w.Write(closeParenBytes) + } + + // Display dereferenced value. + d.w.Write(openParenBytes) + switch { + case nilFound: + d.w.Write(nilAngleBytes) + + case cycleFound: + d.w.Write(circularBytes) + + default: + d.ignoreNextType = true + d.dump(ve) + } + d.w.Write(closeParenBytes) +} + +// dumpSlice handles formatting of arrays and slices. Byte (uint8 under +// reflection) arrays and slices are dumped in hexdump -C fashion. +func (d *dumpState) dumpSlice(v reflect.Value) { + // Determine whether this type should be hex dumped or not. Also, + // for types which should be hexdumped, try to use the underlying data + // first, then fall back to trying to convert them to a uint8 slice. + var buf []uint8 + doConvert := false + doHexDump := false + numEntries := v.Len() + if numEntries > 0 { + vt := v.Index(0).Type() + vts := vt.String() + switch { + // C types that need to be converted. + case cCharRE.MatchString(vts): + fallthrough + case cUnsignedCharRE.MatchString(vts): + fallthrough + case cUint8tCharRE.MatchString(vts): + doConvert = true + + // Try to use existing uint8 slices and fall back to converting + // and copying if that fails. + case vt.Kind() == reflect.Uint8: + // We need an addressable interface to convert the type + // to a byte slice. However, the reflect package won't + // give us an interface on certain things like + // unexported struct fields in order to enforce + // visibility rules. We use unsafe, when available, to + // bypass these restrictions since this package does not + // mutate the values. + vs := v + if !vs.CanInterface() || !vs.CanAddr() { + vs = unsafeReflectValue(vs) + } + if !UnsafeDisabled { + vs = vs.Slice(0, numEntries) + + // Use the existing uint8 slice if it can be + // type asserted. + iface := vs.Interface() + if slice, ok := iface.([]uint8); ok { + buf = slice + doHexDump = true + break + } + } + + // The underlying data needs to be converted if it can't + // be type asserted to a uint8 slice. + doConvert = true + } + + // Copy and convert the underlying type if needed. + if doConvert && vt.ConvertibleTo(uint8Type) { + // Convert and copy each element into a uint8 byte + // slice. + buf = make([]uint8, numEntries) + for i := 0; i < numEntries; i++ { + vv := v.Index(i) + buf[i] = uint8(vv.Convert(uint8Type).Uint()) + } + doHexDump = true + } + } + + // Hexdump the entire slice as needed. + if doHexDump { + indent := strings.Repeat(d.cs.Indent, d.depth) + str := indent + hex.Dump(buf) + str = strings.Replace(str, "\n", "\n"+indent, -1) + str = strings.TrimRight(str, d.cs.Indent) + d.w.Write([]byte(str)) + return + } + + // Recursively call dump for each item. + for i := 0; i < numEntries; i++ { + d.dump(d.unpackValue(v.Index(i))) + if i < (numEntries - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } +} + +// dump is the main workhorse for dumping a value. It uses the passed reflect +// value to figure out what kind of object we are dealing with and formats it +// appropriately. It is a recursive function, however circular data structures +// are detected and handled properly. +func (d *dumpState) dump(v reflect.Value) { + // Handle invalid reflect values immediately. + kind := v.Kind() + if kind == reflect.Invalid { + d.w.Write(invalidAngleBytes) + return + } + + // Handle pointers specially. + if kind == reflect.Ptr { + d.indent() + d.dumpPtr(v) + return + } + + // Print type information unless already handled elsewhere. + if !d.ignoreNextType { + d.indent() + d.w.Write(openParenBytes) + d.w.Write([]byte(v.Type().String())) + d.w.Write(closeParenBytes) + d.w.Write(spaceBytes) + } + d.ignoreNextType = false + + // Display length and capacity if the built-in len and cap functions + // work with the value's kind and the len/cap itself is non-zero. + valueLen, valueCap := 0, 0 + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.Chan: + valueLen, valueCap = v.Len(), v.Cap() + case reflect.Map, reflect.String: + valueLen = v.Len() + } + if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 { + d.w.Write(openParenBytes) + if valueLen != 0 { + d.w.Write(lenEqualsBytes) + printInt(d.w, int64(valueLen), 10) + } + if !d.cs.DisableCapacities && valueCap != 0 { + if valueLen != 0 { + d.w.Write(spaceBytes) + } + d.w.Write(capEqualsBytes) + printInt(d.w, int64(valueCap), 10) + } + d.w.Write(closeParenBytes) + d.w.Write(spaceBytes) + } + + // Call Stringer/error interfaces if they exist and the handle methods flag + // is enabled + if !d.cs.DisableMethods { + if (kind != reflect.Invalid) && (kind != reflect.Interface) { + if handled := handleMethods(d.cs, d.w, v); handled { + return + } + } + } + + switch kind { + case reflect.Invalid: + // Do nothing. We should never get here since invalid has already + // been handled above. + + case reflect.Bool: + printBool(d.w, v.Bool()) + + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + printInt(d.w, v.Int(), 10) + + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + printUint(d.w, v.Uint(), 10) + + case reflect.Float32: + printFloat(d.w, v.Float(), 32) + + case reflect.Float64: + printFloat(d.w, v.Float(), 64) + + case reflect.Complex64: + printComplex(d.w, v.Complex(), 32) + + case reflect.Complex128: + printComplex(d.w, v.Complex(), 64) + + case reflect.Slice: + if v.IsNil() { + d.w.Write(nilAngleBytes) + break + } + fallthrough + + case reflect.Array: + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + d.dumpSlice(v) + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.String: + d.w.Write([]byte(strconv.Quote(v.String()))) + + case reflect.Interface: + // The only time we should get here is for nil interfaces due to + // unpackValue calls. + if v.IsNil() { + d.w.Write(nilAngleBytes) + } + + case reflect.Ptr: + // Do nothing. We should never get here since pointers have already + // been handled above. + + case reflect.Map: + // nil maps should be indicated as different than empty maps + if v.IsNil() { + d.w.Write(nilAngleBytes) + break + } + + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + numEntries := v.Len() + keys := v.MapKeys() + if d.cs.SortKeys { + sortValues(keys, d.cs) + } + for i, key := range keys { + d.dump(d.unpackValue(key)) + d.w.Write(colonSpaceBytes) + d.ignoreNextIndent = true + d.dump(d.unpackValue(v.MapIndex(key))) + if i < (numEntries - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.Struct: + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + vt := v.Type() + numFields := v.NumField() + for i := 0; i < numFields; i++ { + d.indent() + vtf := vt.Field(i) + d.w.Write([]byte(vtf.Name)) + d.w.Write(colonSpaceBytes) + d.ignoreNextIndent = true + d.dump(d.unpackValue(v.Field(i))) + if i < (numFields - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.Uintptr: + printHexPtr(d.w, uintptr(v.Uint())) + + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + printHexPtr(d.w, v.Pointer()) + + // There were not any other types at the time this code was written, but + // fall back to letting the default fmt package handle it in case any new + // types are added. + default: + if v.CanInterface() { + fmt.Fprintf(d.w, "%v", v.Interface()) + } else { + fmt.Fprintf(d.w, "%v", v.String()) + } + } +} + +// fdump is a helper function to consolidate the logic from the various public +// methods which take varying writers and config states. +func fdump(cs *ConfigState, w io.Writer, a ...interface{}) { + for _, arg := range a { + if arg == nil { + w.Write(interfaceBytes) + w.Write(spaceBytes) + w.Write(nilAngleBytes) + w.Write(newlineBytes) + continue + } + + d := dumpState{w: w, cs: cs} + d.pointers = make(map[uintptr]int) + d.dump(reflect.ValueOf(arg)) + d.w.Write(newlineBytes) + } +} + +// Fdump formats and displays the passed arguments to io.Writer w. It formats +// exactly the same as Dump. +func Fdump(w io.Writer, a ...interface{}) { + fdump(&Config, w, a...) +} + +// Sdump returns a string with the passed arguments formatted exactly the same +// as Dump. +func Sdump(a ...interface{}) string { + var buf bytes.Buffer + fdump(&Config, &buf, a...) + return buf.String() +} + +/* +Dump displays the passed parameters to standard out with newlines, customizable +indentation, and additional debug information such as complete types and all +pointer addresses used to indirect to the final value. It provides the +following features over the built-in printing facilities provided by the fmt +package: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output + +The configuration options are controlled by an exported package global, +spew.Config. See ConfigState for options documentation. + +See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to +get the formatted result as a string. +*/ +func Dump(a ...interface{}) { + fdump(&Config, os.Stdout, a...) +} diff --git a/vendor/github.com/davecgh/go-spew/spew/dump_test.go b/vendor/github.com/davecgh/go-spew/spew/dump_test.go new file mode 100644 index 000000000..4a31a2ee3 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/dump_test.go @@ -0,0 +1,1042 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +Test Summary: +NOTE: For each test, a nil pointer, a single pointer and double pointer to the +base test element are also tested to ensure proper indirection across all types. + +- Max int8, int16, int32, int64, int +- Max uint8, uint16, uint32, uint64, uint +- Boolean true and false +- Standard complex64 and complex128 +- Array containing standard ints +- Array containing type with custom formatter on pointer receiver only +- Array containing interfaces +- Array containing bytes +- Slice containing standard float32 values +- Slice containing type with custom formatter on pointer receiver only +- Slice containing interfaces +- Slice containing bytes +- Nil slice +- Standard string +- Nil interface +- Sub-interface +- Map with string keys and int vals +- Map with custom formatter type on pointer receiver only keys and vals +- Map with interface keys and values +- Map with nil interface value +- Struct with primitives +- Struct that contains another struct +- Struct that contains custom type with Stringer pointer interface via both + exported and unexported fields +- Struct that contains embedded struct and field to same struct +- Uintptr to 0 (null pointer) +- Uintptr address of real variable +- Unsafe.Pointer to 0 (null pointer) +- Unsafe.Pointer to address of real variable +- Nil channel +- Standard int channel +- Function with no params and no returns +- Function with param and no returns +- Function with multiple params and multiple returns +- Struct that is circular through self referencing +- Structs that are circular through cross referencing +- Structs that are indirectly circular +- Type that panics in its Stringer interface +*/ + +package spew_test + +import ( + "bytes" + "fmt" + "testing" + "unsafe" + + "github.com/davecgh/go-spew/spew" +) + +// dumpTest is used to describe a test to be performed against the Dump method. +type dumpTest struct { + in interface{} + wants []string +} + +// dumpTests houses all of the tests to be performed against the Dump method. +var dumpTests = make([]dumpTest, 0) + +// addDumpTest is a helper method to append the passed input and desired result +// to dumpTests +func addDumpTest(in interface{}, wants ...string) { + test := dumpTest{in, wants} + dumpTests = append(dumpTests, test) +} + +func addIntDumpTests() { + // Max int8. + v := int8(127) + nv := (*int8)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "int8" + vs := "127" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") + + // Max int16. + v2 := int16(32767) + nv2 := (*int16)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "int16" + v2s := "32767" + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") + addDumpTest(nv2, "(*"+v2t+")()\n") + + // Max int32. + v3 := int32(2147483647) + nv3 := (*int32)(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "int32" + v3s := "2147483647" + addDumpTest(v3, "("+v3t+") "+v3s+"\n") + addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")\n") + addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")\n") + addDumpTest(nv3, "(*"+v3t+")()\n") + + // Max int64. + v4 := int64(9223372036854775807) + nv4 := (*int64)(nil) + pv4 := &v4 + v4Addr := fmt.Sprintf("%p", pv4) + pv4Addr := fmt.Sprintf("%p", &pv4) + v4t := "int64" + v4s := "9223372036854775807" + addDumpTest(v4, "("+v4t+") "+v4s+"\n") + addDumpTest(pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")\n") + addDumpTest(&pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")\n") + addDumpTest(nv4, "(*"+v4t+")()\n") + + // Max int. + v5 := int(2147483647) + nv5 := (*int)(nil) + pv5 := &v5 + v5Addr := fmt.Sprintf("%p", pv5) + pv5Addr := fmt.Sprintf("%p", &pv5) + v5t := "int" + v5s := "2147483647" + addDumpTest(v5, "("+v5t+") "+v5s+"\n") + addDumpTest(pv5, "(*"+v5t+")("+v5Addr+")("+v5s+")\n") + addDumpTest(&pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")("+v5s+")\n") + addDumpTest(nv5, "(*"+v5t+")()\n") +} + +func addUintDumpTests() { + // Max uint8. + v := uint8(255) + nv := (*uint8)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "uint8" + vs := "255" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") + + // Max uint16. + v2 := uint16(65535) + nv2 := (*uint16)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "uint16" + v2s := "65535" + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") + addDumpTest(nv2, "(*"+v2t+")()\n") + + // Max uint32. + v3 := uint32(4294967295) + nv3 := (*uint32)(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "uint32" + v3s := "4294967295" + addDumpTest(v3, "("+v3t+") "+v3s+"\n") + addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")\n") + addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")\n") + addDumpTest(nv3, "(*"+v3t+")()\n") + + // Max uint64. + v4 := uint64(18446744073709551615) + nv4 := (*uint64)(nil) + pv4 := &v4 + v4Addr := fmt.Sprintf("%p", pv4) + pv4Addr := fmt.Sprintf("%p", &pv4) + v4t := "uint64" + v4s := "18446744073709551615" + addDumpTest(v4, "("+v4t+") "+v4s+"\n") + addDumpTest(pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")\n") + addDumpTest(&pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")\n") + addDumpTest(nv4, "(*"+v4t+")()\n") + + // Max uint. + v5 := uint(4294967295) + nv5 := (*uint)(nil) + pv5 := &v5 + v5Addr := fmt.Sprintf("%p", pv5) + pv5Addr := fmt.Sprintf("%p", &pv5) + v5t := "uint" + v5s := "4294967295" + addDumpTest(v5, "("+v5t+") "+v5s+"\n") + addDumpTest(pv5, "(*"+v5t+")("+v5Addr+")("+v5s+")\n") + addDumpTest(&pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")("+v5s+")\n") + addDumpTest(nv5, "(*"+v5t+")()\n") +} + +func addBoolDumpTests() { + // Boolean true. + v := bool(true) + nv := (*bool)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "bool" + vs := "true" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") + + // Boolean false. + v2 := bool(false) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "bool" + v2s := "false" + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") +} + +func addFloatDumpTests() { + // Standard float32. + v := float32(3.1415) + nv := (*float32)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "float32" + vs := "3.1415" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") + + // Standard float64. + v2 := float64(3.1415926) + nv2 := (*float64)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "float64" + v2s := "3.1415926" + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") + addDumpTest(nv2, "(*"+v2t+")()\n") +} + +func addComplexDumpTests() { + // Standard complex64. + v := complex(float32(6), -2) + nv := (*complex64)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "complex64" + vs := "(6-2i)" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") + + // Standard complex128. + v2 := complex(float64(-6), 2) + nv2 := (*complex128)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "complex128" + v2s := "(-6+2i)" + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") + addDumpTest(nv2, "(*"+v2t+")()\n") +} + +func addArrayDumpTests() { + // Array containing standard ints. + v := [3]int{1, 2, 3} + vLen := fmt.Sprintf("%d", len(v)) + vCap := fmt.Sprintf("%d", cap(v)) + nv := (*[3]int)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "int" + vs := "(len=" + vLen + " cap=" + vCap + ") {\n (" + vt + ") 1,\n (" + + vt + ") 2,\n (" + vt + ") 3\n}" + addDumpTest(v, "([3]"+vt+") "+vs+"\n") + addDumpTest(pv, "(*[3]"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**[3]"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*[3]"+vt+")()\n") + + // Array containing type with custom formatter on pointer receiver only. + v2i0 := pstringer("1") + v2i1 := pstringer("2") + v2i2 := pstringer("3") + v2 := [3]pstringer{v2i0, v2i1, v2i2} + v2i0Len := fmt.Sprintf("%d", len(v2i0)) + v2i1Len := fmt.Sprintf("%d", len(v2i1)) + v2i2Len := fmt.Sprintf("%d", len(v2i2)) + v2Len := fmt.Sprintf("%d", len(v2)) + v2Cap := fmt.Sprintf("%d", cap(v2)) + nv2 := (*[3]pstringer)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "spew_test.pstringer" + v2sp := "(len=" + v2Len + " cap=" + v2Cap + ") {\n (" + v2t + + ") (len=" + v2i0Len + ") stringer 1,\n (" + v2t + + ") (len=" + v2i1Len + ") stringer 2,\n (" + v2t + + ") (len=" + v2i2Len + ") " + "stringer 3\n}" + v2s := v2sp + if spew.UnsafeDisabled { + v2s = "(len=" + v2Len + " cap=" + v2Cap + ") {\n (" + v2t + + ") (len=" + v2i0Len + ") \"1\",\n (" + v2t + ") (len=" + + v2i1Len + ") \"2\",\n (" + v2t + ") (len=" + v2i2Len + + ") " + "\"3\"\n}" + } + addDumpTest(v2, "([3]"+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*[3]"+v2t+")("+v2Addr+")("+v2sp+")\n") + addDumpTest(&pv2, "(**[3]"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2sp+")\n") + addDumpTest(nv2, "(*[3]"+v2t+")()\n") + + // Array containing interfaces. + v3i0 := "one" + v3 := [3]interface{}{v3i0, int(2), uint(3)} + v3i0Len := fmt.Sprintf("%d", len(v3i0)) + v3Len := fmt.Sprintf("%d", len(v3)) + v3Cap := fmt.Sprintf("%d", cap(v3)) + nv3 := (*[3]interface{})(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "[3]interface {}" + v3t2 := "string" + v3t3 := "int" + v3t4 := "uint" + v3s := "(len=" + v3Len + " cap=" + v3Cap + ") {\n (" + v3t2 + ") " + + "(len=" + v3i0Len + ") \"one\",\n (" + v3t3 + ") 2,\n (" + + v3t4 + ") 3\n}" + addDumpTest(v3, "("+v3t+") "+v3s+"\n") + addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")\n") + addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")\n") + addDumpTest(nv3, "(*"+v3t+")()\n") + + // Array containing bytes. + v4 := [34]byte{ + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, + } + v4Len := fmt.Sprintf("%d", len(v4)) + v4Cap := fmt.Sprintf("%d", cap(v4)) + nv4 := (*[34]byte)(nil) + pv4 := &v4 + v4Addr := fmt.Sprintf("%p", pv4) + pv4Addr := fmt.Sprintf("%p", &pv4) + v4t := "[34]uint8" + v4s := "(len=" + v4Len + " cap=" + v4Cap + ") " + + "{\n 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20" + + " |............... |\n" + + " 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30" + + " |!\"#$%&'()*+,-./0|\n" + + " 00000020 31 32 " + + " |12|\n}" + addDumpTest(v4, "("+v4t+") "+v4s+"\n") + addDumpTest(pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")\n") + addDumpTest(&pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")\n") + addDumpTest(nv4, "(*"+v4t+")()\n") +} + +func addSliceDumpTests() { + // Slice containing standard float32 values. + v := []float32{3.14, 6.28, 12.56} + vLen := fmt.Sprintf("%d", len(v)) + vCap := fmt.Sprintf("%d", cap(v)) + nv := (*[]float32)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "float32" + vs := "(len=" + vLen + " cap=" + vCap + ") {\n (" + vt + ") 3.14,\n (" + + vt + ") 6.28,\n (" + vt + ") 12.56\n}" + addDumpTest(v, "([]"+vt+") "+vs+"\n") + addDumpTest(pv, "(*[]"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**[]"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*[]"+vt+")()\n") + + // Slice containing type with custom formatter on pointer receiver only. + v2i0 := pstringer("1") + v2i1 := pstringer("2") + v2i2 := pstringer("3") + v2 := []pstringer{v2i0, v2i1, v2i2} + v2i0Len := fmt.Sprintf("%d", len(v2i0)) + v2i1Len := fmt.Sprintf("%d", len(v2i1)) + v2i2Len := fmt.Sprintf("%d", len(v2i2)) + v2Len := fmt.Sprintf("%d", len(v2)) + v2Cap := fmt.Sprintf("%d", cap(v2)) + nv2 := (*[]pstringer)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "spew_test.pstringer" + v2s := "(len=" + v2Len + " cap=" + v2Cap + ") {\n (" + v2t + ") (len=" + + v2i0Len + ") stringer 1,\n (" + v2t + ") (len=" + v2i1Len + + ") stringer 2,\n (" + v2t + ") (len=" + v2i2Len + ") " + + "stringer 3\n}" + addDumpTest(v2, "([]"+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*[]"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**[]"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") + addDumpTest(nv2, "(*[]"+v2t+")()\n") + + // Slice containing interfaces. + v3i0 := "one" + v3 := []interface{}{v3i0, int(2), uint(3), nil} + v3i0Len := fmt.Sprintf("%d", len(v3i0)) + v3Len := fmt.Sprintf("%d", len(v3)) + v3Cap := fmt.Sprintf("%d", cap(v3)) + nv3 := (*[]interface{})(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "[]interface {}" + v3t2 := "string" + v3t3 := "int" + v3t4 := "uint" + v3t5 := "interface {}" + v3s := "(len=" + v3Len + " cap=" + v3Cap + ") {\n (" + v3t2 + ") " + + "(len=" + v3i0Len + ") \"one\",\n (" + v3t3 + ") 2,\n (" + + v3t4 + ") 3,\n (" + v3t5 + ") \n}" + addDumpTest(v3, "("+v3t+") "+v3s+"\n") + addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")\n") + addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")\n") + addDumpTest(nv3, "(*"+v3t+")()\n") + + // Slice containing bytes. + v4 := []byte{ + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, + } + v4Len := fmt.Sprintf("%d", len(v4)) + v4Cap := fmt.Sprintf("%d", cap(v4)) + nv4 := (*[]byte)(nil) + pv4 := &v4 + v4Addr := fmt.Sprintf("%p", pv4) + pv4Addr := fmt.Sprintf("%p", &pv4) + v4t := "[]uint8" + v4s := "(len=" + v4Len + " cap=" + v4Cap + ") " + + "{\n 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20" + + " |............... |\n" + + " 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30" + + " |!\"#$%&'()*+,-./0|\n" + + " 00000020 31 32 " + + " |12|\n}" + addDumpTest(v4, "("+v4t+") "+v4s+"\n") + addDumpTest(pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")\n") + addDumpTest(&pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")\n") + addDumpTest(nv4, "(*"+v4t+")()\n") + + // Nil slice. + v5 := []int(nil) + nv5 := (*[]int)(nil) + pv5 := &v5 + v5Addr := fmt.Sprintf("%p", pv5) + pv5Addr := fmt.Sprintf("%p", &pv5) + v5t := "[]int" + v5s := "" + addDumpTest(v5, "("+v5t+") "+v5s+"\n") + addDumpTest(pv5, "(*"+v5t+")("+v5Addr+")("+v5s+")\n") + addDumpTest(&pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")("+v5s+")\n") + addDumpTest(nv5, "(*"+v5t+")()\n") +} + +func addStringDumpTests() { + // Standard string. + v := "test" + vLen := fmt.Sprintf("%d", len(v)) + nv := (*string)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "string" + vs := "(len=" + vLen + ") \"test\"" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") +} + +func addInterfaceDumpTests() { + // Nil interface. + var v interface{} + nv := (*interface{})(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "interface {}" + vs := "" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") + + // Sub-interface. + v2 := interface{}(uint16(65535)) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "uint16" + v2s := "65535" + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") +} + +func addMapDumpTests() { + // Map with string keys and int vals. + k := "one" + kk := "two" + m := map[string]int{k: 1, kk: 2} + klen := fmt.Sprintf("%d", len(k)) // not kLen to shut golint up + kkLen := fmt.Sprintf("%d", len(kk)) + mLen := fmt.Sprintf("%d", len(m)) + nilMap := map[string]int(nil) + nm := (*map[string]int)(nil) + pm := &m + mAddr := fmt.Sprintf("%p", pm) + pmAddr := fmt.Sprintf("%p", &pm) + mt := "map[string]int" + mt1 := "string" + mt2 := "int" + ms := "(len=" + mLen + ") {\n (" + mt1 + ") (len=" + klen + ") " + + "\"one\": (" + mt2 + ") 1,\n (" + mt1 + ") (len=" + kkLen + + ") \"two\": (" + mt2 + ") 2\n}" + ms2 := "(len=" + mLen + ") {\n (" + mt1 + ") (len=" + kkLen + ") " + + "\"two\": (" + mt2 + ") 2,\n (" + mt1 + ") (len=" + klen + + ") \"one\": (" + mt2 + ") 1\n}" + addDumpTest(m, "("+mt+") "+ms+"\n", "("+mt+") "+ms2+"\n") + addDumpTest(pm, "(*"+mt+")("+mAddr+")("+ms+")\n", + "(*"+mt+")("+mAddr+")("+ms2+")\n") + addDumpTest(&pm, "(**"+mt+")("+pmAddr+"->"+mAddr+")("+ms+")\n", + "(**"+mt+")("+pmAddr+"->"+mAddr+")("+ms2+")\n") + addDumpTest(nm, "(*"+mt+")()\n") + addDumpTest(nilMap, "("+mt+") \n") + + // Map with custom formatter type on pointer receiver only keys and vals. + k2 := pstringer("one") + v2 := pstringer("1") + m2 := map[pstringer]pstringer{k2: v2} + k2Len := fmt.Sprintf("%d", len(k2)) + v2Len := fmt.Sprintf("%d", len(v2)) + m2Len := fmt.Sprintf("%d", len(m2)) + nilMap2 := map[pstringer]pstringer(nil) + nm2 := (*map[pstringer]pstringer)(nil) + pm2 := &m2 + m2Addr := fmt.Sprintf("%p", pm2) + pm2Addr := fmt.Sprintf("%p", &pm2) + m2t := "map[spew_test.pstringer]spew_test.pstringer" + m2t1 := "spew_test.pstringer" + m2t2 := "spew_test.pstringer" + m2s := "(len=" + m2Len + ") {\n (" + m2t1 + ") (len=" + k2Len + ") " + + "stringer one: (" + m2t2 + ") (len=" + v2Len + ") stringer 1\n}" + if spew.UnsafeDisabled { + m2s = "(len=" + m2Len + ") {\n (" + m2t1 + ") (len=" + k2Len + + ") " + "\"one\": (" + m2t2 + ") (len=" + v2Len + + ") \"1\"\n}" + } + addDumpTest(m2, "("+m2t+") "+m2s+"\n") + addDumpTest(pm2, "(*"+m2t+")("+m2Addr+")("+m2s+")\n") + addDumpTest(&pm2, "(**"+m2t+")("+pm2Addr+"->"+m2Addr+")("+m2s+")\n") + addDumpTest(nm2, "(*"+m2t+")()\n") + addDumpTest(nilMap2, "("+m2t+") \n") + + // Map with interface keys and values. + k3 := "one" + k3Len := fmt.Sprintf("%d", len(k3)) + m3 := map[interface{}]interface{}{k3: 1} + m3Len := fmt.Sprintf("%d", len(m3)) + nilMap3 := map[interface{}]interface{}(nil) + nm3 := (*map[interface{}]interface{})(nil) + pm3 := &m3 + m3Addr := fmt.Sprintf("%p", pm3) + pm3Addr := fmt.Sprintf("%p", &pm3) + m3t := "map[interface {}]interface {}" + m3t1 := "string" + m3t2 := "int" + m3s := "(len=" + m3Len + ") {\n (" + m3t1 + ") (len=" + k3Len + ") " + + "\"one\": (" + m3t2 + ") 1\n}" + addDumpTest(m3, "("+m3t+") "+m3s+"\n") + addDumpTest(pm3, "(*"+m3t+")("+m3Addr+")("+m3s+")\n") + addDumpTest(&pm3, "(**"+m3t+")("+pm3Addr+"->"+m3Addr+")("+m3s+")\n") + addDumpTest(nm3, "(*"+m3t+")()\n") + addDumpTest(nilMap3, "("+m3t+") \n") + + // Map with nil interface value. + k4 := "nil" + k4Len := fmt.Sprintf("%d", len(k4)) + m4 := map[string]interface{}{k4: nil} + m4Len := fmt.Sprintf("%d", len(m4)) + nilMap4 := map[string]interface{}(nil) + nm4 := (*map[string]interface{})(nil) + pm4 := &m4 + m4Addr := fmt.Sprintf("%p", pm4) + pm4Addr := fmt.Sprintf("%p", &pm4) + m4t := "map[string]interface {}" + m4t1 := "string" + m4t2 := "interface {}" + m4s := "(len=" + m4Len + ") {\n (" + m4t1 + ") (len=" + k4Len + ")" + + " \"nil\": (" + m4t2 + ") \n}" + addDumpTest(m4, "("+m4t+") "+m4s+"\n") + addDumpTest(pm4, "(*"+m4t+")("+m4Addr+")("+m4s+")\n") + addDumpTest(&pm4, "(**"+m4t+")("+pm4Addr+"->"+m4Addr+")("+m4s+")\n") + addDumpTest(nm4, "(*"+m4t+")()\n") + addDumpTest(nilMap4, "("+m4t+") \n") +} + +func addStructDumpTests() { + // Struct with primitives. + type s1 struct { + a int8 + b uint8 + } + v := s1{127, 255} + nv := (*s1)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "spew_test.s1" + vt2 := "int8" + vt3 := "uint8" + vs := "{\n a: (" + vt2 + ") 127,\n b: (" + vt3 + ") 255\n}" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") + + // Struct that contains another struct. + type s2 struct { + s1 s1 + b bool + } + v2 := s2{s1{127, 255}, true} + nv2 := (*s2)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "spew_test.s2" + v2t2 := "spew_test.s1" + v2t3 := "int8" + v2t4 := "uint8" + v2t5 := "bool" + v2s := "{\n s1: (" + v2t2 + ") {\n a: (" + v2t3 + ") 127,\n b: (" + + v2t4 + ") 255\n },\n b: (" + v2t5 + ") true\n}" + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") + addDumpTest(nv2, "(*"+v2t+")()\n") + + // Struct that contains custom type with Stringer pointer interface via both + // exported and unexported fields. + type s3 struct { + s pstringer + S pstringer + } + v3 := s3{"test", "test2"} + nv3 := (*s3)(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "spew_test.s3" + v3t2 := "spew_test.pstringer" + v3s := "{\n s: (" + v3t2 + ") (len=4) stringer test,\n S: (" + v3t2 + + ") (len=5) stringer test2\n}" + v3sp := v3s + if spew.UnsafeDisabled { + v3s = "{\n s: (" + v3t2 + ") (len=4) \"test\",\n S: (" + + v3t2 + ") (len=5) \"test2\"\n}" + v3sp = "{\n s: (" + v3t2 + ") (len=4) \"test\",\n S: (" + + v3t2 + ") (len=5) stringer test2\n}" + } + addDumpTest(v3, "("+v3t+") "+v3s+"\n") + addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3sp+")\n") + addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3sp+")\n") + addDumpTest(nv3, "(*"+v3t+")()\n") + + // Struct that contains embedded struct and field to same struct. + e := embed{"embedstr"} + eLen := fmt.Sprintf("%d", len("embedstr")) + v4 := embedwrap{embed: &e, e: &e} + nv4 := (*embedwrap)(nil) + pv4 := &v4 + eAddr := fmt.Sprintf("%p", &e) + v4Addr := fmt.Sprintf("%p", pv4) + pv4Addr := fmt.Sprintf("%p", &pv4) + v4t := "spew_test.embedwrap" + v4t2 := "spew_test.embed" + v4t3 := "string" + v4s := "{\n embed: (*" + v4t2 + ")(" + eAddr + ")({\n a: (" + v4t3 + + ") (len=" + eLen + ") \"embedstr\"\n }),\n e: (*" + v4t2 + + ")(" + eAddr + ")({\n a: (" + v4t3 + ") (len=" + eLen + ")" + + " \"embedstr\"\n })\n}" + addDumpTest(v4, "("+v4t+") "+v4s+"\n") + addDumpTest(pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")\n") + addDumpTest(&pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")\n") + addDumpTest(nv4, "(*"+v4t+")()\n") +} + +func addUintptrDumpTests() { + // Null pointer. + v := uintptr(0) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "uintptr" + vs := "" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + + // Address of real variable. + i := 1 + v2 := uintptr(unsafe.Pointer(&i)) + nv2 := (*uintptr)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "uintptr" + v2s := fmt.Sprintf("%p", &i) + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") + addDumpTest(nv2, "(*"+v2t+")()\n") +} + +func addUnsafePointerDumpTests() { + // Null pointer. + v := unsafe.Pointer(nil) + nv := (*unsafe.Pointer)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "unsafe.Pointer" + vs := "" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") + + // Address of real variable. + i := 1 + v2 := unsafe.Pointer(&i) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "unsafe.Pointer" + v2s := fmt.Sprintf("%p", &i) + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") + addDumpTest(nv, "(*"+vt+")()\n") +} + +func addChanDumpTests() { + // Nil channel. + var v chan int + pv := &v + nv := (*chan int)(nil) + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "chan int" + vs := "" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") + + // Real channel. + v2 := make(chan int) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "chan int" + v2s := fmt.Sprintf("%p", v2) + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") +} + +func addFuncDumpTests() { + // Function with no params and no returns. + v := addIntDumpTests + nv := (*func())(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "func()" + vs := fmt.Sprintf("%p", v) + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") + + // Function with param and no returns. + v2 := TestDump + nv2 := (*func(*testing.T))(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "func(*testing.T)" + v2s := fmt.Sprintf("%p", v2) + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n") + addDumpTest(nv2, "(*"+v2t+")()\n") + + // Function with multiple params and multiple returns. + var v3 = func(i int, s string) (b bool, err error) { + return true, nil + } + nv3 := (*func(int, string) (bool, error))(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "func(int, string) (bool, error)" + v3s := fmt.Sprintf("%p", v3) + addDumpTest(v3, "("+v3t+") "+v3s+"\n") + addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")\n") + addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")\n") + addDumpTest(nv3, "(*"+v3t+")()\n") +} + +func addCircularDumpTests() { + // Struct that is circular through self referencing. + type circular struct { + c *circular + } + v := circular{nil} + v.c = &v + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "spew_test.circular" + vs := "{\n c: (*" + vt + ")(" + vAddr + ")({\n c: (*" + vt + ")(" + + vAddr + ")()\n })\n}" + vs2 := "{\n c: (*" + vt + ")(" + vAddr + ")()\n}" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs2+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs2+")\n") + + // Structs that are circular through cross referencing. + v2 := xref1{nil} + ts2 := xref2{&v2} + v2.ps2 = &ts2 + pv2 := &v2 + ts2Addr := fmt.Sprintf("%p", &ts2) + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "spew_test.xref1" + v2t2 := "spew_test.xref2" + v2s := "{\n ps2: (*" + v2t2 + ")(" + ts2Addr + ")({\n ps1: (*" + v2t + + ")(" + v2Addr + ")({\n ps2: (*" + v2t2 + ")(" + ts2Addr + + ")()\n })\n })\n}" + v2s2 := "{\n ps2: (*" + v2t2 + ")(" + ts2Addr + ")({\n ps1: (*" + v2t + + ")(" + v2Addr + ")()\n })\n}" + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s2+")\n") + addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s2+")\n") + + // Structs that are indirectly circular. + v3 := indirCir1{nil} + tic2 := indirCir2{nil} + tic3 := indirCir3{&v3} + tic2.ps3 = &tic3 + v3.ps2 = &tic2 + pv3 := &v3 + tic2Addr := fmt.Sprintf("%p", &tic2) + tic3Addr := fmt.Sprintf("%p", &tic3) + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "spew_test.indirCir1" + v3t2 := "spew_test.indirCir2" + v3t3 := "spew_test.indirCir3" + v3s := "{\n ps2: (*" + v3t2 + ")(" + tic2Addr + ")({\n ps3: (*" + v3t3 + + ")(" + tic3Addr + ")({\n ps1: (*" + v3t + ")(" + v3Addr + + ")({\n ps2: (*" + v3t2 + ")(" + tic2Addr + + ")()\n })\n })\n })\n}" + v3s2 := "{\n ps2: (*" + v3t2 + ")(" + tic2Addr + ")({\n ps3: (*" + v3t3 + + ")(" + tic3Addr + ")({\n ps1: (*" + v3t + ")(" + v3Addr + + ")()\n })\n })\n}" + addDumpTest(v3, "("+v3t+") "+v3s+"\n") + addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s2+")\n") + addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s2+")\n") +} + +func addPanicDumpTests() { + // Type that panics in its Stringer interface. + v := panicer(127) + nv := (*panicer)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "spew_test.panicer" + vs := "(PANIC=test panic)127" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") +} + +func addErrorDumpTests() { + // Type that has a custom Error interface. + v := customError(127) + nv := (*customError)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "spew_test.customError" + vs := "error: 127" + addDumpTest(v, "("+vt+") "+vs+"\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n") + addDumpTest(nv, "(*"+vt+")()\n") +} + +// TestDump executes all of the tests described by dumpTests. +func TestDump(t *testing.T) { + // Setup tests. + addIntDumpTests() + addUintDumpTests() + addBoolDumpTests() + addFloatDumpTests() + addComplexDumpTests() + addArrayDumpTests() + addSliceDumpTests() + addStringDumpTests() + addInterfaceDumpTests() + addMapDumpTests() + addStructDumpTests() + addUintptrDumpTests() + addUnsafePointerDumpTests() + addChanDumpTests() + addFuncDumpTests() + addCircularDumpTests() + addPanicDumpTests() + addErrorDumpTests() + addCgoDumpTests() + + t.Logf("Running %d tests", len(dumpTests)) + for i, test := range dumpTests { + buf := new(bytes.Buffer) + spew.Fdump(buf, test.in) + s := buf.String() + if testFailed(s, test.wants) { + t.Errorf("Dump #%d\n got: %s %s", i, s, stringizeWants(test.wants)) + continue + } + } +} + +func TestDumpSortedKeys(t *testing.T) { + cfg := spew.ConfigState{SortKeys: true} + s := cfg.Sdump(map[int]string{1: "1", 3: "3", 2: "2"}) + expected := "(map[int]string) (len=3) {\n(int) 1: (string) (len=1) " + + "\"1\",\n(int) 2: (string) (len=1) \"2\",\n(int) 3: (string) " + + "(len=1) \"3\"\n" + + "}\n" + if s != expected { + t.Errorf("Sorted keys mismatch:\n %v %v", s, expected) + } + + s = cfg.Sdump(map[stringer]int{"1": 1, "3": 3, "2": 2}) + expected = "(map[spew_test.stringer]int) (len=3) {\n" + + "(spew_test.stringer) (len=1) stringer 1: (int) 1,\n" + + "(spew_test.stringer) (len=1) stringer 2: (int) 2,\n" + + "(spew_test.stringer) (len=1) stringer 3: (int) 3\n" + + "}\n" + if s != expected { + t.Errorf("Sorted keys mismatch:\n %v %v", s, expected) + } + + s = cfg.Sdump(map[pstringer]int{pstringer("1"): 1, pstringer("3"): 3, pstringer("2"): 2}) + expected = "(map[spew_test.pstringer]int) (len=3) {\n" + + "(spew_test.pstringer) (len=1) stringer 1: (int) 1,\n" + + "(spew_test.pstringer) (len=1) stringer 2: (int) 2,\n" + + "(spew_test.pstringer) (len=1) stringer 3: (int) 3\n" + + "}\n" + if spew.UnsafeDisabled { + expected = "(map[spew_test.pstringer]int) (len=3) {\n" + + "(spew_test.pstringer) (len=1) \"1\": (int) 1,\n" + + "(spew_test.pstringer) (len=1) \"2\": (int) 2,\n" + + "(spew_test.pstringer) (len=1) \"3\": (int) 3\n" + + "}\n" + } + if s != expected { + t.Errorf("Sorted keys mismatch:\n %v %v", s, expected) + } + + s = cfg.Sdump(map[customError]int{customError(1): 1, customError(3): 3, customError(2): 2}) + expected = "(map[spew_test.customError]int) (len=3) {\n" + + "(spew_test.customError) error: 1: (int) 1,\n" + + "(spew_test.customError) error: 2: (int) 2,\n" + + "(spew_test.customError) error: 3: (int) 3\n" + + "}\n" + if s != expected { + t.Errorf("Sorted keys mismatch:\n %v %v", s, expected) + } + +} diff --git a/vendor/github.com/davecgh/go-spew/spew/dumpcgo_test.go b/vendor/github.com/davecgh/go-spew/spew/dumpcgo_test.go new file mode 100644 index 000000000..108baa55f --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/dumpcgo_test.go @@ -0,0 +1,101 @@ +// Copyright (c) 2013-2016 Dave Collins +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when both cgo is supported and "-tags testcgo" is added to the go test +// command line. This means the cgo tests are only added (and hence run) when +// specifially requested. This configuration is used because spew itself +// does not require cgo to run even though it does handle certain cgo types +// specially. Rather than forcing all clients to require cgo and an external +// C compiler just to run the tests, this scheme makes them optional. +// +build cgo,testcgo + +package spew_test + +import ( + "fmt" + + "github.com/davecgh/go-spew/spew/testdata" +) + +func addCgoDumpTests() { + // C char pointer. + v := testdata.GetCgoCharPointer() + nv := testdata.GetCgoNullCharPointer() + pv := &v + vcAddr := fmt.Sprintf("%p", v) + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "*testdata._Ctype_char" + vs := "116" + addDumpTest(v, "("+vt+")("+vcAddr+")("+vs+")\n") + addDumpTest(pv, "(*"+vt+")("+vAddr+"->"+vcAddr+")("+vs+")\n") + addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+"->"+vcAddr+")("+vs+")\n") + addDumpTest(nv, "("+vt+")()\n") + + // C char array. + v2, v2l, v2c := testdata.GetCgoCharArray() + v2Len := fmt.Sprintf("%d", v2l) + v2Cap := fmt.Sprintf("%d", v2c) + v2t := "[6]testdata._Ctype_char" + v2s := "(len=" + v2Len + " cap=" + v2Cap + ") " + + "{\n 00000000 74 65 73 74 32 00 " + + " |test2.|\n}" + addDumpTest(v2, "("+v2t+") "+v2s+"\n") + + // C unsigned char array. + v3, v3l, v3c := testdata.GetCgoUnsignedCharArray() + v3Len := fmt.Sprintf("%d", v3l) + v3Cap := fmt.Sprintf("%d", v3c) + v3t := "[6]testdata._Ctype_unsignedchar" + v3t2 := "[6]testdata._Ctype_uchar" + v3s := "(len=" + v3Len + " cap=" + v3Cap + ") " + + "{\n 00000000 74 65 73 74 33 00 " + + " |test3.|\n}" + addDumpTest(v3, "("+v3t+") "+v3s+"\n", "("+v3t2+") "+v3s+"\n") + + // C signed char array. + v4, v4l, v4c := testdata.GetCgoSignedCharArray() + v4Len := fmt.Sprintf("%d", v4l) + v4Cap := fmt.Sprintf("%d", v4c) + v4t := "[6]testdata._Ctype_schar" + v4t2 := "testdata._Ctype_schar" + v4s := "(len=" + v4Len + " cap=" + v4Cap + ") " + + "{\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 101,\n (" + v4t2 + + ") 115,\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 52,\n (" + v4t2 + + ") 0\n}" + addDumpTest(v4, "("+v4t+") "+v4s+"\n") + + // C uint8_t array. + v5, v5l, v5c := testdata.GetCgoUint8tArray() + v5Len := fmt.Sprintf("%d", v5l) + v5Cap := fmt.Sprintf("%d", v5c) + v5t := "[6]testdata._Ctype_uint8_t" + v5t2 := "[6]testdata._Ctype_uchar" + v5s := "(len=" + v5Len + " cap=" + v5Cap + ") " + + "{\n 00000000 74 65 73 74 35 00 " + + " |test5.|\n}" + addDumpTest(v5, "("+v5t+") "+v5s+"\n", "("+v5t2+") "+v5s+"\n") + + // C typedefed unsigned char array. + v6, v6l, v6c := testdata.GetCgoTypdefedUnsignedCharArray() + v6Len := fmt.Sprintf("%d", v6l) + v6Cap := fmt.Sprintf("%d", v6c) + v6t := "[6]testdata._Ctype_custom_uchar_t" + v6t2 := "[6]testdata._Ctype_uchar" + v6s := "(len=" + v6Len + " cap=" + v6Cap + ") " + + "{\n 00000000 74 65 73 74 36 00 " + + " |test6.|\n}" + addDumpTest(v6, "("+v6t+") "+v6s+"\n", "("+v6t2+") "+v6s+"\n") +} diff --git a/vendor/github.com/davecgh/go-spew/spew/dumpnocgo_test.go b/vendor/github.com/davecgh/go-spew/spew/dumpnocgo_test.go new file mode 100644 index 000000000..52a0971fb --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/dumpnocgo_test.go @@ -0,0 +1,26 @@ +// Copyright (c) 2013 Dave Collins +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when either cgo is not supported or "-tags testcgo" is not added to the go +// test command line. This file intentionally does not setup any cgo tests in +// this scenario. +// +build !cgo !testcgo + +package spew_test + +func addCgoDumpTests() { + // Don't add any tests for cgo since this file is only compiled when + // there should not be any cgo tests. +} diff --git a/vendor/github.com/davecgh/go-spew/spew/example_test.go b/vendor/github.com/davecgh/go-spew/spew/example_test.go new file mode 100644 index 000000000..c6ec8c6d5 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/example_test.go @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew_test + +import ( + "fmt" + + "github.com/davecgh/go-spew/spew" +) + +type Flag int + +const ( + flagOne Flag = iota + flagTwo +) + +var flagStrings = map[Flag]string{ + flagOne: "flagOne", + flagTwo: "flagTwo", +} + +func (f Flag) String() string { + if s, ok := flagStrings[f]; ok { + return s + } + return fmt.Sprintf("Unknown flag (%d)", int(f)) +} + +type Bar struct { + data uintptr +} + +type Foo struct { + unexportedField Bar + ExportedField map[interface{}]interface{} +} + +// This example demonstrates how to use Dump to dump variables to stdout. +func ExampleDump() { + // The following package level declarations are assumed for this example: + /* + type Flag int + + const ( + flagOne Flag = iota + flagTwo + ) + + var flagStrings = map[Flag]string{ + flagOne: "flagOne", + flagTwo: "flagTwo", + } + + func (f Flag) String() string { + if s, ok := flagStrings[f]; ok { + return s + } + return fmt.Sprintf("Unknown flag (%d)", int(f)) + } + + type Bar struct { + data uintptr + } + + type Foo struct { + unexportedField Bar + ExportedField map[interface{}]interface{} + } + */ + + // Setup some sample data structures for the example. + bar := Bar{uintptr(0)} + s1 := Foo{bar, map[interface{}]interface{}{"one": true}} + f := Flag(5) + b := []byte{ + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, + } + + // Dump! + spew.Dump(s1, f, b) + + // Output: + // (spew_test.Foo) { + // unexportedField: (spew_test.Bar) { + // data: (uintptr) + // }, + // ExportedField: (map[interface {}]interface {}) (len=1) { + // (string) (len=3) "one": (bool) true + // } + // } + // (spew_test.Flag) Unknown flag (5) + // ([]uint8) (len=34 cap=34) { + // 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | + // 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| + // 00000020 31 32 |12| + // } + // +} + +// This example demonstrates how to use Printf to display a variable with a +// format string and inline formatting. +func ExamplePrintf() { + // Create a double pointer to a uint 8. + ui8 := uint8(5) + pui8 := &ui8 + ppui8 := &pui8 + + // Create a circular data type. + type circular struct { + ui8 uint8 + c *circular + } + c := circular{ui8: 1} + c.c = &c + + // Print! + spew.Printf("ppui8: %v\n", ppui8) + spew.Printf("circular: %v\n", c) + + // Output: + // ppui8: <**>5 + // circular: {1 <*>{1 <*>}} +} + +// This example demonstrates how to use a ConfigState. +func ExampleConfigState() { + // Modify the indent level of the ConfigState only. The global + // configuration is not modified. + scs := spew.ConfigState{Indent: "\t"} + + // Output using the ConfigState instance. + v := map[string]int{"one": 1} + scs.Printf("v: %v\n", v) + scs.Dump(v) + + // Output: + // v: map[one:1] + // (map[string]int) (len=1) { + // (string) (len=3) "one": (int) 1 + // } +} + +// This example demonstrates how to use ConfigState.Dump to dump variables to +// stdout +func ExampleConfigState_Dump() { + // See the top-level Dump example for details on the types used in this + // example. + + // Create two ConfigState instances with different indentation. + scs := spew.ConfigState{Indent: "\t"} + scs2 := spew.ConfigState{Indent: " "} + + // Setup some sample data structures for the example. + bar := Bar{uintptr(0)} + s1 := Foo{bar, map[interface{}]interface{}{"one": true}} + + // Dump using the ConfigState instances. + scs.Dump(s1) + scs2.Dump(s1) + + // Output: + // (spew_test.Foo) { + // unexportedField: (spew_test.Bar) { + // data: (uintptr) + // }, + // ExportedField: (map[interface {}]interface {}) (len=1) { + // (string) (len=3) "one": (bool) true + // } + // } + // (spew_test.Foo) { + // unexportedField: (spew_test.Bar) { + // data: (uintptr) + // }, + // ExportedField: (map[interface {}]interface {}) (len=1) { + // (string) (len=3) "one": (bool) true + // } + // } + // +} + +// This example demonstrates how to use ConfigState.Printf to display a variable +// with a format string and inline formatting. +func ExampleConfigState_Printf() { + // See the top-level Dump example for details on the types used in this + // example. + + // Create two ConfigState instances and modify the method handling of the + // first ConfigState only. + scs := spew.NewDefaultConfig() + scs2 := spew.NewDefaultConfig() + scs.DisableMethods = true + + // Alternatively + // scs := spew.ConfigState{Indent: " ", DisableMethods: true} + // scs2 := spew.ConfigState{Indent: " "} + + // This is of type Flag which implements a Stringer and has raw value 1. + f := flagTwo + + // Dump using the ConfigState instances. + scs.Printf("f: %v\n", f) + scs2.Printf("f: %v\n", f) + + // Output: + // f: 1 + // f: flagTwo +} diff --git a/vendor/github.com/davecgh/go-spew/spew/format.go b/vendor/github.com/davecgh/go-spew/spew/format.go new file mode 100644 index 000000000..b04edb7d7 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/format.go @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" +) + +// supportedFlags is a list of all the character flags supported by fmt package. +const supportedFlags = "0-+# " + +// formatState implements the fmt.Formatter interface and contains information +// about the state of a formatting operation. The NewFormatter function can +// be used to get a new Formatter which can be used directly as arguments +// in standard fmt package printing calls. +type formatState struct { + value interface{} + fs fmt.State + depth int + pointers map[uintptr]int + ignoreNextType bool + cs *ConfigState +} + +// buildDefaultFormat recreates the original format string without precision +// and width information to pass in to fmt.Sprintf in the case of an +// unrecognized type. Unless new types are added to the language, this +// function won't ever be called. +func (f *formatState) buildDefaultFormat() (format string) { + buf := bytes.NewBuffer(percentBytes) + + for _, flag := range supportedFlags { + if f.fs.Flag(int(flag)) { + buf.WriteRune(flag) + } + } + + buf.WriteRune('v') + + format = buf.String() + return format +} + +// constructOrigFormat recreates the original format string including precision +// and width information to pass along to the standard fmt package. This allows +// automatic deferral of all format strings this package doesn't support. +func (f *formatState) constructOrigFormat(verb rune) (format string) { + buf := bytes.NewBuffer(percentBytes) + + for _, flag := range supportedFlags { + if f.fs.Flag(int(flag)) { + buf.WriteRune(flag) + } + } + + if width, ok := f.fs.Width(); ok { + buf.WriteString(strconv.Itoa(width)) + } + + if precision, ok := f.fs.Precision(); ok { + buf.Write(precisionBytes) + buf.WriteString(strconv.Itoa(precision)) + } + + buf.WriteRune(verb) + + format = buf.String() + return format +} + +// unpackValue returns values inside of non-nil interfaces when possible and +// ensures that types for values which have been unpacked from an interface +// are displayed when the show types flag is also set. +// This is useful for data types like structs, arrays, slices, and maps which +// can contain varying types packed inside an interface. +func (f *formatState) unpackValue(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Interface { + f.ignoreNextType = false + if !v.IsNil() { + v = v.Elem() + } + } + return v +} + +// formatPtr handles formatting of pointers by indirecting them as necessary. +func (f *formatState) formatPtr(v reflect.Value) { + // Display nil if top level pointer is nil. + showTypes := f.fs.Flag('#') + if v.IsNil() && (!showTypes || f.ignoreNextType) { + f.fs.Write(nilAngleBytes) + return + } + + // Remove pointers at or below the current depth from map used to detect + // circular refs. + for k, depth := range f.pointers { + if depth >= f.depth { + delete(f.pointers, k) + } + } + + // Keep list of all dereferenced pointers to possibly show later. + pointerChain := make([]uintptr, 0) + + // Figure out how many levels of indirection there are by derferencing + // pointers and unpacking interfaces down the chain while detecting circular + // references. + nilFound := false + cycleFound := false + indirects := 0 + ve := v + for ve.Kind() == reflect.Ptr { + if ve.IsNil() { + nilFound = true + break + } + indirects++ + addr := ve.Pointer() + pointerChain = append(pointerChain, addr) + if pd, ok := f.pointers[addr]; ok && pd < f.depth { + cycleFound = true + indirects-- + break + } + f.pointers[addr] = f.depth + + ve = ve.Elem() + if ve.Kind() == reflect.Interface { + if ve.IsNil() { + nilFound = true + break + } + ve = ve.Elem() + } + } + + // Display type or indirection level depending on flags. + if showTypes && !f.ignoreNextType { + f.fs.Write(openParenBytes) + f.fs.Write(bytes.Repeat(asteriskBytes, indirects)) + f.fs.Write([]byte(ve.Type().String())) + f.fs.Write(closeParenBytes) + } else { + if nilFound || cycleFound { + indirects += strings.Count(ve.Type().String(), "*") + } + f.fs.Write(openAngleBytes) + f.fs.Write([]byte(strings.Repeat("*", indirects))) + f.fs.Write(closeAngleBytes) + } + + // Display pointer information depending on flags. + if f.fs.Flag('+') && (len(pointerChain) > 0) { + f.fs.Write(openParenBytes) + for i, addr := range pointerChain { + if i > 0 { + f.fs.Write(pointerChainBytes) + } + printHexPtr(f.fs, addr) + } + f.fs.Write(closeParenBytes) + } + + // Display dereferenced value. + switch { + case nilFound: + f.fs.Write(nilAngleBytes) + + case cycleFound: + f.fs.Write(circularShortBytes) + + default: + f.ignoreNextType = true + f.format(ve) + } +} + +// format is the main workhorse for providing the Formatter interface. It +// uses the passed reflect value to figure out what kind of object we are +// dealing with and formats it appropriately. It is a recursive function, +// however circular data structures are detected and handled properly. +func (f *formatState) format(v reflect.Value) { + // Handle invalid reflect values immediately. + kind := v.Kind() + if kind == reflect.Invalid { + f.fs.Write(invalidAngleBytes) + return + } + + // Handle pointers specially. + if kind == reflect.Ptr { + f.formatPtr(v) + return + } + + // Print type information unless already handled elsewhere. + if !f.ignoreNextType && f.fs.Flag('#') { + f.fs.Write(openParenBytes) + f.fs.Write([]byte(v.Type().String())) + f.fs.Write(closeParenBytes) + } + f.ignoreNextType = false + + // Call Stringer/error interfaces if they exist and the handle methods + // flag is enabled. + if !f.cs.DisableMethods { + if (kind != reflect.Invalid) && (kind != reflect.Interface) { + if handled := handleMethods(f.cs, f.fs, v); handled { + return + } + } + } + + switch kind { + case reflect.Invalid: + // Do nothing. We should never get here since invalid has already + // been handled above. + + case reflect.Bool: + printBool(f.fs, v.Bool()) + + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + printInt(f.fs, v.Int(), 10) + + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + printUint(f.fs, v.Uint(), 10) + + case reflect.Float32: + printFloat(f.fs, v.Float(), 32) + + case reflect.Float64: + printFloat(f.fs, v.Float(), 64) + + case reflect.Complex64: + printComplex(f.fs, v.Complex(), 32) + + case reflect.Complex128: + printComplex(f.fs, v.Complex(), 64) + + case reflect.Slice: + if v.IsNil() { + f.fs.Write(nilAngleBytes) + break + } + fallthrough + + case reflect.Array: + f.fs.Write(openBracketBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + numEntries := v.Len() + for i := 0; i < numEntries; i++ { + if i > 0 { + f.fs.Write(spaceBytes) + } + f.ignoreNextType = true + f.format(f.unpackValue(v.Index(i))) + } + } + f.depth-- + f.fs.Write(closeBracketBytes) + + case reflect.String: + f.fs.Write([]byte(v.String())) + + case reflect.Interface: + // The only time we should get here is for nil interfaces due to + // unpackValue calls. + if v.IsNil() { + f.fs.Write(nilAngleBytes) + } + + case reflect.Ptr: + // Do nothing. We should never get here since pointers have already + // been handled above. + + case reflect.Map: + // nil maps should be indicated as different than empty maps + if v.IsNil() { + f.fs.Write(nilAngleBytes) + break + } + + f.fs.Write(openMapBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + keys := v.MapKeys() + if f.cs.SortKeys { + sortValues(keys, f.cs) + } + for i, key := range keys { + if i > 0 { + f.fs.Write(spaceBytes) + } + f.ignoreNextType = true + f.format(f.unpackValue(key)) + f.fs.Write(colonBytes) + f.ignoreNextType = true + f.format(f.unpackValue(v.MapIndex(key))) + } + } + f.depth-- + f.fs.Write(closeMapBytes) + + case reflect.Struct: + numFields := v.NumField() + f.fs.Write(openBraceBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + vt := v.Type() + for i := 0; i < numFields; i++ { + if i > 0 { + f.fs.Write(spaceBytes) + } + vtf := vt.Field(i) + if f.fs.Flag('+') || f.fs.Flag('#') { + f.fs.Write([]byte(vtf.Name)) + f.fs.Write(colonBytes) + } + f.format(f.unpackValue(v.Field(i))) + } + } + f.depth-- + f.fs.Write(closeBraceBytes) + + case reflect.Uintptr: + printHexPtr(f.fs, uintptr(v.Uint())) + + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + printHexPtr(f.fs, v.Pointer()) + + // There were not any other types at the time this code was written, but + // fall back to letting the default fmt package handle it if any get added. + default: + format := f.buildDefaultFormat() + if v.CanInterface() { + fmt.Fprintf(f.fs, format, v.Interface()) + } else { + fmt.Fprintf(f.fs, format, v.String()) + } + } +} + +// Format satisfies the fmt.Formatter interface. See NewFormatter for usage +// details. +func (f *formatState) Format(fs fmt.State, verb rune) { + f.fs = fs + + // Use standard formatting for verbs that are not v. + if verb != 'v' { + format := f.constructOrigFormat(verb) + fmt.Fprintf(fs, format, f.value) + return + } + + if f.value == nil { + if fs.Flag('#') { + fs.Write(interfaceBytes) + } + fs.Write(nilAngleBytes) + return + } + + f.format(reflect.ValueOf(f.value)) +} + +// newFormatter is a helper function to consolidate the logic from the various +// public methods which take varying config states. +func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter { + fs := &formatState{value: v, cs: cs} + fs.pointers = make(map[uintptr]int) + return fs +} + +/* +NewFormatter returns a custom formatter that satisfies the fmt.Formatter +interface. As a result, it integrates cleanly with standard fmt package +printing functions. The formatter is useful for inline printing of smaller data +types similar to the standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Typically this function shouldn't be called directly. It is much easier to make +use of the custom formatter by calling one of the convenience functions such as +Printf, Println, or Fprintf. +*/ +func NewFormatter(v interface{}) fmt.Formatter { + return newFormatter(&Config, v) +} diff --git a/vendor/github.com/davecgh/go-spew/spew/format_test.go b/vendor/github.com/davecgh/go-spew/spew/format_test.go new file mode 100644 index 000000000..87ee9651e --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/format_test.go @@ -0,0 +1,1558 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +Test Summary: +NOTE: For each test, a nil pointer, a single pointer and double pointer to the +base test element are also tested to ensure proper indirection across all types. + +- Max int8, int16, int32, int64, int +- Max uint8, uint16, uint32, uint64, uint +- Boolean true and false +- Standard complex64 and complex128 +- Array containing standard ints +- Array containing type with custom formatter on pointer receiver only +- Array containing interfaces +- Slice containing standard float32 values +- Slice containing type with custom formatter on pointer receiver only +- Slice containing interfaces +- Nil slice +- Standard string +- Nil interface +- Sub-interface +- Map with string keys and int vals +- Map with custom formatter type on pointer receiver only keys and vals +- Map with interface keys and values +- Map with nil interface value +- Struct with primitives +- Struct that contains another struct +- Struct that contains custom type with Stringer pointer interface via both + exported and unexported fields +- Struct that contains embedded struct and field to same struct +- Uintptr to 0 (null pointer) +- Uintptr address of real variable +- Unsafe.Pointer to 0 (null pointer) +- Unsafe.Pointer to address of real variable +- Nil channel +- Standard int channel +- Function with no params and no returns +- Function with param and no returns +- Function with multiple params and multiple returns +- Struct that is circular through self referencing +- Structs that are circular through cross referencing +- Structs that are indirectly circular +- Type that panics in its Stringer interface +- Type that has a custom Error interface +- %x passthrough with uint +- %#x passthrough with uint +- %f passthrough with precision +- %f passthrough with width and precision +- %d passthrough with width +- %q passthrough with string +*/ + +package spew_test + +import ( + "bytes" + "fmt" + "testing" + "unsafe" + + "github.com/davecgh/go-spew/spew" +) + +// formatterTest is used to describe a test to be performed against NewFormatter. +type formatterTest struct { + format string + in interface{} + wants []string +} + +// formatterTests houses all of the tests to be performed against NewFormatter. +var formatterTests = make([]formatterTest, 0) + +// addFormatterTest is a helper method to append the passed input and desired +// result to formatterTests. +func addFormatterTest(format string, in interface{}, wants ...string) { + test := formatterTest{format, in, wants} + formatterTests = append(formatterTests, test) +} + +func addIntFormatterTests() { + // Max int8. + v := int8(127) + nv := (*int8)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "int8" + vs := "127" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Max int16. + v2 := int16(32767) + nv2 := (*int16)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "int16" + v2s := "32767" + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%v", nv2, "") + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#v", nv2, "(*"+v2t+")"+"") + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") + + // Max int32. + v3 := int32(2147483647) + nv3 := (*int32)(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "int32" + v3s := "2147483647" + addFormatterTest("%v", v3, v3s) + addFormatterTest("%v", pv3, "<*>"+v3s) + addFormatterTest("%v", &pv3, "<**>"+v3s) + addFormatterTest("%v", nv3, "") + addFormatterTest("%+v", v3, v3s) + addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s) + addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%#v", v3, "("+v3t+")"+v3s) + addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s) + addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s) + addFormatterTest("%#v", nv3, "(*"+v3t+")"+"") + addFormatterTest("%#+v", v3, "("+v3t+")"+v3s) + addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s) + addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s) + addFormatterTest("%#v", nv3, "(*"+v3t+")"+"") + + // Max int64. + v4 := int64(9223372036854775807) + nv4 := (*int64)(nil) + pv4 := &v4 + v4Addr := fmt.Sprintf("%p", pv4) + pv4Addr := fmt.Sprintf("%p", &pv4) + v4t := "int64" + v4s := "9223372036854775807" + addFormatterTest("%v", v4, v4s) + addFormatterTest("%v", pv4, "<*>"+v4s) + addFormatterTest("%v", &pv4, "<**>"+v4s) + addFormatterTest("%v", nv4, "") + addFormatterTest("%+v", v4, v4s) + addFormatterTest("%+v", pv4, "<*>("+v4Addr+")"+v4s) + addFormatterTest("%+v", &pv4, "<**>("+pv4Addr+"->"+v4Addr+")"+v4s) + addFormatterTest("%+v", nv4, "") + addFormatterTest("%#v", v4, "("+v4t+")"+v4s) + addFormatterTest("%#v", pv4, "(*"+v4t+")"+v4s) + addFormatterTest("%#v", &pv4, "(**"+v4t+")"+v4s) + addFormatterTest("%#v", nv4, "(*"+v4t+")"+"") + addFormatterTest("%#+v", v4, "("+v4t+")"+v4s) + addFormatterTest("%#+v", pv4, "(*"+v4t+")("+v4Addr+")"+v4s) + addFormatterTest("%#+v", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")"+v4s) + addFormatterTest("%#+v", nv4, "(*"+v4t+")"+"") + + // Max int. + v5 := int(2147483647) + nv5 := (*int)(nil) + pv5 := &v5 + v5Addr := fmt.Sprintf("%p", pv5) + pv5Addr := fmt.Sprintf("%p", &pv5) + v5t := "int" + v5s := "2147483647" + addFormatterTest("%v", v5, v5s) + addFormatterTest("%v", pv5, "<*>"+v5s) + addFormatterTest("%v", &pv5, "<**>"+v5s) + addFormatterTest("%v", nv5, "") + addFormatterTest("%+v", v5, v5s) + addFormatterTest("%+v", pv5, "<*>("+v5Addr+")"+v5s) + addFormatterTest("%+v", &pv5, "<**>("+pv5Addr+"->"+v5Addr+")"+v5s) + addFormatterTest("%+v", nv5, "") + addFormatterTest("%#v", v5, "("+v5t+")"+v5s) + addFormatterTest("%#v", pv5, "(*"+v5t+")"+v5s) + addFormatterTest("%#v", &pv5, "(**"+v5t+")"+v5s) + addFormatterTest("%#v", nv5, "(*"+v5t+")"+"") + addFormatterTest("%#+v", v5, "("+v5t+")"+v5s) + addFormatterTest("%#+v", pv5, "(*"+v5t+")("+v5Addr+")"+v5s) + addFormatterTest("%#+v", &pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")"+v5s) + addFormatterTest("%#+v", nv5, "(*"+v5t+")"+"") +} + +func addUintFormatterTests() { + // Max uint8. + v := uint8(255) + nv := (*uint8)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "uint8" + vs := "255" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Max uint16. + v2 := uint16(65535) + nv2 := (*uint16)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "uint16" + v2s := "65535" + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%v", nv2, "") + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#v", nv2, "(*"+v2t+")"+"") + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") + + // Max uint32. + v3 := uint32(4294967295) + nv3 := (*uint32)(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "uint32" + v3s := "4294967295" + addFormatterTest("%v", v3, v3s) + addFormatterTest("%v", pv3, "<*>"+v3s) + addFormatterTest("%v", &pv3, "<**>"+v3s) + addFormatterTest("%v", nv3, "") + addFormatterTest("%+v", v3, v3s) + addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s) + addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%#v", v3, "("+v3t+")"+v3s) + addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s) + addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s) + addFormatterTest("%#v", nv3, "(*"+v3t+")"+"") + addFormatterTest("%#+v", v3, "("+v3t+")"+v3s) + addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s) + addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s) + addFormatterTest("%#v", nv3, "(*"+v3t+")"+"") + + // Max uint64. + v4 := uint64(18446744073709551615) + nv4 := (*uint64)(nil) + pv4 := &v4 + v4Addr := fmt.Sprintf("%p", pv4) + pv4Addr := fmt.Sprintf("%p", &pv4) + v4t := "uint64" + v4s := "18446744073709551615" + addFormatterTest("%v", v4, v4s) + addFormatterTest("%v", pv4, "<*>"+v4s) + addFormatterTest("%v", &pv4, "<**>"+v4s) + addFormatterTest("%v", nv4, "") + addFormatterTest("%+v", v4, v4s) + addFormatterTest("%+v", pv4, "<*>("+v4Addr+")"+v4s) + addFormatterTest("%+v", &pv4, "<**>("+pv4Addr+"->"+v4Addr+")"+v4s) + addFormatterTest("%+v", nv4, "") + addFormatterTest("%#v", v4, "("+v4t+")"+v4s) + addFormatterTest("%#v", pv4, "(*"+v4t+")"+v4s) + addFormatterTest("%#v", &pv4, "(**"+v4t+")"+v4s) + addFormatterTest("%#v", nv4, "(*"+v4t+")"+"") + addFormatterTest("%#+v", v4, "("+v4t+")"+v4s) + addFormatterTest("%#+v", pv4, "(*"+v4t+")("+v4Addr+")"+v4s) + addFormatterTest("%#+v", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")"+v4s) + addFormatterTest("%#+v", nv4, "(*"+v4t+")"+"") + + // Max uint. + v5 := uint(4294967295) + nv5 := (*uint)(nil) + pv5 := &v5 + v5Addr := fmt.Sprintf("%p", pv5) + pv5Addr := fmt.Sprintf("%p", &pv5) + v5t := "uint" + v5s := "4294967295" + addFormatterTest("%v", v5, v5s) + addFormatterTest("%v", pv5, "<*>"+v5s) + addFormatterTest("%v", &pv5, "<**>"+v5s) + addFormatterTest("%v", nv5, "") + addFormatterTest("%+v", v5, v5s) + addFormatterTest("%+v", pv5, "<*>("+v5Addr+")"+v5s) + addFormatterTest("%+v", &pv5, "<**>("+pv5Addr+"->"+v5Addr+")"+v5s) + addFormatterTest("%+v", nv5, "") + addFormatterTest("%#v", v5, "("+v5t+")"+v5s) + addFormatterTest("%#v", pv5, "(*"+v5t+")"+v5s) + addFormatterTest("%#v", &pv5, "(**"+v5t+")"+v5s) + addFormatterTest("%#v", nv5, "(*"+v5t+")"+"") + addFormatterTest("%#+v", v5, "("+v5t+")"+v5s) + addFormatterTest("%#+v", pv5, "(*"+v5t+")("+v5Addr+")"+v5s) + addFormatterTest("%#+v", &pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")"+v5s) + addFormatterTest("%#v", nv5, "(*"+v5t+")"+"") +} + +func addBoolFormatterTests() { + // Boolean true. + v := bool(true) + nv := (*bool)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "bool" + vs := "true" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Boolean false. + v2 := bool(false) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "bool" + v2s := "false" + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) +} + +func addFloatFormatterTests() { + // Standard float32. + v := float32(3.1415) + nv := (*float32)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "float32" + vs := "3.1415" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Standard float64. + v2 := float64(3.1415926) + nv2 := (*float64)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "float64" + v2s := "3.1415926" + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#v", nv2, "(*"+v2t+")"+"") + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") +} + +func addComplexFormatterTests() { + // Standard complex64. + v := complex(float32(6), -2) + nv := (*complex64)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "complex64" + vs := "(6-2i)" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Standard complex128. + v2 := complex(float64(-6), 2) + nv2 := (*complex128)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "complex128" + v2s := "(-6+2i)" + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#v", nv2, "(*"+v2t+")"+"") + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") +} + +func addArrayFormatterTests() { + // Array containing standard ints. + v := [3]int{1, 2, 3} + nv := (*[3]int)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "[3]int" + vs := "[1 2 3]" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Array containing type with custom formatter on pointer receiver only. + v2 := [3]pstringer{"1", "2", "3"} + nv2 := (*[3]pstringer)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "[3]spew_test.pstringer" + v2sp := "[stringer 1 stringer 2 stringer 3]" + v2s := v2sp + if spew.UnsafeDisabled { + v2s = "[1 2 3]" + } + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2sp) + addFormatterTest("%v", &pv2, "<**>"+v2sp) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2sp) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2sp) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2sp) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2sp) + addFormatterTest("%#v", nv2, "(*"+v2t+")"+"") + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2sp) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2sp) + addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") + + // Array containing interfaces. + v3 := [3]interface{}{"one", int(2), uint(3)} + nv3 := (*[3]interface{})(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "[3]interface {}" + v3t2 := "string" + v3t3 := "int" + v3t4 := "uint" + v3s := "[one 2 3]" + v3s2 := "[(" + v3t2 + ")one (" + v3t3 + ")2 (" + v3t4 + ")3]" + addFormatterTest("%v", v3, v3s) + addFormatterTest("%v", pv3, "<*>"+v3s) + addFormatterTest("%v", &pv3, "<**>"+v3s) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%+v", v3, v3s) + addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s) + addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%#v", v3, "("+v3t+")"+v3s2) + addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s2) + addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s2) + addFormatterTest("%#v", nv3, "(*"+v3t+")"+"") + addFormatterTest("%#+v", v3, "("+v3t+")"+v3s2) + addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s2) + addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s2) + addFormatterTest("%#+v", nv3, "(*"+v3t+")"+"") +} + +func addSliceFormatterTests() { + // Slice containing standard float32 values. + v := []float32{3.14, 6.28, 12.56} + nv := (*[]float32)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "[]float32" + vs := "[3.14 6.28 12.56]" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Slice containing type with custom formatter on pointer receiver only. + v2 := []pstringer{"1", "2", "3"} + nv2 := (*[]pstringer)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "[]spew_test.pstringer" + v2s := "[stringer 1 stringer 2 stringer 3]" + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#v", nv2, "(*"+v2t+")"+"") + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") + + // Slice containing interfaces. + v3 := []interface{}{"one", int(2), uint(3), nil} + nv3 := (*[]interface{})(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "[]interface {}" + v3t2 := "string" + v3t3 := "int" + v3t4 := "uint" + v3t5 := "interface {}" + v3s := "[one 2 3 ]" + v3s2 := "[(" + v3t2 + ")one (" + v3t3 + ")2 (" + v3t4 + ")3 (" + v3t5 + + ")]" + addFormatterTest("%v", v3, v3s) + addFormatterTest("%v", pv3, "<*>"+v3s) + addFormatterTest("%v", &pv3, "<**>"+v3s) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%+v", v3, v3s) + addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s) + addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%#v", v3, "("+v3t+")"+v3s2) + addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s2) + addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s2) + addFormatterTest("%#v", nv3, "(*"+v3t+")"+"") + addFormatterTest("%#+v", v3, "("+v3t+")"+v3s2) + addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s2) + addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s2) + addFormatterTest("%#+v", nv3, "(*"+v3t+")"+"") + + // Nil slice. + var v4 []int + nv4 := (*[]int)(nil) + pv4 := &v4 + v4Addr := fmt.Sprintf("%p", pv4) + pv4Addr := fmt.Sprintf("%p", &pv4) + v4t := "[]int" + v4s := "" + addFormatterTest("%v", v4, v4s) + addFormatterTest("%v", pv4, "<*>"+v4s) + addFormatterTest("%v", &pv4, "<**>"+v4s) + addFormatterTest("%+v", nv4, "") + addFormatterTest("%+v", v4, v4s) + addFormatterTest("%+v", pv4, "<*>("+v4Addr+")"+v4s) + addFormatterTest("%+v", &pv4, "<**>("+pv4Addr+"->"+v4Addr+")"+v4s) + addFormatterTest("%+v", nv4, "") + addFormatterTest("%#v", v4, "("+v4t+")"+v4s) + addFormatterTest("%#v", pv4, "(*"+v4t+")"+v4s) + addFormatterTest("%#v", &pv4, "(**"+v4t+")"+v4s) + addFormatterTest("%#v", nv4, "(*"+v4t+")"+"") + addFormatterTest("%#+v", v4, "("+v4t+")"+v4s) + addFormatterTest("%#+v", pv4, "(*"+v4t+")("+v4Addr+")"+v4s) + addFormatterTest("%#+v", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")"+v4s) + addFormatterTest("%#+v", nv4, "(*"+v4t+")"+"") +} + +func addStringFormatterTests() { + // Standard string. + v := "test" + nv := (*string)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "string" + vs := "test" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") +} + +func addInterfaceFormatterTests() { + // Nil interface. + var v interface{} + nv := (*interface{})(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "interface {}" + vs := "" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Sub-interface. + v2 := interface{}(uint16(65535)) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "uint16" + v2s := "65535" + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) +} + +func addMapFormatterTests() { + // Map with string keys and int vals. + v := map[string]int{"one": 1, "two": 2} + nilMap := map[string]int(nil) + nv := (*map[string]int)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "map[string]int" + vs := "map[one:1 two:2]" + vs2 := "map[two:2 one:1]" + addFormatterTest("%v", v, vs, vs2) + addFormatterTest("%v", pv, "<*>"+vs, "<*>"+vs2) + addFormatterTest("%v", &pv, "<**>"+vs, "<**>"+vs2) + addFormatterTest("%+v", nilMap, "") + addFormatterTest("%+v", nv, "") + addFormatterTest("%+v", v, vs, vs2) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs, "<*>("+vAddr+")"+vs2) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs, + "<**>("+pvAddr+"->"+vAddr+")"+vs2) + addFormatterTest("%+v", nilMap, "") + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs, "("+vt+")"+vs2) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs, "(*"+vt+")"+vs2) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs, "(**"+vt+")"+vs2) + addFormatterTest("%#v", nilMap, "("+vt+")"+"") + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs, "("+vt+")"+vs2) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs, + "(*"+vt+")("+vAddr+")"+vs2) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs, + "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs2) + addFormatterTest("%#+v", nilMap, "("+vt+")"+"") + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Map with custom formatter type on pointer receiver only keys and vals. + v2 := map[pstringer]pstringer{"one": "1"} + nv2 := (*map[pstringer]pstringer)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "map[spew_test.pstringer]spew_test.pstringer" + v2s := "map[stringer one:stringer 1]" + if spew.UnsafeDisabled { + v2s = "map[one:1]" + } + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#v", nv2, "(*"+v2t+")"+"") + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") + + // Map with interface keys and values. + v3 := map[interface{}]interface{}{"one": 1} + nv3 := (*map[interface{}]interface{})(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "map[interface {}]interface {}" + v3t1 := "string" + v3t2 := "int" + v3s := "map[one:1]" + v3s2 := "map[(" + v3t1 + ")one:(" + v3t2 + ")1]" + addFormatterTest("%v", v3, v3s) + addFormatterTest("%v", pv3, "<*>"+v3s) + addFormatterTest("%v", &pv3, "<**>"+v3s) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%+v", v3, v3s) + addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s) + addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%#v", v3, "("+v3t+")"+v3s2) + addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s2) + addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s2) + addFormatterTest("%#v", nv3, "(*"+v3t+")"+"") + addFormatterTest("%#+v", v3, "("+v3t+")"+v3s2) + addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s2) + addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s2) + addFormatterTest("%#+v", nv3, "(*"+v3t+")"+"") + + // Map with nil interface value + v4 := map[string]interface{}{"nil": nil} + nv4 := (*map[string]interface{})(nil) + pv4 := &v4 + v4Addr := fmt.Sprintf("%p", pv4) + pv4Addr := fmt.Sprintf("%p", &pv4) + v4t := "map[string]interface {}" + v4t1 := "interface {}" + v4s := "map[nil:]" + v4s2 := "map[nil:(" + v4t1 + ")]" + addFormatterTest("%v", v4, v4s) + addFormatterTest("%v", pv4, "<*>"+v4s) + addFormatterTest("%v", &pv4, "<**>"+v4s) + addFormatterTest("%+v", nv4, "") + addFormatterTest("%+v", v4, v4s) + addFormatterTest("%+v", pv4, "<*>("+v4Addr+")"+v4s) + addFormatterTest("%+v", &pv4, "<**>("+pv4Addr+"->"+v4Addr+")"+v4s) + addFormatterTest("%+v", nv4, "") + addFormatterTest("%#v", v4, "("+v4t+")"+v4s2) + addFormatterTest("%#v", pv4, "(*"+v4t+")"+v4s2) + addFormatterTest("%#v", &pv4, "(**"+v4t+")"+v4s2) + addFormatterTest("%#v", nv4, "(*"+v4t+")"+"") + addFormatterTest("%#+v", v4, "("+v4t+")"+v4s2) + addFormatterTest("%#+v", pv4, "(*"+v4t+")("+v4Addr+")"+v4s2) + addFormatterTest("%#+v", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")"+v4s2) + addFormatterTest("%#+v", nv4, "(*"+v4t+")"+"") +} + +func addStructFormatterTests() { + // Struct with primitives. + type s1 struct { + a int8 + b uint8 + } + v := s1{127, 255} + nv := (*s1)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "spew_test.s1" + vt2 := "int8" + vt3 := "uint8" + vs := "{127 255}" + vs2 := "{a:127 b:255}" + vs3 := "{a:(" + vt2 + ")127 b:(" + vt3 + ")255}" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%+v", v, vs2) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs2) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs2) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs3) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs3) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs3) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs3) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs3) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs3) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Struct that contains another struct. + type s2 struct { + s1 s1 + b bool + } + v2 := s2{s1{127, 255}, true} + nv2 := (*s2)(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "spew_test.s2" + v2t2 := "spew_test.s1" + v2t3 := "int8" + v2t4 := "uint8" + v2t5 := "bool" + v2s := "{{127 255} true}" + v2s2 := "{s1:{a:127 b:255} b:true}" + v2s3 := "{s1:(" + v2t2 + "){a:(" + v2t3 + ")127 b:(" + v2t4 + ")255} b:(" + + v2t5 + ")true}" + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%+v", v2, v2s2) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s2) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s2) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%#v", v2, "("+v2t+")"+v2s3) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s3) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s3) + addFormatterTest("%#v", nv2, "(*"+v2t+")"+"") + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s3) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s3) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s3) + addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") + + // Struct that contains custom type with Stringer pointer interface via both + // exported and unexported fields. + type s3 struct { + s pstringer + S pstringer + } + v3 := s3{"test", "test2"} + nv3 := (*s3)(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "spew_test.s3" + v3t2 := "spew_test.pstringer" + v3s := "{stringer test stringer test2}" + v3sp := v3s + v3s2 := "{s:stringer test S:stringer test2}" + v3s2p := v3s2 + v3s3 := "{s:(" + v3t2 + ")stringer test S:(" + v3t2 + ")stringer test2}" + v3s3p := v3s3 + if spew.UnsafeDisabled { + v3s = "{test test2}" + v3sp = "{test stringer test2}" + v3s2 = "{s:test S:test2}" + v3s2p = "{s:test S:stringer test2}" + v3s3 = "{s:(" + v3t2 + ")test S:(" + v3t2 + ")test2}" + v3s3p = "{s:(" + v3t2 + ")test S:(" + v3t2 + ")stringer test2}" + } + addFormatterTest("%v", v3, v3s) + addFormatterTest("%v", pv3, "<*>"+v3sp) + addFormatterTest("%v", &pv3, "<**>"+v3sp) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%+v", v3, v3s2) + addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s2p) + addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s2p) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%#v", v3, "("+v3t+")"+v3s3) + addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s3p) + addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s3p) + addFormatterTest("%#v", nv3, "(*"+v3t+")"+"") + addFormatterTest("%#+v", v3, "("+v3t+")"+v3s3) + addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s3p) + addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s3p) + addFormatterTest("%#+v", nv3, "(*"+v3t+")"+"") + + // Struct that contains embedded struct and field to same struct. + e := embed{"embedstr"} + v4 := embedwrap{embed: &e, e: &e} + nv4 := (*embedwrap)(nil) + pv4 := &v4 + eAddr := fmt.Sprintf("%p", &e) + v4Addr := fmt.Sprintf("%p", pv4) + pv4Addr := fmt.Sprintf("%p", &pv4) + v4t := "spew_test.embedwrap" + v4t2 := "spew_test.embed" + v4t3 := "string" + v4s := "{<*>{embedstr} <*>{embedstr}}" + v4s2 := "{embed:<*>(" + eAddr + "){a:embedstr} e:<*>(" + eAddr + + "){a:embedstr}}" + v4s3 := "{embed:(*" + v4t2 + "){a:(" + v4t3 + ")embedstr} e:(*" + v4t2 + + "){a:(" + v4t3 + ")embedstr}}" + v4s4 := "{embed:(*" + v4t2 + ")(" + eAddr + "){a:(" + v4t3 + + ")embedstr} e:(*" + v4t2 + ")(" + eAddr + "){a:(" + v4t3 + ")embedstr}}" + addFormatterTest("%v", v4, v4s) + addFormatterTest("%v", pv4, "<*>"+v4s) + addFormatterTest("%v", &pv4, "<**>"+v4s) + addFormatterTest("%+v", nv4, "") + addFormatterTest("%+v", v4, v4s2) + addFormatterTest("%+v", pv4, "<*>("+v4Addr+")"+v4s2) + addFormatterTest("%+v", &pv4, "<**>("+pv4Addr+"->"+v4Addr+")"+v4s2) + addFormatterTest("%+v", nv4, "") + addFormatterTest("%#v", v4, "("+v4t+")"+v4s3) + addFormatterTest("%#v", pv4, "(*"+v4t+")"+v4s3) + addFormatterTest("%#v", &pv4, "(**"+v4t+")"+v4s3) + addFormatterTest("%#v", nv4, "(*"+v4t+")"+"") + addFormatterTest("%#+v", v4, "("+v4t+")"+v4s4) + addFormatterTest("%#+v", pv4, "(*"+v4t+")("+v4Addr+")"+v4s4) + addFormatterTest("%#+v", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")"+v4s4) + addFormatterTest("%#+v", nv4, "(*"+v4t+")"+"") +} + +func addUintptrFormatterTests() { + // Null pointer. + v := uintptr(0) + nv := (*uintptr)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "uintptr" + vs := "" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Address of real variable. + i := 1 + v2 := uintptr(unsafe.Pointer(&i)) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "uintptr" + v2s := fmt.Sprintf("%p", &i) + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) +} + +func addUnsafePointerFormatterTests() { + // Null pointer. + v := unsafe.Pointer(nil) + nv := (*unsafe.Pointer)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "unsafe.Pointer" + vs := "" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Address of real variable. + i := 1 + v2 := unsafe.Pointer(&i) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "unsafe.Pointer" + v2s := fmt.Sprintf("%p", &i) + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) +} + +func addChanFormatterTests() { + // Nil channel. + var v chan int + pv := &v + nv := (*chan int)(nil) + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "chan int" + vs := "" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Real channel. + v2 := make(chan int) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "chan int" + v2s := fmt.Sprintf("%p", v2) + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) +} + +func addFuncFormatterTests() { + // Function with no params and no returns. + v := addIntFormatterTests + nv := (*func())(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "func()" + vs := fmt.Sprintf("%p", v) + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") + + // Function with param and no returns. + v2 := TestFormatter + nv2 := (*func(*testing.T))(nil) + pv2 := &v2 + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "func(*testing.T)" + v2s := fmt.Sprintf("%p", v2) + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s) + addFormatterTest("%v", &pv2, "<**>"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%+v", v2, v2s) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%+v", nv2, "") + addFormatterTest("%#v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s) + addFormatterTest("%#v", nv2, "(*"+v2t+")"+"") + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s) + addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"") + + // Function with multiple params and multiple returns. + var v3 = func(i int, s string) (b bool, err error) { + return true, nil + } + nv3 := (*func(int, string) (bool, error))(nil) + pv3 := &v3 + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "func(int, string) (bool, error)" + v3s := fmt.Sprintf("%p", v3) + addFormatterTest("%v", v3, v3s) + addFormatterTest("%v", pv3, "<*>"+v3s) + addFormatterTest("%v", &pv3, "<**>"+v3s) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%+v", v3, v3s) + addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s) + addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s) + addFormatterTest("%+v", nv3, "") + addFormatterTest("%#v", v3, "("+v3t+")"+v3s) + addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s) + addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s) + addFormatterTest("%#v", nv3, "(*"+v3t+")"+"") + addFormatterTest("%#+v", v3, "("+v3t+")"+v3s) + addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s) + addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s) + addFormatterTest("%#+v", nv3, "(*"+v3t+")"+"") +} + +func addCircularFormatterTests() { + // Struct that is circular through self referencing. + type circular struct { + c *circular + } + v := circular{nil} + v.c = &v + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "spew_test.circular" + vs := "{<*>{<*>}}" + vs2 := "{<*>}" + vs3 := "{c:<*>(" + vAddr + "){c:<*>(" + vAddr + ")}}" + vs4 := "{c:<*>(" + vAddr + ")}" + vs5 := "{c:(*" + vt + "){c:(*" + vt + ")}}" + vs6 := "{c:(*" + vt + ")}" + vs7 := "{c:(*" + vt + ")(" + vAddr + "){c:(*" + vt + ")(" + vAddr + + ")}}" + vs8 := "{c:(*" + vt + ")(" + vAddr + ")}" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs2) + addFormatterTest("%v", &pv, "<**>"+vs2) + addFormatterTest("%+v", v, vs3) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs4) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs4) + addFormatterTest("%#v", v, "("+vt+")"+vs5) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs6) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs6) + addFormatterTest("%#+v", v, "("+vt+")"+vs7) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs8) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs8) + + // Structs that are circular through cross referencing. + v2 := xref1{nil} + ts2 := xref2{&v2} + v2.ps2 = &ts2 + pv2 := &v2 + ts2Addr := fmt.Sprintf("%p", &ts2) + v2Addr := fmt.Sprintf("%p", pv2) + pv2Addr := fmt.Sprintf("%p", &pv2) + v2t := "spew_test.xref1" + v2t2 := "spew_test.xref2" + v2s := "{<*>{<*>{<*>}}}" + v2s2 := "{<*>{<*>}}" + v2s3 := "{ps2:<*>(" + ts2Addr + "){ps1:<*>(" + v2Addr + "){ps2:<*>(" + + ts2Addr + ")}}}" + v2s4 := "{ps2:<*>(" + ts2Addr + "){ps1:<*>(" + v2Addr + ")}}" + v2s5 := "{ps2:(*" + v2t2 + "){ps1:(*" + v2t + "){ps2:(*" + v2t2 + + ")}}}" + v2s6 := "{ps2:(*" + v2t2 + "){ps1:(*" + v2t + ")}}" + v2s7 := "{ps2:(*" + v2t2 + ")(" + ts2Addr + "){ps1:(*" + v2t + + ")(" + v2Addr + "){ps2:(*" + v2t2 + ")(" + ts2Addr + + ")}}}" + v2s8 := "{ps2:(*" + v2t2 + ")(" + ts2Addr + "){ps1:(*" + v2t + + ")(" + v2Addr + ")}}" + addFormatterTest("%v", v2, v2s) + addFormatterTest("%v", pv2, "<*>"+v2s2) + addFormatterTest("%v", &pv2, "<**>"+v2s2) + addFormatterTest("%+v", v2, v2s3) + addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s4) + addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s4) + addFormatterTest("%#v", v2, "("+v2t+")"+v2s5) + addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s6) + addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s6) + addFormatterTest("%#+v", v2, "("+v2t+")"+v2s7) + addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s8) + addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s8) + + // Structs that are indirectly circular. + v3 := indirCir1{nil} + tic2 := indirCir2{nil} + tic3 := indirCir3{&v3} + tic2.ps3 = &tic3 + v3.ps2 = &tic2 + pv3 := &v3 + tic2Addr := fmt.Sprintf("%p", &tic2) + tic3Addr := fmt.Sprintf("%p", &tic3) + v3Addr := fmt.Sprintf("%p", pv3) + pv3Addr := fmt.Sprintf("%p", &pv3) + v3t := "spew_test.indirCir1" + v3t2 := "spew_test.indirCir2" + v3t3 := "spew_test.indirCir3" + v3s := "{<*>{<*>{<*>{<*>}}}}" + v3s2 := "{<*>{<*>{<*>}}}" + v3s3 := "{ps2:<*>(" + tic2Addr + "){ps3:<*>(" + tic3Addr + "){ps1:<*>(" + + v3Addr + "){ps2:<*>(" + tic2Addr + ")}}}}" + v3s4 := "{ps2:<*>(" + tic2Addr + "){ps3:<*>(" + tic3Addr + "){ps1:<*>(" + + v3Addr + ")}}}" + v3s5 := "{ps2:(*" + v3t2 + "){ps3:(*" + v3t3 + "){ps1:(*" + v3t + + "){ps2:(*" + v3t2 + ")}}}}" + v3s6 := "{ps2:(*" + v3t2 + "){ps3:(*" + v3t3 + "){ps1:(*" + v3t + + ")}}}" + v3s7 := "{ps2:(*" + v3t2 + ")(" + tic2Addr + "){ps3:(*" + v3t3 + ")(" + + tic3Addr + "){ps1:(*" + v3t + ")(" + v3Addr + "){ps2:(*" + v3t2 + + ")(" + tic2Addr + ")}}}}" + v3s8 := "{ps2:(*" + v3t2 + ")(" + tic2Addr + "){ps3:(*" + v3t3 + ")(" + + tic3Addr + "){ps1:(*" + v3t + ")(" + v3Addr + ")}}}" + addFormatterTest("%v", v3, v3s) + addFormatterTest("%v", pv3, "<*>"+v3s2) + addFormatterTest("%v", &pv3, "<**>"+v3s2) + addFormatterTest("%+v", v3, v3s3) + addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s4) + addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s4) + addFormatterTest("%#v", v3, "("+v3t+")"+v3s5) + addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s6) + addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s6) + addFormatterTest("%#+v", v3, "("+v3t+")"+v3s7) + addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s8) + addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s8) +} + +func addPanicFormatterTests() { + // Type that panics in its Stringer interface. + v := panicer(127) + nv := (*panicer)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "spew_test.panicer" + vs := "(PANIC=test panic)127" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") +} + +func addErrorFormatterTests() { + // Type that has a custom Error interface. + v := customError(127) + nv := (*customError)(nil) + pv := &v + vAddr := fmt.Sprintf("%p", pv) + pvAddr := fmt.Sprintf("%p", &pv) + vt := "spew_test.customError" + vs := "error: 127" + addFormatterTest("%v", v, vs) + addFormatterTest("%v", pv, "<*>"+vs) + addFormatterTest("%v", &pv, "<**>"+vs) + addFormatterTest("%v", nv, "") + addFormatterTest("%+v", v, vs) + addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs) + addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%+v", nv, "") + addFormatterTest("%#v", v, "("+vt+")"+vs) + addFormatterTest("%#v", pv, "(*"+vt+")"+vs) + addFormatterTest("%#v", &pv, "(**"+vt+")"+vs) + addFormatterTest("%#v", nv, "(*"+vt+")"+"") + addFormatterTest("%#+v", v, "("+vt+")"+vs) + addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs) + addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs) + addFormatterTest("%#+v", nv, "(*"+vt+")"+"") +} + +func addPassthroughFormatterTests() { + // %x passthrough with uint. + v := uint(4294967295) + pv := &v + vAddr := fmt.Sprintf("%x", pv) + pvAddr := fmt.Sprintf("%x", &pv) + vs := "ffffffff" + addFormatterTest("%x", v, vs) + addFormatterTest("%x", pv, vAddr) + addFormatterTest("%x", &pv, pvAddr) + + // %#x passthrough with uint. + v2 := int(2147483647) + pv2 := &v2 + v2Addr := fmt.Sprintf("%#x", pv2) + pv2Addr := fmt.Sprintf("%#x", &pv2) + v2s := "0x7fffffff" + addFormatterTest("%#x", v2, v2s) + addFormatterTest("%#x", pv2, v2Addr) + addFormatterTest("%#x", &pv2, pv2Addr) + + // %f passthrough with precision. + addFormatterTest("%.2f", 3.1415, "3.14") + addFormatterTest("%.3f", 3.1415, "3.142") + addFormatterTest("%.4f", 3.1415, "3.1415") + + // %f passthrough with width and precision. + addFormatterTest("%5.2f", 3.1415, " 3.14") + addFormatterTest("%6.3f", 3.1415, " 3.142") + addFormatterTest("%7.4f", 3.1415, " 3.1415") + + // %d passthrough with width. + addFormatterTest("%3d", 127, "127") + addFormatterTest("%4d", 127, " 127") + addFormatterTest("%5d", 127, " 127") + + // %q passthrough with string. + addFormatterTest("%q", "test", "\"test\"") +} + +// TestFormatter executes all of the tests described by formatterTests. +func TestFormatter(t *testing.T) { + // Setup tests. + addIntFormatterTests() + addUintFormatterTests() + addBoolFormatterTests() + addFloatFormatterTests() + addComplexFormatterTests() + addArrayFormatterTests() + addSliceFormatterTests() + addStringFormatterTests() + addInterfaceFormatterTests() + addMapFormatterTests() + addStructFormatterTests() + addUintptrFormatterTests() + addUnsafePointerFormatterTests() + addChanFormatterTests() + addFuncFormatterTests() + addCircularFormatterTests() + addPanicFormatterTests() + addErrorFormatterTests() + addPassthroughFormatterTests() + + t.Logf("Running %d tests", len(formatterTests)) + for i, test := range formatterTests { + buf := new(bytes.Buffer) + spew.Fprintf(buf, test.format, test.in) + s := buf.String() + if testFailed(s, test.wants) { + t.Errorf("Formatter #%d format: %s got: %s %s", i, test.format, s, + stringizeWants(test.wants)) + continue + } + } +} + +type testStruct struct { + x int +} + +func (ts testStruct) String() string { + return fmt.Sprintf("ts.%d", ts.x) +} + +type testStructP struct { + x int +} + +func (ts *testStructP) String() string { + return fmt.Sprintf("ts.%d", ts.x) +} + +func TestPrintSortedKeys(t *testing.T) { + cfg := spew.ConfigState{SortKeys: true} + s := cfg.Sprint(map[int]string{1: "1", 3: "3", 2: "2"}) + expected := "map[1:1 2:2 3:3]" + if s != expected { + t.Errorf("Sorted keys mismatch 1:\n %v %v", s, expected) + } + + s = cfg.Sprint(map[stringer]int{"1": 1, "3": 3, "2": 2}) + expected = "map[stringer 1:1 stringer 2:2 stringer 3:3]" + if s != expected { + t.Errorf("Sorted keys mismatch 2:\n %v %v", s, expected) + } + + s = cfg.Sprint(map[pstringer]int{pstringer("1"): 1, pstringer("3"): 3, pstringer("2"): 2}) + expected = "map[stringer 1:1 stringer 2:2 stringer 3:3]" + if spew.UnsafeDisabled { + expected = "map[1:1 2:2 3:3]" + } + if s != expected { + t.Errorf("Sorted keys mismatch 3:\n %v %v", s, expected) + } + + s = cfg.Sprint(map[testStruct]int{{1}: 1, {3}: 3, {2}: 2}) + expected = "map[ts.1:1 ts.2:2 ts.3:3]" + if s != expected { + t.Errorf("Sorted keys mismatch 4:\n %v %v", s, expected) + } + + if !spew.UnsafeDisabled { + s = cfg.Sprint(map[testStructP]int{{1}: 1, {3}: 3, {2}: 2}) + expected = "map[ts.1:1 ts.2:2 ts.3:3]" + if s != expected { + t.Errorf("Sorted keys mismatch 5:\n %v %v", s, expected) + } + } + + s = cfg.Sprint(map[customError]int{customError(1): 1, customError(3): 3, customError(2): 2}) + expected = "map[error: 1:1 error: 2:2 error: 3:3]" + if s != expected { + t.Errorf("Sorted keys mismatch 6:\n %v %v", s, expected) + } +} diff --git a/vendor/github.com/davecgh/go-spew/spew/internal_test.go b/vendor/github.com/davecgh/go-spew/spew/internal_test.go new file mode 100644 index 000000000..e312b4fad --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/internal_test.go @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This test file is part of the spew package rather than than the spew_test +package because it needs access to internals to properly test certain cases +which are not possible via the public interface since they should never happen. +*/ + +package spew + +import ( + "bytes" + "reflect" + "testing" +) + +// dummyFmtState implements a fake fmt.State to use for testing invalid +// reflect.Value handling. This is necessary because the fmt package catches +// invalid values before invoking the formatter on them. +type dummyFmtState struct { + bytes.Buffer +} + +func (dfs *dummyFmtState) Flag(f int) bool { + return f == int('+') +} + +func (dfs *dummyFmtState) Precision() (int, bool) { + return 0, false +} + +func (dfs *dummyFmtState) Width() (int, bool) { + return 0, false +} + +// TestInvalidReflectValue ensures the dump and formatter code handles an +// invalid reflect value properly. This needs access to internal state since it +// should never happen in real code and therefore can't be tested via the public +// API. +func TestInvalidReflectValue(t *testing.T) { + i := 1 + + // Dump invalid reflect value. + v := new(reflect.Value) + buf := new(bytes.Buffer) + d := dumpState{w: buf, cs: &Config} + d.dump(*v) + s := buf.String() + want := "" + if s != want { + t.Errorf("InvalidReflectValue #%d\n got: %s want: %s", i, s, want) + } + i++ + + // Formatter invalid reflect value. + buf2 := new(dummyFmtState) + f := formatState{value: *v, cs: &Config, fs: buf2} + f.format(*v) + s = buf2.String() + want = "" + if s != want { + t.Errorf("InvalidReflectValue #%d got: %s want: %s", i, s, want) + } +} + +// SortValues makes the internal sortValues function available to the test +// package. +func SortValues(values []reflect.Value, cs *ConfigState) { + sortValues(values, cs) +} diff --git a/vendor/github.com/davecgh/go-spew/spew/internalunsafe_test.go b/vendor/github.com/davecgh/go-spew/spew/internalunsafe_test.go new file mode 100644 index 000000000..80dc22177 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/internalunsafe_test.go @@ -0,0 +1,101 @@ +// Copyright (c) 2013-2016 Dave Collins + +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. + +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when the code is not running on Google App Engine, compiled by GopherJS, and +// "-tags safe" is not added to the go build command line. The "disableunsafe" +// tag is deprecated and thus should not be used. +// +build !js,!appengine,!safe,!disableunsafe,go1.4 + +/* +This test file is part of the spew package rather than than the spew_test +package because it needs access to internals to properly test certain cases +which are not possible via the public interface since they should never happen. +*/ + +package spew + +import ( + "bytes" + "reflect" + "testing" +) + +// changeKind uses unsafe to intentionally change the kind of a reflect.Value to +// the maximum kind value which does not exist. This is needed to test the +// fallback code which punts to the standard fmt library for new types that +// might get added to the language. +func changeKind(v *reflect.Value, readOnly bool) { + flags := flagField(v) + if readOnly { + *flags |= flagRO + } else { + *flags &^= flagRO + } + *flags |= flagKindMask +} + +// TestAddedReflectValue tests functionaly of the dump and formatter code which +// falls back to the standard fmt library for new types that might get added to +// the language. +func TestAddedReflectValue(t *testing.T) { + i := 1 + + // Dump using a reflect.Value that is exported. + v := reflect.ValueOf(int8(5)) + changeKind(&v, false) + buf := new(bytes.Buffer) + d := dumpState{w: buf, cs: &Config} + d.dump(v) + s := buf.String() + want := "(int8) 5" + if s != want { + t.Errorf("TestAddedReflectValue #%d\n got: %s want: %s", i, s, want) + } + i++ + + // Dump using a reflect.Value that is not exported. + changeKind(&v, true) + buf.Reset() + d.dump(v) + s = buf.String() + want = "(int8) " + if s != want { + t.Errorf("TestAddedReflectValue #%d\n got: %s want: %s", i, s, want) + } + i++ + + // Formatter using a reflect.Value that is exported. + changeKind(&v, false) + buf2 := new(dummyFmtState) + f := formatState{value: v, cs: &Config, fs: buf2} + f.format(v) + s = buf2.String() + want = "5" + if s != want { + t.Errorf("TestAddedReflectValue #%d got: %s want: %s", i, s, want) + } + i++ + + // Formatter using a reflect.Value that is not exported. + changeKind(&v, true) + buf2.Reset() + f = formatState{value: v, cs: &Config, fs: buf2} + f.format(v) + s = buf2.String() + want = "" + if s != want { + t.Errorf("TestAddedReflectValue #%d got: %s want: %s", i, s, want) + } +} diff --git a/vendor/github.com/davecgh/go-spew/spew/spew.go b/vendor/github.com/davecgh/go-spew/spew/spew.go new file mode 100644 index 000000000..32c0e3388 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/spew.go @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "fmt" + "io" +) + +// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the formatted string as a value that satisfies error. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Errorf(format string, a ...interface{}) (err error) { + return fmt.Errorf(format, convertArgs(a)...) +} + +// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprint(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprint(w, convertArgs(a)...) +} + +// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, format, convertArgs(a)...) +} + +// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it +// passed with a default Formatter interface returned by NewFormatter. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, convertArgs(a)...) +} + +// Print is a wrapper for fmt.Print that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b)) +func Print(a ...interface{}) (n int, err error) { + return fmt.Print(convertArgs(a)...) +} + +// Printf is a wrapper for fmt.Printf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(format, convertArgs(a)...) +} + +// Println is a wrapper for fmt.Println that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b)) +func Println(a ...interface{}) (n int, err error) { + return fmt.Println(convertArgs(a)...) +} + +// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprint(a ...interface{}) string { + return fmt.Sprint(convertArgs(a)...) +} + +// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, convertArgs(a)...) +} + +// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it +// were passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprintln(a ...interface{}) string { + return fmt.Sprintln(convertArgs(a)...) +} + +// convertArgs accepts a slice of arguments and returns a slice of the same +// length with each argument converted to a default spew Formatter interface. +func convertArgs(args []interface{}) (formatters []interface{}) { + formatters = make([]interface{}, len(args)) + for index, arg := range args { + formatters[index] = NewFormatter(arg) + } + return formatters +} diff --git a/vendor/github.com/davecgh/go-spew/spew/spew_test.go b/vendor/github.com/davecgh/go-spew/spew/spew_test.go new file mode 100644 index 000000000..b70466c69 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/spew_test.go @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/davecgh/go-spew/spew" +) + +// spewFunc is used to identify which public function of the spew package or +// ConfigState a test applies to. +type spewFunc int + +const ( + fCSFdump spewFunc = iota + fCSFprint + fCSFprintf + fCSFprintln + fCSPrint + fCSPrintln + fCSSdump + fCSSprint + fCSSprintf + fCSSprintln + fCSErrorf + fCSNewFormatter + fErrorf + fFprint + fFprintln + fPrint + fPrintln + fSdump + fSprint + fSprintf + fSprintln +) + +// Map of spewFunc values to names for pretty printing. +var spewFuncStrings = map[spewFunc]string{ + fCSFdump: "ConfigState.Fdump", + fCSFprint: "ConfigState.Fprint", + fCSFprintf: "ConfigState.Fprintf", + fCSFprintln: "ConfigState.Fprintln", + fCSSdump: "ConfigState.Sdump", + fCSPrint: "ConfigState.Print", + fCSPrintln: "ConfigState.Println", + fCSSprint: "ConfigState.Sprint", + fCSSprintf: "ConfigState.Sprintf", + fCSSprintln: "ConfigState.Sprintln", + fCSErrorf: "ConfigState.Errorf", + fCSNewFormatter: "ConfigState.NewFormatter", + fErrorf: "spew.Errorf", + fFprint: "spew.Fprint", + fFprintln: "spew.Fprintln", + fPrint: "spew.Print", + fPrintln: "spew.Println", + fSdump: "spew.Sdump", + fSprint: "spew.Sprint", + fSprintf: "spew.Sprintf", + fSprintln: "spew.Sprintln", +} + +func (f spewFunc) String() string { + if s, ok := spewFuncStrings[f]; ok { + return s + } + return fmt.Sprintf("Unknown spewFunc (%d)", int(f)) +} + +// spewTest is used to describe a test to be performed against the public +// functions of the spew package or ConfigState. +type spewTest struct { + cs *spew.ConfigState + f spewFunc + format string + in interface{} + want string +} + +// spewTests houses the tests to be performed against the public functions of +// the spew package and ConfigState. +// +// These tests are only intended to ensure the public functions are exercised +// and are intentionally not exhaustive of types. The exhaustive type +// tests are handled in the dump and format tests. +var spewTests []spewTest + +// redirStdout is a helper function to return the standard output from f as a +// byte slice. +func redirStdout(f func()) ([]byte, error) { + tempFile, err := ioutil.TempFile("", "ss-test") + if err != nil { + return nil, err + } + fileName := tempFile.Name() + defer os.Remove(fileName) // Ignore error + + origStdout := os.Stdout + os.Stdout = tempFile + f() + os.Stdout = origStdout + tempFile.Close() + + return ioutil.ReadFile(fileName) +} + +func initSpewTests() { + // Config states with various settings. + scsDefault := spew.NewDefaultConfig() + scsNoMethods := &spew.ConfigState{Indent: " ", DisableMethods: true} + scsNoPmethods := &spew.ConfigState{Indent: " ", DisablePointerMethods: true} + scsMaxDepth := &spew.ConfigState{Indent: " ", MaxDepth: 1} + scsContinue := &spew.ConfigState{Indent: " ", ContinueOnMethod: true} + scsNoPtrAddr := &spew.ConfigState{DisablePointerAddresses: true} + scsNoCap := &spew.ConfigState{DisableCapacities: true} + + // Variables for tests on types which implement Stringer interface with and + // without a pointer receiver. + ts := stringer("test") + tps := pstringer("test") + + type ptrTester struct { + s *struct{} + } + tptr := &ptrTester{s: &struct{}{}} + + // depthTester is used to test max depth handling for structs, array, slices + // and maps. + type depthTester struct { + ic indirCir1 + arr [1]string + slice []string + m map[string]int + } + dt := depthTester{indirCir1{nil}, [1]string{"arr"}, []string{"slice"}, + map[string]int{"one": 1}} + + // Variable for tests on types which implement error interface. + te := customError(10) + + spewTests = []spewTest{ + {scsDefault, fCSFdump, "", int8(127), "(int8) 127\n"}, + {scsDefault, fCSFprint, "", int16(32767), "32767"}, + {scsDefault, fCSFprintf, "%v", int32(2147483647), "2147483647"}, + {scsDefault, fCSFprintln, "", int(2147483647), "2147483647\n"}, + {scsDefault, fCSPrint, "", int64(9223372036854775807), "9223372036854775807"}, + {scsDefault, fCSPrintln, "", uint8(255), "255\n"}, + {scsDefault, fCSSdump, "", uint8(64), "(uint8) 64\n"}, + {scsDefault, fCSSprint, "", complex(1, 2), "(1+2i)"}, + {scsDefault, fCSSprintf, "%v", complex(float32(3), 4), "(3+4i)"}, + {scsDefault, fCSSprintln, "", complex(float64(5), 6), "(5+6i)\n"}, + {scsDefault, fCSErrorf, "%#v", uint16(65535), "(uint16)65535"}, + {scsDefault, fCSNewFormatter, "%v", uint32(4294967295), "4294967295"}, + {scsDefault, fErrorf, "%v", uint64(18446744073709551615), "18446744073709551615"}, + {scsDefault, fFprint, "", float32(3.14), "3.14"}, + {scsDefault, fFprintln, "", float64(6.28), "6.28\n"}, + {scsDefault, fPrint, "", true, "true"}, + {scsDefault, fPrintln, "", false, "false\n"}, + {scsDefault, fSdump, "", complex(-10, -20), "(complex128) (-10-20i)\n"}, + {scsDefault, fSprint, "", complex(-1, -2), "(-1-2i)"}, + {scsDefault, fSprintf, "%v", complex(float32(-3), -4), "(-3-4i)"}, + {scsDefault, fSprintln, "", complex(float64(-5), -6), "(-5-6i)\n"}, + {scsNoMethods, fCSFprint, "", ts, "test"}, + {scsNoMethods, fCSFprint, "", &ts, "<*>test"}, + {scsNoMethods, fCSFprint, "", tps, "test"}, + {scsNoMethods, fCSFprint, "", &tps, "<*>test"}, + {scsNoPmethods, fCSFprint, "", ts, "stringer test"}, + {scsNoPmethods, fCSFprint, "", &ts, "<*>stringer test"}, + {scsNoPmethods, fCSFprint, "", tps, "test"}, + {scsNoPmethods, fCSFprint, "", &tps, "<*>stringer test"}, + {scsMaxDepth, fCSFprint, "", dt, "{{} [] [] map[]}"}, + {scsMaxDepth, fCSFdump, "", dt, "(spew_test.depthTester) {\n" + + " ic: (spew_test.indirCir1) {\n \n },\n" + + " arr: ([1]string) (len=1 cap=1) {\n \n },\n" + + " slice: ([]string) (len=1 cap=1) {\n \n },\n" + + " m: (map[string]int) (len=1) {\n \n }\n}\n"}, + {scsContinue, fCSFprint, "", ts, "(stringer test) test"}, + {scsContinue, fCSFdump, "", ts, "(spew_test.stringer) " + + "(len=4) (stringer test) \"test\"\n"}, + {scsContinue, fCSFprint, "", te, "(error: 10) 10"}, + {scsContinue, fCSFdump, "", te, "(spew_test.customError) " + + "(error: 10) 10\n"}, + {scsNoPtrAddr, fCSFprint, "", tptr, "<*>{<*>{}}"}, + {scsNoPtrAddr, fCSSdump, "", tptr, "(*spew_test.ptrTester)({\ns: (*struct {})({\n})\n})\n"}, + {scsNoCap, fCSSdump, "", make([]string, 0, 10), "([]string) {\n}\n"}, + {scsNoCap, fCSSdump, "", make([]string, 1, 10), "([]string) (len=1) {\n(string) \"\"\n}\n"}, + } +} + +// TestSpew executes all of the tests described by spewTests. +func TestSpew(t *testing.T) { + initSpewTests() + + t.Logf("Running %d tests", len(spewTests)) + for i, test := range spewTests { + buf := new(bytes.Buffer) + switch test.f { + case fCSFdump: + test.cs.Fdump(buf, test.in) + + case fCSFprint: + test.cs.Fprint(buf, test.in) + + case fCSFprintf: + test.cs.Fprintf(buf, test.format, test.in) + + case fCSFprintln: + test.cs.Fprintln(buf, test.in) + + case fCSPrint: + b, err := redirStdout(func() { test.cs.Print(test.in) }) + if err != nil { + t.Errorf("%v #%d %v", test.f, i, err) + continue + } + buf.Write(b) + + case fCSPrintln: + b, err := redirStdout(func() { test.cs.Println(test.in) }) + if err != nil { + t.Errorf("%v #%d %v", test.f, i, err) + continue + } + buf.Write(b) + + case fCSSdump: + str := test.cs.Sdump(test.in) + buf.WriteString(str) + + case fCSSprint: + str := test.cs.Sprint(test.in) + buf.WriteString(str) + + case fCSSprintf: + str := test.cs.Sprintf(test.format, test.in) + buf.WriteString(str) + + case fCSSprintln: + str := test.cs.Sprintln(test.in) + buf.WriteString(str) + + case fCSErrorf: + err := test.cs.Errorf(test.format, test.in) + buf.WriteString(err.Error()) + + case fCSNewFormatter: + fmt.Fprintf(buf, test.format, test.cs.NewFormatter(test.in)) + + case fErrorf: + err := spew.Errorf(test.format, test.in) + buf.WriteString(err.Error()) + + case fFprint: + spew.Fprint(buf, test.in) + + case fFprintln: + spew.Fprintln(buf, test.in) + + case fPrint: + b, err := redirStdout(func() { spew.Print(test.in) }) + if err != nil { + t.Errorf("%v #%d %v", test.f, i, err) + continue + } + buf.Write(b) + + case fPrintln: + b, err := redirStdout(func() { spew.Println(test.in) }) + if err != nil { + t.Errorf("%v #%d %v", test.f, i, err) + continue + } + buf.Write(b) + + case fSdump: + str := spew.Sdump(test.in) + buf.WriteString(str) + + case fSprint: + str := spew.Sprint(test.in) + buf.WriteString(str) + + case fSprintf: + str := spew.Sprintf(test.format, test.in) + buf.WriteString(str) + + case fSprintln: + str := spew.Sprintln(test.in) + buf.WriteString(str) + + default: + t.Errorf("%v #%d unrecognized function", test.f, i) + continue + } + s := buf.String() + if test.want != s { + t.Errorf("ConfigState #%d\n got: %s want: %s", i, s, test.want) + continue + } + } +} diff --git a/vendor/github.com/davecgh/go-spew/spew/testdata/dumpcgo.go b/vendor/github.com/davecgh/go-spew/spew/testdata/dumpcgo.go new file mode 100644 index 000000000..5c87dd456 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/testdata/dumpcgo.go @@ -0,0 +1,82 @@ +// Copyright (c) 2013 Dave Collins +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when both cgo is supported and "-tags testcgo" is added to the go test +// command line. This code should really only be in the dumpcgo_test.go file, +// but unfortunately Go will not allow cgo in test files, so this is a +// workaround to allow cgo types to be tested. This configuration is used +// because spew itself does not require cgo to run even though it does handle +// certain cgo types specially. Rather than forcing all clients to require cgo +// and an external C compiler just to run the tests, this scheme makes them +// optional. +// +build cgo,testcgo + +package testdata + +/* +#include +typedef unsigned char custom_uchar_t; + +char *ncp = 0; +char *cp = "test"; +char ca[6] = {'t', 'e', 's', 't', '2', '\0'}; +unsigned char uca[6] = {'t', 'e', 's', 't', '3', '\0'}; +signed char sca[6] = {'t', 'e', 's', 't', '4', '\0'}; +uint8_t ui8ta[6] = {'t', 'e', 's', 't', '5', '\0'}; +custom_uchar_t tuca[6] = {'t', 'e', 's', 't', '6', '\0'}; +*/ +import "C" + +// GetCgoNullCharPointer returns a null char pointer via cgo. This is only +// used for tests. +func GetCgoNullCharPointer() interface{} { + return C.ncp +} + +// GetCgoCharPointer returns a char pointer via cgo. This is only used for +// tests. +func GetCgoCharPointer() interface{} { + return C.cp +} + +// GetCgoCharArray returns a char array via cgo and the array's len and cap. +// This is only used for tests. +func GetCgoCharArray() (interface{}, int, int) { + return C.ca, len(C.ca), cap(C.ca) +} + +// GetCgoUnsignedCharArray returns an unsigned char array via cgo and the +// array's len and cap. This is only used for tests. +func GetCgoUnsignedCharArray() (interface{}, int, int) { + return C.uca, len(C.uca), cap(C.uca) +} + +// GetCgoSignedCharArray returns a signed char array via cgo and the array's len +// and cap. This is only used for tests. +func GetCgoSignedCharArray() (interface{}, int, int) { + return C.sca, len(C.sca), cap(C.sca) +} + +// GetCgoUint8tArray returns a uint8_t array via cgo and the array's len and +// cap. This is only used for tests. +func GetCgoUint8tArray() (interface{}, int, int) { + return C.ui8ta, len(C.ui8ta), cap(C.ui8ta) +} + +// GetCgoTypdefedUnsignedCharArray returns a typedefed unsigned char array via +// cgo and the array's len and cap. This is only used for tests. +func GetCgoTypdefedUnsignedCharArray() (interface{}, int, int) { + return C.tuca, len(C.tuca), cap(C.tuca) +} diff --git a/vendor/github.com/davecgh/go-spew/test_coverage.txt b/vendor/github.com/davecgh/go-spew/test_coverage.txt new file mode 100644 index 000000000..2cd087a2a --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/test_coverage.txt @@ -0,0 +1,61 @@ + +github.com/davecgh/go-spew/spew/dump.go dumpState.dump 100.00% (88/88) +github.com/davecgh/go-spew/spew/format.go formatState.format 100.00% (82/82) +github.com/davecgh/go-spew/spew/format.go formatState.formatPtr 100.00% (52/52) +github.com/davecgh/go-spew/spew/dump.go dumpState.dumpPtr 100.00% (44/44) +github.com/davecgh/go-spew/spew/dump.go dumpState.dumpSlice 100.00% (39/39) +github.com/davecgh/go-spew/spew/common.go handleMethods 100.00% (30/30) +github.com/davecgh/go-spew/spew/common.go printHexPtr 100.00% (18/18) +github.com/davecgh/go-spew/spew/common.go unsafeReflectValue 100.00% (13/13) +github.com/davecgh/go-spew/spew/format.go formatState.constructOrigFormat 100.00% (12/12) +github.com/davecgh/go-spew/spew/dump.go fdump 100.00% (11/11) +github.com/davecgh/go-spew/spew/format.go formatState.Format 100.00% (11/11) +github.com/davecgh/go-spew/spew/common.go init 100.00% (10/10) +github.com/davecgh/go-spew/spew/common.go printComplex 100.00% (9/9) +github.com/davecgh/go-spew/spew/common.go valuesSorter.Less 100.00% (8/8) +github.com/davecgh/go-spew/spew/format.go formatState.buildDefaultFormat 100.00% (7/7) +github.com/davecgh/go-spew/spew/format.go formatState.unpackValue 100.00% (5/5) +github.com/davecgh/go-spew/spew/dump.go dumpState.indent 100.00% (4/4) +github.com/davecgh/go-spew/spew/common.go catchPanic 100.00% (4/4) +github.com/davecgh/go-spew/spew/config.go ConfigState.convertArgs 100.00% (4/4) +github.com/davecgh/go-spew/spew/spew.go convertArgs 100.00% (4/4) +github.com/davecgh/go-spew/spew/format.go newFormatter 100.00% (3/3) +github.com/davecgh/go-spew/spew/dump.go Sdump 100.00% (3/3) +github.com/davecgh/go-spew/spew/common.go printBool 100.00% (3/3) +github.com/davecgh/go-spew/spew/common.go sortValues 100.00% (3/3) +github.com/davecgh/go-spew/spew/config.go ConfigState.Sdump 100.00% (3/3) +github.com/davecgh/go-spew/spew/dump.go dumpState.unpackValue 100.00% (3/3) +github.com/davecgh/go-spew/spew/spew.go Printf 100.00% (1/1) +github.com/davecgh/go-spew/spew/spew.go Println 100.00% (1/1) +github.com/davecgh/go-spew/spew/spew.go Sprint 100.00% (1/1) +github.com/davecgh/go-spew/spew/spew.go Sprintf 100.00% (1/1) +github.com/davecgh/go-spew/spew/spew.go Sprintln 100.00% (1/1) +github.com/davecgh/go-spew/spew/common.go printFloat 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go NewDefaultConfig 100.00% (1/1) +github.com/davecgh/go-spew/spew/common.go printInt 100.00% (1/1) +github.com/davecgh/go-spew/spew/common.go printUint 100.00% (1/1) +github.com/davecgh/go-spew/spew/common.go valuesSorter.Len 100.00% (1/1) +github.com/davecgh/go-spew/spew/common.go valuesSorter.Swap 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Errorf 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Fprint 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Fprintf 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Fprintln 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Print 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Printf 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Println 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Sprint 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Sprintf 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Sprintln 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.NewFormatter 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Fdump 100.00% (1/1) +github.com/davecgh/go-spew/spew/config.go ConfigState.Dump 100.00% (1/1) +github.com/davecgh/go-spew/spew/dump.go Fdump 100.00% (1/1) +github.com/davecgh/go-spew/spew/dump.go Dump 100.00% (1/1) +github.com/davecgh/go-spew/spew/spew.go Fprintln 100.00% (1/1) +github.com/davecgh/go-spew/spew/format.go NewFormatter 100.00% (1/1) +github.com/davecgh/go-spew/spew/spew.go Errorf 100.00% (1/1) +github.com/davecgh/go-spew/spew/spew.go Fprint 100.00% (1/1) +github.com/davecgh/go-spew/spew/spew.go Fprintf 100.00% (1/1) +github.com/davecgh/go-spew/spew/spew.go Print 100.00% (1/1) +github.com/davecgh/go-spew/spew ------------------------------- 100.00% (505/505) + diff --git a/vendor/github.com/hashicorp/logutils/.gitignore b/vendor/github.com/hashicorp/logutils/.gitignore new file mode 100644 index 000000000..00268614f --- /dev/null +++ b/vendor/github.com/hashicorp/logutils/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/github.com/hashicorp/logutils/LICENSE b/vendor/github.com/hashicorp/logutils/LICENSE new file mode 100644 index 000000000..c33dcc7c9 --- /dev/null +++ b/vendor/github.com/hashicorp/logutils/LICENSE @@ -0,0 +1,354 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. “Contributor” + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. “Contributor Version” + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” + + means Covered Software of a particular Contributor. + +1.4. “Covered Software” + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. “Incompatible With Secondary Licenses” + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. “Executable Form” + + means any form of the work other than Source Code Form. + +1.7. “Larger Work” + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. “License” + + means this document. + +1.9. “Licensable” + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. “Modifications” + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. “Secondary License” + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” + + means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) + + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, “control” means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an “as is” basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is “Incompatible + With Secondary Licenses”, as defined by + the Mozilla Public License, v. 2.0. + diff --git a/vendor/github.com/hashicorp/logutils/README.md b/vendor/github.com/hashicorp/logutils/README.md new file mode 100644 index 000000000..49490eaeb --- /dev/null +++ b/vendor/github.com/hashicorp/logutils/README.md @@ -0,0 +1,36 @@ +# logutils + +logutils is a Go package that augments the standard library "log" package +to make logging a bit more modern, without fragmenting the Go ecosystem +with new logging packages. + +## The simplest thing that could possibly work + +Presumably your application already uses the default `log` package. To switch, you'll want your code to look like the following: + +```go +package main + +import ( + "log" + "os" + + "github.com/hashicorp/logutils" +) + +func main() { + filter := &logutils.LevelFilter{ + Levels: []logutils.LogLevel{"DEBUG", "WARN", "ERROR"}, + MinLevel: logutils.LogLevel("WARN"), + Writer: os.Stderr, + } + log.SetOutput(filter) + + log.Print("[DEBUG] Debugging") // this will not print + log.Print("[WARN] Warning") // this will + log.Print("[ERROR] Erring") // and so will this + log.Print("Message I haven't updated") // and so will this +} +``` + +This logs to standard error exactly like go's standard logger. Any log messages you haven't converted to have a level will continue to print as before. diff --git a/vendor/github.com/hashicorp/logutils/go.mod b/vendor/github.com/hashicorp/logutils/go.mod new file mode 100644 index 000000000..ba38a4576 --- /dev/null +++ b/vendor/github.com/hashicorp/logutils/go.mod @@ -0,0 +1 @@ +module github.com/hashicorp/logutils diff --git a/vendor/github.com/hashicorp/logutils/level.go b/vendor/github.com/hashicorp/logutils/level.go new file mode 100644 index 000000000..6381bf162 --- /dev/null +++ b/vendor/github.com/hashicorp/logutils/level.go @@ -0,0 +1,81 @@ +// Package logutils augments the standard log package with levels. +package logutils + +import ( + "bytes" + "io" + "sync" +) + +type LogLevel string + +// LevelFilter is an io.Writer that can be used with a logger that +// will filter out log messages that aren't at least a certain level. +// +// Once the filter is in use somewhere, it is not safe to modify +// the structure. +type LevelFilter struct { + // Levels is the list of log levels, in increasing order of + // severity. Example might be: {"DEBUG", "WARN", "ERROR"}. + Levels []LogLevel + + // MinLevel is the minimum level allowed through + MinLevel LogLevel + + // The underlying io.Writer where log messages that pass the filter + // will be set. + Writer io.Writer + + badLevels map[LogLevel]struct{} + once sync.Once +} + +// Check will check a given line if it would be included in the level +// filter. +func (f *LevelFilter) Check(line []byte) bool { + f.once.Do(f.init) + + // Check for a log level + var level LogLevel + x := bytes.IndexByte(line, '[') + if x >= 0 { + y := bytes.IndexByte(line[x:], ']') + if y >= 0 { + level = LogLevel(line[x+1 : x+y]) + } + } + + _, ok := f.badLevels[level] + return !ok +} + +func (f *LevelFilter) Write(p []byte) (n int, err error) { + // Note in general that io.Writer can receive any byte sequence + // to write, but the "log" package always guarantees that we only + // get a single line. We use that as a slight optimization within + // this method, assuming we're dealing with a single, complete line + // of log data. + + if !f.Check(p) { + return len(p), nil + } + + return f.Writer.Write(p) +} + +// SetMinLevel is used to update the minimum log level +func (f *LevelFilter) SetMinLevel(min LogLevel) { + f.MinLevel = min + f.init() +} + +func (f *LevelFilter) init() { + badLevels := make(map[LogLevel]struct{}) + for _, level := range f.Levels { + if level == f.MinLevel { + break + } + badLevels[level] = struct{}{} + } + f.badLevels = badLevels +} diff --git a/vendor/github.com/hashicorp/logutils/level_benchmark_test.go b/vendor/github.com/hashicorp/logutils/level_benchmark_test.go new file mode 100644 index 000000000..3c2caf70e --- /dev/null +++ b/vendor/github.com/hashicorp/logutils/level_benchmark_test.go @@ -0,0 +1,37 @@ +package logutils + +import ( + "io/ioutil" + "testing" +) + +var messages [][]byte + +func init() { + messages = [][]byte{ + []byte("[TRACE] foo"), + []byte("[DEBUG] foo"), + []byte("[INFO] foo"), + []byte("[WARN] foo"), + []byte("[ERROR] foo"), + } +} + +func BenchmarkDiscard(b *testing.B) { + for i := 0; i < b.N; i++ { + ioutil.Discard.Write(messages[i%len(messages)]) + } +} + +func BenchmarkLevelFilter(b *testing.B) { + filter := &LevelFilter{ + Levels: []LogLevel{"TRACE", "DEBUG", "INFO", "WARN", "ERROR"}, + MinLevel: "WARN", + Writer: ioutil.Discard, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + filter.Write(messages[i%len(messages)]) + } +} diff --git a/vendor/github.com/hashicorp/logutils/level_test.go b/vendor/github.com/hashicorp/logutils/level_test.go new file mode 100644 index 000000000..f6b6ac3c3 --- /dev/null +++ b/vendor/github.com/hashicorp/logutils/level_test.go @@ -0,0 +1,94 @@ +package logutils + +import ( + "bytes" + "io" + "log" + "testing" +) + +func TestLevelFilter_impl(t *testing.T) { + var _ io.Writer = new(LevelFilter) +} + +func TestLevelFilter(t *testing.T) { + buf := new(bytes.Buffer) + filter := &LevelFilter{ + Levels: []LogLevel{"DEBUG", "WARN", "ERROR"}, + MinLevel: "WARN", + Writer: buf, + } + + logger := log.New(filter, "", 0) + logger.Print("[WARN] foo") + logger.Println("[ERROR] bar") + logger.Println("[DEBUG] baz") + logger.Println("[WARN] buzz") + + result := buf.String() + expected := "[WARN] foo\n[ERROR] bar\n[WARN] buzz\n" + if result != expected { + t.Fatalf("bad: %#v", result) + } +} + +func TestLevelFilterCheck(t *testing.T) { + filter := &LevelFilter{ + Levels: []LogLevel{"DEBUG", "WARN", "ERROR"}, + MinLevel: "WARN", + Writer: nil, + } + + testCases := []struct { + line string + check bool + }{ + {"[WARN] foo\n", true}, + {"[ERROR] bar\n", true}, + {"[DEBUG] baz\n", false}, + {"[WARN] buzz\n", true}, + } + + for _, testCase := range testCases { + result := filter.Check([]byte(testCase.line)) + if result != testCase.check { + t.Errorf("Fail: %s", testCase.line) + } + } +} + +func TestLevelFilter_SetMinLevel(t *testing.T) { + filter := &LevelFilter{ + Levels: []LogLevel{"DEBUG", "WARN", "ERROR"}, + MinLevel: "ERROR", + Writer: nil, + } + + testCases := []struct { + line string + checkBefore bool + checkAfter bool + }{ + {"[WARN] foo\n", false, true}, + {"[ERROR] bar\n", true, true}, + {"[DEBUG] baz\n", false, false}, + {"[WARN] buzz\n", false, true}, + } + + for _, testCase := range testCases { + result := filter.Check([]byte(testCase.line)) + if result != testCase.checkBefore { + t.Errorf("Fail: %s", testCase.line) + } + } + + // Update the minimum level to WARN + filter.SetMinLevel("WARN") + + for _, testCase := range testCases { + result := filter.Check([]byte(testCase.line)) + if result != testCase.checkAfter { + t.Errorf("Fail: %s", testCase.line) + } + } +} From 0f08ecf332a12d14f23d7c7e969ec4f55ad746ed Mon Sep 17 00:00:00 2001 From: dikhan Date: Wed, 5 Sep 2018 10:46:39 -0700 Subject: [PATCH 13/60] refactor lb api async logic to reuse code --- examples/swaggercodegen/api/api/lb_api.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/swaggercodegen/api/api/lb_api.go b/examples/swaggercodegen/api/api/lb_api.go index 990d6ac1f..90d18670e 100644 --- a/examples/swaggercodegen/api/api/lb_api.go +++ b/examples/swaggercodegen/api/api/lb_api.go @@ -110,7 +110,7 @@ func pretendResourceOperationIsProcessing(lb *Lbv1, pendingStatues []status, com var finalStatus status var inProgressStatuses []status if lb.SimulateFailure { - log.Println("Simulating failure...") + log.Printf("Simulating failure for '%s'", lb.Id) inProgressStatuses = []status{failureStatus} finalStatus = failureStatus } else { @@ -118,15 +118,17 @@ func pretendResourceOperationIsProcessing(lb *Lbv1, pendingStatues []status, com finalStatus = completed } waitTimePerPendingStatus := timeToProcess / int32(len(inProgressStatuses) + 1) - timeToProcessPerStatusDuration := time.Duration(waitTimePerPendingStatus) * time.Second for _, newStatus := range inProgressStatuses { - log.Printf("Precessing resource [%s] [%s => %s] - timeToProcess = %ds", lb.Id, lb.Status, newStatus, waitTimePerPendingStatus) - time.Sleep(timeToProcessPerStatusDuration) - updateLBStatus(lb, newStatus) + sleepAndUpdateLB(lb, newStatus, waitTimePerPendingStatus) } - log.Printf("Precessing resource final status [%s] [%s => %s] - timeToProcess = %ds", lb.Id, lb.Status, finalStatus, waitTimePerPendingStatus) + sleepAndUpdateLB(lb, finalStatus, waitTimePerPendingStatus) +} + +func sleepAndUpdateLB(lb *Lbv1, newStatus status, waitTime int32) { + timeToProcessPerStatusDuration := time.Duration(waitTime) * time.Second + log.Printf("Precessing resource [%s] [%s => %s] - timeToProcess = %ds", lb.Id, lb.Status, newStatus, waitTime) time.Sleep(timeToProcessPerStatusDuration) - updateLBStatus(lb, finalStatus) + updateLBStatus(lb, newStatus) } func updateLBStatus(lb *Lbv1, newStatus status) { From 02e8df6edf2a1299705dc43f9a358943ad178517 Mon Sep 17 00:00:00 2001 From: dikhan Date: Wed, 5 Sep 2018 10:47:32 -0700 Subject: [PATCH 14/60] pass in specific operation timeout value - this allows for code reusability as update,delete will make use of this method too --- openapi/resource_factory.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index 4be181269..da7879557 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -75,7 +75,7 @@ func (r resourceFactory) create(resourceLocalData *schema.ResourceData, i interf } log.Printf("[INFO] Resource '%s' ID: %s", r.resourceInfo.name, resourceLocalData.Id()) - err = r.handlePollingIfConfigured(resourceLocalData, providerConfig, operation.Responses, res.StatusCode) + err = r.handlePollingIfConfigured(resourceLocalData, providerConfig, operation.Responses, res.StatusCode, schema.TimeoutCreate) if err != nil { return fmt.Errorf("polling mechanism failed after POST %s call with response status code (%d): %s", resourceURL, res.StatusCode, err) } @@ -83,7 +83,7 @@ func (r resourceFactory) create(resourceLocalData *schema.ResourceData, i interf return r.read(resourceLocalData, i) } -func (r resourceFactory) handlePollingIfConfigured(resourceLocalData *schema.ResourceData, providerConfig providerConfig, responses *spec.Responses, responseStatusCode int) error { +func (r resourceFactory) handlePollingIfConfigured(resourceLocalData *schema.ResourceData, providerConfig providerConfig, responses *spec.Responses, responseStatusCode int, timeoutFor string) error { if pollingEnabled, response := r.resourceInfo.isResourcePollingEnabled(responses, responseStatusCode); pollingEnabled { targetStatuses, err := r.resourceInfo.getResourcePollTargetStatuses(*response) if err != nil { @@ -101,7 +101,7 @@ func (r resourceFactory) handlePollingIfConfigured(resourceLocalData *schema.Res Pending: pendingStatuses, Target: targetStatuses, Refresh: r.resourceStateRefreshFunc(resourceLocalData, providerConfig), - Timeout: resourceLocalData.Timeout(schema.TimeoutCreate), + Timeout: resourceLocalData.Timeout(timeoutFor), PollInterval: 5 * time.Second, MinTimeout: 10 * time.Second, Delay: 1 * time.Second, From 2dd4d5101955beb8d90a8607bfb395d34ebc4b76 Mon Sep 17 00:00:00 2001 From: dikhan Date: Wed, 5 Sep 2018 13:46:00 -0700 Subject: [PATCH 15/60] add support for update async operations --- examples/swaggercodegen/api/api/lb_api.go | 28 +++++++++++-------- .../swaggercodegen/api/resources/swagger.yaml | 20 ++++++++----- openapi/resource_factory.go | 8 +++++- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/examples/swaggercodegen/api/api/lb_api.go b/examples/swaggercodegen/api/api/lb_api.go index 90d18670e..0d550a022 100644 --- a/examples/swaggercodegen/api/api/lb_api.go +++ b/examples/swaggercodegen/api/api/lb_api.go @@ -39,11 +39,11 @@ var deletePendingStatuses = []status{deleteInProgress} func LBGetV1(w http.ResponseWriter, r *http.Request) { lb, err := retrieveLB(r) - log.Printf("GET [%+v\n]", lb) if err != nil { sendErrorResponse(http.StatusNotFound, err.Error(), w) return } + log.Printf("GET Response [%+v\n]", lb) sendResponse(http.StatusOK, w, lb) } @@ -54,10 +54,8 @@ func LBCreateV1(w http.ResponseWriter, r *http.Request) { sendErrorResponse(http.StatusBadRequest, err.Error(), w) return } - lb.Id = uuid.New() - updateLBStatus(lb, deployPending) - lbsDB[lb.Id] = lb - log.Printf("POST [%+v\n]", lb) + UpdateLBV1(lb, uuid.New(), lb.Name, lb.Backends, lb.SimulateFailure, lb.TimeToProcess, deployPending) + log.Printf("POST Response [%+v\n]", lb) go pretendResourceOperationIsProcessing(lb, deployPendingStatuses, deployed, deployFailed) @@ -76,14 +74,22 @@ func LBUpdateV1(w http.ResponseWriter, r *http.Request) { sendErrorResponse(http.StatusBadRequest, err.Error(), w) return } - newLB.Id = lb.Id - updateLBStatus(newLB, deployPending) - lbsDB[newLB.Id] = newLB - log.Printf("UPDATE [%+v\n]", newLB) + UpdateLBV1(lb, lb.Id, newLB.Name, newLB.Backends, newLB.SimulateFailure, newLB.TimeToProcess, deployPending) + log.Printf("UPDATE Response [%+v\n]", lb) go pretendResourceOperationIsProcessing(lb, deployPendingStatuses, deployed, deployFailed) - sendResponse(http.StatusAccepted, w, newLB) + sendResponse(http.StatusAccepted, w, lbsDB[lb.Id]) +} + +func UpdateLBV1(lb *Lbv1, id string, name string, backends []string, simulateFailure bool, timeToProcess int32, newStatus status) { + lb.Id = id + lb.Name = name + lb.Backends = backends + lb.SimulateFailure = simulateFailure + lb.TimeToProcess = timeToProcess + updateLBStatus(lb, newStatus) + lbsDB[lb.Id] = lb } func LBDeleteV1(w http.ResponseWriter, r *http.Request) { @@ -94,7 +100,7 @@ func LBDeleteV1(w http.ResponseWriter, r *http.Request) { } updateLBStatus(lb, deletePending) delete(db, lb.Id) - log.Printf("DELETE [%s]", lb.Id) + log.Printf("DELETE Response [%s]", lb.Id) go pretendResourceOperationIsProcessing(lb, deletePendingStatuses, deleted, deleteFailed) diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index e6f8ea2c5..b329c238d 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -169,12 +169,12 @@ paths: responses: 202: # Accepted x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' - x-terraform-resource-poll-delay: "30s" # [type (string)] - Wait this time before quering the resource state - x-terraform-resource-poll-min-timeout: "10s" # [type (string)] - Smallest time to wait before refreshes - x-terraform-resource-poll-poll-interval: "60s" # [type (string)] - Override MinTimeout/backoff and only poll this often - x-terraform-resource-poll-not-found-checks: 2 # [type (int)] - Number of times to allow not found x-terraform-resource-poll-target-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed x-terraform-resource-poll-pending-statuses: "deploy_pending,deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying + #x-terraform-resource-poll-delay: "30s" # [type (string)] - Wait this time before quering the resource state + #x-terraform-resource-poll-min-timeout: "10s" # [type (string)] - Smallest time to wait before refreshes + #x-terraform-resource-poll-poll-interval: "60s" # [type (string)] - Override MinTimeout/backoff and only poll this often + #x-terraform-resource-poll-not-found-checks: 2 # [type (int)] - Number of times to allow not found description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" schema: $ref: "#/definitions/LBV1" @@ -222,10 +222,17 @@ paths: schema: $ref: "#/definitions/LBV1" responses: - 202: - description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" + 202: # Accepted + x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' + x-terraform-resource-poll-target-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed + x-terraform-resource-poll-pending-statuses: "deploy_pending,deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying + #x-terraform-resource-poll-delay: "30s" # [type (string)] - Wait this time before quering the resource state + #x-terraform-resource-poll-min-timeout: "10s" # [type (string)] - Smallest time to wait before refreshes + #x-terraform-resource-poll-poll-interval: "60s" # [type (string)] - Override MinTimeout/backoff and only poll this often + #x-terraform-resource-poll-not-found-checks: 2 # [type (int)] - Number of times to allow not found schema: $ref: "#/definitions/LBV1" + description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" 400: description: "Invalid lb id supplied" 404: @@ -301,7 +308,6 @@ definitions: type: "string" backends: type: "array" - x-terraform-force-new: true # when this value changes terraform will force the creation of a new resource items: type: "string" status: diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index da7879557..3c3b1122d 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -207,7 +207,13 @@ func (r resourceFactory) update(resourceLocalData *schema.ResourceData, i interf if err := r.checkHTTPStatusCode(res, []int{http.StatusOK, http.StatusAccepted}); err != nil { return fmt.Errorf("UPDATE %s failed: %s", resourceIDURL, err) } - return r.updateStateWithPayloadData(responsePayload, resourceLocalData) + + err = r.handlePollingIfConfigured(resourceLocalData, providerConfig, operation.Responses, res.StatusCode, schema.TimeoutUpdate) + if err != nil { + return fmt.Errorf("polling mechanism failed after PUT %s call with response status code (%d): %s", resourceIDURL, res.StatusCode, err) + } + + return r.read(resourceLocalData, i) } func (r resourceFactory) delete(resourceLocalData *schema.ResourceData, i interface{}) error { From f9f49c062400a89c83f2469f6349f0e4a86a10db Mon Sep 17 00:00:00 2001 From: dikhan Date: Wed, 5 Sep 2018 14:49:14 -0700 Subject: [PATCH 16/60] add support for delete async operations --- examples/swaggercodegen/api/resources/swagger.yaml | 9 +++++++-- examples/swaggercodegen/main.tf | 2 +- openapi/resource_factory.go | 6 ++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index b329c238d..60c451ae9 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -250,7 +250,13 @@ paths: type: "string" responses: 202: - description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" + x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' + x-terraform-resource-poll-target-statuses: "deleted" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed + x-terraform-resource-poll-pending-statuses: "delete_pending,delete_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying + #x-terraform-resource-poll-delay: "30s" # [type (string)] - Wait this time before quering the resource state + #x-terraform-resource-poll-min-timeout: "10s" # [type (string)] - Smallest time to wait before refreshes + #x-terraform-resource-poll-poll-interval: "60s" # [type (string)] - Override MinTimeout/backoff and only poll this often + #x-terraform-resource-poll-not-found-checks: 2 # [type (int)] - Number of times to allow not found 400: $ref: "#/responses/Unauthorized" 404: @@ -293,7 +299,6 @@ definitions: x-terraform-field-name: betterExampleNumberFieldName # overriding exampleNumber with a different name 'betterExampleNumberFieldName'; the preferred name is not terraform compliant either so the provider will perform the name conversion automatically when translating the name into the provider resource configuration and when saving the field into the state file example_boolean: type: boolean - LBV1: type: "object" required: diff --git a/examples/swaggercodegen/main.tf b/examples/swaggercodegen/main.tf index 69eb8a3b7..dc3cfc810 100644 --- a/examples/swaggercodegen/main.tf +++ b/examples/swaggercodegen/main.tf @@ -17,7 +17,7 @@ resource "swaggercodegen_cdns_v1" "my_cdn" { resource "swaggercodegen_lbs_v1" "my_lb" { name = "some_name" - backends = ["backend1.com"] + backends = ["backend.com"] time_to_process = 15 # the operation (post,update,delete) will take 15s in the API to complete simulate_failure = false # no failures wished now ;) (post,update,delete) } \ No newline at end of file diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index 3c3b1122d..f4f82f427 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -240,6 +240,12 @@ func (r resourceFactory) delete(resourceLocalData *schema.ResourceData, i interf if err := r.checkHTTPStatusCode(res, []int{http.StatusNoContent, http.StatusOK, http.StatusAccepted}); err != nil { return fmt.Errorf("DELETE %s failed: %s", resourceIDURL, err) } + + err = r.handlePollingIfConfigured(resourceLocalData, providerConfig, operation.Responses, res.StatusCode, schema.TimeoutDelete) + if err != nil { + return fmt.Errorf("polling mechanism failed after DELETE %s call with response status code (%d): %s", resourceIDURL, res.StatusCode, err) + } + return nil } From 3178b6af2be3a3d863ecf6cec872c1b4b906273c Mon Sep 17 00:00:00 2001 From: dikhan Date: Wed, 5 Sep 2018 14:49:54 -0700 Subject: [PATCH 17/60] comment out extension not yet implemented --- examples/swaggercodegen/api/resources/swagger.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 60c451ae9..dd0377f1f 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -165,7 +165,7 @@ paths: required: true schema: $ref: "#/definitions/LBV1" - x-terraform-resource-timeout: "30s" # The amount of time to wait before timeout (applicable to both sync and async operations) +# x-terraform-resource-timeout: "30s" # The amount of time to wait before timeout (applicable to both sync and async operations) responses: 202: # Accepted x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' From 5379793f96729be1144ba171ca4bdbfe7937e103 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 11:25:08 -0700 Subject: [PATCH 18/60] handle properly 404 not founds when resource is destroyed - the APIs are expected to return 404 when a GET request to a given resource instance is made...this is the expected behaviour when a resource instance is destroyed and subsequent GET calls are made until a 404 is returned confirming that the resource does no longer exist. --- examples/swaggercodegen/api/api/http_utils.go | 5 +- examples/swaggercodegen/api/api/lb_api.go | 21 ++- .../swaggercodegen/api/resources/swagger.yaml | 2 +- openapi/openapierr/error.go | 33 +++++ openapi/resource_factory.go | 128 ++++++++++-------- 5 files changed, 123 insertions(+), 66 deletions(-) create mode 100644 openapi/openapierr/error.go diff --git a/examples/swaggercodegen/api/api/http_utils.go b/examples/swaggercodegen/api/api/http_utils.go index 4b1b14342..cc2db7f3d 100644 --- a/examples/swaggercodegen/api/api/http_utils.go +++ b/examples/swaggercodegen/api/api/http_utils.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "log" ) func readRequest(r *http.Request, in interface{}) error { @@ -35,7 +36,9 @@ func sendResponse(httpResponseStatusCode int, w http.ResponseWriter, out interfa func sendErrorResponse(httpStatusCode int, message string, w http.ResponseWriter) { updateResponseHeaders(httpStatusCode, w) - w.Write([]byte(fmt.Sprintf(`{"code":"%d", "message": "%s"}`, httpStatusCode, message))) + err := fmt.Sprintf(`{"code":"%d", "message": "%s"}`, httpStatusCode, message) + w.Write([]byte(err)) + log.Printf("Error Response sent '%s'", err) } func updateResponseHeaders(httpStatusCode int, w http.ResponseWriter) { diff --git a/examples/swaggercodegen/api/api/lb_api.go b/examples/swaggercodegen/api/api/lb_api.go index 0d550a022..78655b8dd 100644 --- a/examples/swaggercodegen/api/api/lb_api.go +++ b/examples/swaggercodegen/api/api/lb_api.go @@ -99,8 +99,6 @@ func LBDeleteV1(w http.ResponseWriter, r *http.Request) { return } updateLBStatus(lb, deletePending) - delete(db, lb.Id) - log.Printf("DELETE Response [%s]", lb.Id) go pretendResourceOperationIsProcessing(lb, deletePendingStatuses, deleted, deleteFailed) @@ -127,7 +125,12 @@ func pretendResourceOperationIsProcessing(lb *Lbv1, pendingStatues []status, com for _, newStatus := range inProgressStatuses { sleepAndUpdateLB(lb, newStatus, waitTimePerPendingStatus) } - sleepAndUpdateLB(lb, finalStatus, waitTimePerPendingStatus) + // This is the case of delete operation; where there is no completed status as at point the resource should be destroyed completely + if completed == deleted { + sleepAndDestroyLB(lb, waitTimePerPendingStatus) + } else { + sleepAndUpdateLB(lb, finalStatus, waitTimePerPendingStatus) + } } func sleepAndUpdateLB(lb *Lbv1, newStatus status, waitTime int32) { @@ -137,6 +140,14 @@ func sleepAndUpdateLB(lb *Lbv1, newStatus status, waitTime int32) { updateLBStatus(lb, newStatus) } +func sleepAndDestroyLB(lb *Lbv1, waitTime int32) { + timeToProcessPerStatusDuration := time.Duration(waitTime) * time.Second + log.Printf("Destroying resource [%s] [%s] - timeToProcess = %ds", lb.Id, lb.Status, waitTime) + time.Sleep(timeToProcessPerStatusDuration) + delete(lbsDB, lb.Id) + log.Printf("resource [%s] destroyed", lb.Id) +} + func updateLBStatus(lb *Lbv1, newStatus status) { oldStatus := lb.Status lb.Status = string(newStatus) @@ -148,8 +159,8 @@ func retrieveLB(r *http.Request) (*Lbv1, error) { if id == "" { return nil, fmt.Errorf("lb id path param not provided") } - lb := lbsDB[id] - if lb == nil { + lb, exists := lbsDB[id] + if lb == nil || !exists{ return nil, fmt.Errorf("lb id '%s' not found", id) } return lb, nil diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index dd0377f1f..03db87ccc 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -251,7 +251,7 @@ paths: responses: 202: x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' - x-terraform-resource-poll-target-statuses: "deleted" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed + x-terraform-resource-poll-target-statuses: "destroyed" # [type (string)] - This value is the default value returned by the refresh function when the read call returns a 404/ This is needed since it is expected that when the resource is deleted in the API, subsequent GET API calls to the resource instance will return 404 Not Found acknowledging that the resource instance no longer exists x-terraform-resource-poll-pending-statuses: "delete_pending,delete_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying #x-terraform-resource-poll-delay: "30s" # [type (string)] - Wait this time before quering the resource state #x-terraform-resource-poll-min-timeout: "10s" # [type (string)] - Smallest time to wait before refreshes diff --git a/openapi/openapierr/error.go b/openapi/openapierr/error.go new file mode 100644 index 000000000..dff5e3046 --- /dev/null +++ b/openapi/openapierr/error.go @@ -0,0 +1,33 @@ +package openapierr + +const ( + // NotFound const defines the code value for openapi internal NotFound errors + NotFound = "NotFound" +) + +// Error defines the interface that OpenAPI internal errors must be compliant with +type Error interface { + // Inherit from go error builtin interface + error + + // Code that briefly describes the type of error + Code() string +} + +// NotFoundError represent a NotFound error and implements the openapi Error interface +type NotFoundError struct { + OriginalError error +} + +// Error returns a string containing the original error; or an empty string otherwise +func (e *NotFoundError) Error() string { + if e.OriginalError != nil { + return e.OriginalError.Error() + } + return "" +} + +// Code returns the code that represents the NotFound error +func (e *NotFoundError) Code() string { + return NotFound +} diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index f4f82f427..316a92cc8 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -6,6 +6,7 @@ import ( "reflect" "github.com/dikhan/http_goclient" + "github.com/dikhan/terraform-provider-openapi/openapi/openapierr" "github.com/dikhan/terraform-provider-openapi/openapi/openapiutils" "github.com/go-openapi/spec" "github.com/hashicorp/terraform/helper/resource" @@ -83,67 +84,11 @@ func (r resourceFactory) create(resourceLocalData *schema.ResourceData, i interf return r.read(resourceLocalData, i) } -func (r resourceFactory) handlePollingIfConfigured(resourceLocalData *schema.ResourceData, providerConfig providerConfig, responses *spec.Responses, responseStatusCode int, timeoutFor string) error { - if pollingEnabled, response := r.resourceInfo.isResourcePollingEnabled(responses, responseStatusCode); pollingEnabled { - targetStatuses, err := r.resourceInfo.getResourcePollTargetStatuses(*response) - if err != nil { - return err - } - pendingStatuses, err := r.resourceInfo.getResourcePollPendingStatuses(*response) - if err != nil { - return err - } - - log.Printf("[DEBUG] target statuses (%s); pending statuses (%s)", targetStatuses, pendingStatuses) - log.Printf("[INFO] Waiting for resource '%s' to reach a completion status (%s)", r.resourceInfo.name, targetStatuses) - - stateConf := &resource.StateChangeConf{ - Pending: pendingStatuses, - Target: targetStatuses, - Refresh: r.resourceStateRefreshFunc(resourceLocalData, providerConfig), - Timeout: resourceLocalData.Timeout(timeoutFor), - PollInterval: 5 * time.Second, - MinTimeout: 10 * time.Second, - Delay: 1 * time.Second, - } - - // Wait, catching any errors - _, err = stateConf.WaitForState() - if err != nil { - return fmt.Errorf("error waiting for resource to reach a completion status (%s) [valid pending statuses (%s)]: %s", targetStatuses, pendingStatuses, err) - } - } - return nil -} - -func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.ResourceData, providerConfig providerConfig) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - remoteData, err := r.readRemote(resourceLocalData.Id(), providerConfig) - if err != nil { - return nil, "", err - } - - statusIdentifier, err := r.resourceInfo.getStatusIdentifier() - if err != nil { - log.Printf("[WARN] Error on retrieving resource '%s' (%s) when waiting: %s", r.resourceInfo.name, resourceLocalData.Id(), err) - return nil, "", err - } - - value, statusIdentifierPresentInResponse := remoteData[statusIdentifier] - if !statusIdentifierPresentInResponse { - return nil, "", fmt.Errorf("response payload received from GET /%s/%s missing the status identifier field", r.resourceInfo.path, resourceLocalData.Id()) - } - newStatus := value.(string) - log.Printf("[DEBUG] resource '%s' status (%s): %s", r.resourceInfo.name, resourceLocalData.Id(), newStatus) - return remoteData, newStatus, nil - } -} - func (r resourceFactory) read(resourceLocalData *schema.ResourceData, i interface{}) error { providerConfig := i.(providerConfig) remoteData, err := r.readRemote(resourceLocalData.Id(), providerConfig) if err != nil { - return err + return fmt.Errorf("GET %s/%s failed: %s", r.resourceInfo.path, resourceLocalData.Id(), err) } return r.updateStateWithPayloadData(remoteData, resourceLocalData) } @@ -169,9 +114,11 @@ func (r resourceFactory) readRemote(id string, providerConfig providerConfig) (m if err != nil { return nil, err } + if err := r.checkHTTPStatusCode(res, []int{http.StatusOK}); err != nil { - return nil, fmt.Errorf("GET %s failed: %s", resourceIDURL, err) + return nil, err } + return responsePayload, nil } @@ -249,6 +196,67 @@ func (r resourceFactory) delete(resourceLocalData *schema.ResourceData, i interf return nil } +func (r resourceFactory) handlePollingIfConfigured(resourceLocalData *schema.ResourceData, providerConfig providerConfig, responses *spec.Responses, responseStatusCode int, timeoutFor string) error { + if pollingEnabled, response := r.resourceInfo.isResourcePollingEnabled(responses, responseStatusCode); pollingEnabled { + targetStatuses, err := r.resourceInfo.getResourcePollTargetStatuses(*response) + if err != nil { + return err + } + pendingStatuses, err := r.resourceInfo.getResourcePollPendingStatuses(*response) + if err != nil { + return err + } + + log.Printf("[DEBUG] target statuses (%s); pending statuses (%s)", targetStatuses, pendingStatuses) + log.Printf("[INFO] Waiting for resource '%s' to reach a completion status (%s)", r.resourceInfo.name, targetStatuses) + + stateConf := &resource.StateChangeConf{ + Pending: pendingStatuses, + Target: targetStatuses, + Refresh: r.resourceStateRefreshFunc(resourceLocalData, providerConfig), + Timeout: resourceLocalData.Timeout(timeoutFor), + PollInterval: 5 * time.Second, + MinTimeout: 10 * time.Second, + Delay: 1 * time.Second, + } + + // Wait, catching any errors + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("error waiting for resource to reach a completion status (%s) [valid pending statuses (%s)]: %s", targetStatuses, pendingStatuses, err) + } + } + return nil +} + +func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.ResourceData, providerConfig providerConfig) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + remoteData, err := r.readRemote(resourceLocalData.Id(), providerConfig) + + if err != nil { + if openapiErr, ok := err.(openapierr.Error); ok { + if openapierr.NotFound == openapiErr.Code() { + return "", "destroyed", nil + } + } + return nil, "", fmt.Errorf("error on retrieving resource '%s' (%s) when waiting: %s", r.resourceInfo.name, resourceLocalData.Id(), err) + } + + statusIdentifier, err := r.resourceInfo.getStatusIdentifier() + if err != nil { + return nil, "", fmt.Errorf("error occurred while retrieving status identifier for resource '%s' (%s): %s", r.resourceInfo.name, resourceLocalData.Id(), err) + } + + value, statusIdentifierPresentInResponse := remoteData[statusIdentifier] + if !statusIdentifierPresentInResponse { + return nil, "", fmt.Errorf("response payload received from GET /%s/%s missing the status identifier field", r.resourceInfo.path, resourceLocalData.Id()) + } + newStatus := value.(string) + log.Printf("[DEBUG] resource '%s' status (%s): %s", r.resourceInfo.name, resourceLocalData.Id(), newStatus) + return remoteData, newStatus, nil + } +} + // appendOperationHeaders returns a maps containing the headers passed in and adds whatever headers the operation requires. The values // are retrieved from the provider configuration. func (r resourceFactory) appendOperationHeaders(operation *spec.Operation, providerConfig providerConfig, headers map[string]string) map[string]string { @@ -305,7 +313,9 @@ func (r resourceFactory) checkHTTPStatusCode(res *http.Response, expectedHTTPSta } switch res.StatusCode { case http.StatusUnauthorized: - return fmt.Errorf("HTTP Reponse Status Code %d - Unauthorized: API access is denied due to invalid credentials (%s)", res.StatusCode, resBody) + return fmt.Errorf("HTTP Reponse Status Code %d - Unauthorized. API access is denied due to invalid credentials: %s", res.StatusCode, resBody) + case http.StatusNotFound: + return &openapierr.NotFoundError{OriginalError: fmt.Errorf("HTTP Reponse Status Code %d - Not Found. Could not find resource instance: %s", res.StatusCode, resBody)} default: return fmt.Errorf("HTTP Reponse Status Code %d not matching expected one %v (%s)", res.StatusCode, expectedHTTPStatusCodes, resBody) } From 38ef6ea431f2aaffeb0aa03c88e522edd2008818 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 11:26:51 -0700 Subject: [PATCH 19/60] remove not yet supported extensions as it might be confusing for readers - these were added as part of the design process before even starting with the implementation when coming up with a proposal - created cards to keep track of these feature requests individually --- examples/swaggercodegen/api/resources/swagger.yaml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 03db87ccc..6b9d05984 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -165,16 +165,11 @@ paths: required: true schema: $ref: "#/definitions/LBV1" -# x-terraform-resource-timeout: "30s" # The amount of time to wait before timeout (applicable to both sync and async operations) responses: 202: # Accepted x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' x-terraform-resource-poll-target-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed x-terraform-resource-poll-pending-statuses: "deploy_pending,deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying - #x-terraform-resource-poll-delay: "30s" # [type (string)] - Wait this time before quering the resource state - #x-terraform-resource-poll-min-timeout: "10s" # [type (string)] - Smallest time to wait before refreshes - #x-terraform-resource-poll-poll-interval: "60s" # [type (string)] - Override MinTimeout/backoff and only poll this often - #x-terraform-resource-poll-not-found-checks: 2 # [type (int)] - Number of times to allow not found description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" schema: $ref: "#/definitions/LBV1" @@ -226,10 +221,6 @@ paths: x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' x-terraform-resource-poll-target-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed x-terraform-resource-poll-pending-statuses: "deploy_pending,deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying - #x-terraform-resource-poll-delay: "30s" # [type (string)] - Wait this time before quering the resource state - #x-terraform-resource-poll-min-timeout: "10s" # [type (string)] - Smallest time to wait before refreshes - #x-terraform-resource-poll-poll-interval: "60s" # [type (string)] - Override MinTimeout/backoff and only poll this often - #x-terraform-resource-poll-not-found-checks: 2 # [type (int)] - Number of times to allow not found schema: $ref: "#/definitions/LBV1" description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" @@ -253,10 +244,6 @@ paths: x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' x-terraform-resource-poll-target-statuses: "destroyed" # [type (string)] - This value is the default value returned by the refresh function when the read call returns a 404/ This is needed since it is expected that when the resource is deleted in the API, subsequent GET API calls to the resource instance will return 404 Not Found acknowledging that the resource instance no longer exists x-terraform-resource-poll-pending-statuses: "delete_pending,delete_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying - #x-terraform-resource-poll-delay: "30s" # [type (string)] - Wait this time before quering the resource state - #x-terraform-resource-poll-min-timeout: "10s" # [type (string)] - Smallest time to wait before refreshes - #x-terraform-resource-poll-poll-interval: "60s" # [type (string)] - Override MinTimeout/backoff and only poll this often - #x-terraform-resource-poll-not-found-checks: 2 # [type (int)] - Number of times to allow not found 400: $ref: "#/responses/Unauthorized" 404: From 81c541c4bf58a1977c9607605d1ef12576064b66 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 11:28:56 -0700 Subject: [PATCH 20/60] fix typo --- docs/how_to.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how_to.md b/docs/how_to.md index f41f5d4eb..36e4b48e3 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -516,7 +516,7 @@ definitions: x-terraform-field-status: true # identifies the field that should be used as status for async operations. This is handy when the field name is not 'status' but some other name the service provider might have chosen and enables the provider to identify the field as the status field that will be used to track progress for the async operations type: string readOnly: true - enum: # this is jsut for documentation purposes and to let the consumer know what statues should be expected + enum: # this is just for documentation purposes and to let the consumer know what statues should be expected - deploy_pending - deploy_in_progress - deploy_failed From f7f1082df119392065e82141975621ab75bc1fe6 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 11:29:38 -0700 Subject: [PATCH 21/60] add small comment to enum field --- examples/swaggercodegen/api/resources/swagger.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 6b9d05984..58819d1a3 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -307,7 +307,7 @@ definitions: description: lb resource status type: string readOnly: true - enum: + enum: # this is just for documentation purposes and to let the consumer know what statues should be expected - deploy_pending - deploy_in_progress - deploy_failed From bba69d586b4fdc6b0edc8dca382c23ed1c5da2f2 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 11:31:10 -0700 Subject: [PATCH 22/60] fix typo --- openapi/resource_info.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi/resource_info.go b/openapi/resource_info.go index 20e05521e..d09cc3083 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -314,8 +314,8 @@ func (r resourceInfo) getResourcePollPendingStatuses(response spec.Response) ([] func (r resourceInfo) getPollingStatuses(response spec.Response, extension string) ([]string, error) { statuses := []string{} if resourcePollTargets, exists := response.Extensions.GetString(extension); exists { - spaceTrimmedTargerts := strings.Replace(resourcePollTargets, " ", "", -1) - statuses = strings.Split(spaceTrimmedTargerts, ",") + spaceTrimmedTargets := strings.Replace(resourcePollTargets, " ", "", -1) + statuses = strings.Split(spaceTrimmedTargets, ",") } else { return nil, fmt.Errorf("response missing required extension '%s' for the polling mechanism to work", extension) } From 3dcfea802ffb7b864b62ae8e02486541b8061bd7 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 11:32:29 -0700 Subject: [PATCH 23/60] fixing description of status fields --- docs/how_to.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how_to.md b/docs/how_to.md index 36e4b48e3..83878ccba 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -338,8 +338,8 @@ the header will be the one specified in the terraform configuration ```request h This extension allows the service provider to enable the polling mechanism in the OpenAPI Terraform provider for asynchronous operations. In order for this to work, the following must be met: -- The resource definition must have a read-only field that defines the status of the resource. By default, if a string field caThis can be a field called -called 'status' is present in the resource schema definition that field will be used to track the different statues of the resource. Alternatively, +- The resource definition must have a read-only field that defines the status of the resource. By default, if a string field +named 'status' is present in the resource schema definition that field will be used to track the different statues of the resource. Alternatively, a field can be marked to serve as the status field adding the 'x-terraform-field-status'. This field will be used as the status field even if there is another field named 'status'. This gives service providers flexibility to name their status field the way they desire. More details about the 'x-terraform-field-status' extension can be found in the [Attribute details](#attributeDetails) section. From de5452f873c4dde47d5d73a9e43a3b66b33d9a10 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 11:39:12 -0700 Subject: [PATCH 24/60] clarify default behaviour if 'x-terraform-resource-poll-enabled' extension is not present --- docs/how_to.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/how_to.md b/docs/how_to.md index 83878ccba..56865b86f 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -338,12 +338,13 @@ the header will be the one specified in the terraform configuration ```request h This extension allows the service provider to enable the polling mechanism in the OpenAPI Terraform provider for asynchronous operations. In order for this to work, the following must be met: +- The resource response status code must have the 'x-terraform-resource-poll-enabled' present and set to true. - The resource definition must have a read-only field that defines the status of the resource. By default, if a string field named 'status' is present in the resource schema definition that field will be used to track the different statues of the resource. Alternatively, a field can be marked to serve as the status field adding the 'x-terraform-field-status'. This field will be used as the status field even if there is another field named 'status'. This gives service providers flexibility to name their status field the way they desire. More details about the 'x-terraform-field-status' extension can be found in the [Attribute details](#attributeDetails) section. -- The polling mechanism required two more extensions to work which define the expected 'status' values for both target and +- The polling mechanism requires two more extensions to work which define the expected 'status' values for both target and pending statuses. These are: - **x-terraform-resource-poll-target-statuses**: (type: string) Comma separated values - Defines the statuses on which the resource state will be considered 'completed' @@ -351,6 +352,8 @@ pending statuses. These are: Any other state returned that returned but is not part of this list will be considered as a failure and the polling mechanism will stop its execution accordingly. +**If the above requirements are not met, the operation will be considered synchronous and no polling will be performed.** + In the example below, the response with HTTP status code 202 has the extension defined with value 'true' meaning that the OpenAPI Terraform provider will treat this response as asynchronous. Therefore, the provider will perform continues calls to the resource's instance GET operation and will use the value from the resource 'status' property to From dd6bfb0b0d816b2cb4472b3f53414c941a579a97 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 11:45:40 -0700 Subject: [PATCH 25/60] quick doc update to accentuate the importance of status being read only --- docs/how_to.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how_to.md b/docs/how_to.md index 56865b86f..174acc953 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -339,7 +339,7 @@ This extension allows the service provider to enable the polling mechanism in th operations. In order for this to work, the following must be met: - The resource response status code must have the 'x-terraform-resource-poll-enabled' present and set to true. -- The resource definition must have a read-only field that defines the status of the resource. By default, if a string field +- The resource definition must have a **read-only** field that defines the status of the resource. By default, if a string field named 'status' is present in the resource schema definition that field will be used to track the different statues of the resource. Alternatively, a field can be marked to serve as the status field adding the 'x-terraform-field-status'. This field will be used as the status field even if there is another field named 'status'. This gives service providers flexibility to name their status field the From 5c99f7b720e5ed194c2c208da094eb08b68a45eb Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 12:00:16 -0700 Subject: [PATCH 26/60] rename x-terraform-resource-poll-target-statuses to x-terraform-resource-poll-completed-statuses --- docs/how_to.md | 4 ++-- examples/swaggercodegen/api/resources/swagger.yaml | 6 +++--- openapi/resource_info.go | 2 +- openapi/resource_info_test.go | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/how_to.md b/docs/how_to.md index 174acc953..33a7a2b85 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -347,7 +347,7 @@ way they desire. More details about the 'x-terraform-field-status' extension can - The polling mechanism requires two more extensions to work which define the expected 'status' values for both target and pending statuses. These are: - - **x-terraform-resource-poll-target-statuses**: (type: string) Comma separated values - Defines the statuses on which the resource state will be considered 'completed' + - **x-terraform-resource-poll-completed-statuses**: (type: string) Comma separated values - Defines the statuses on which the resource state will be considered 'completed' - **x-terraform-resource-poll-pending-statuses**: (type: string) Comma separated values - Defines the statuses on which the resource state will be considered 'in progress'. Any other state returned that returned but is not part of this list will be considered as a failure and the polling mechanism will stop its execution accordingly. @@ -366,7 +366,7 @@ determine the state of the resource: responses: 202: # Accepted x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' - x-terraform-resource-poll-target-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed + x-terraform-resource-poll-completed-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed x-terraform-resource-poll-pending-statuses: "deploy_pending, deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying schema: $ref: "#/definitions/LBV1" diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 58819d1a3..17f0c45ba 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -168,7 +168,7 @@ paths: responses: 202: # Accepted x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' - x-terraform-resource-poll-target-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed + x-terraform-resource-poll-completed-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed x-terraform-resource-poll-pending-statuses: "deploy_pending,deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" schema: @@ -219,7 +219,7 @@ paths: responses: 202: # Accepted x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' - x-terraform-resource-poll-target-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed + x-terraform-resource-poll-completed-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed x-terraform-resource-poll-pending-statuses: "deploy_pending,deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying schema: $ref: "#/definitions/LBV1" @@ -242,7 +242,7 @@ paths: responses: 202: x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' - x-terraform-resource-poll-target-statuses: "destroyed" # [type (string)] - This value is the default value returned by the refresh function when the read call returns a 404/ This is needed since it is expected that when the resource is deleted in the API, subsequent GET API calls to the resource instance will return 404 Not Found acknowledging that the resource instance no longer exists + x-terraform-resource-poll-completed-statuses: "destroyed" # [type (string)] - This value is the default value returned by the refresh function when the read call returns a 404/ This is needed since it is expected that when the resource is deleted in the API, subsequent GET API calls to the resource instance will return 404 Not Found acknowledging that the resource instance no longer exists x-terraform-resource-poll-pending-statuses: "delete_pending,delete_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying 400: $ref: "#/responses/Unauthorized" diff --git a/openapi/resource_info.go b/openapi/resource_info.go index d09cc3083..9bf219be5 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -22,7 +22,7 @@ const extTfID = "x-terraform-id" // Operation level extensions const extTfExcludeResource = "x-terraform-exclude-resource" const extTfResourcePollEnabled = "x-terraform-resource-poll-enabled" -const extTfResourcePollTargetStatuses = "x-terraform-resource-poll-target-statuses" +const extTfResourcePollTargetStatuses = "x-terraform-resource-poll-completed-statuses" const extTfResourcePollPendingStatuses = "x-terraform-resource-poll-pending-statuses" const idDefaultPropertyName = "id" diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 7ba724777..ee93f5c2f 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -1404,7 +1404,7 @@ func TestIsResourcePollingEnabled(t *testing.T) { func TestGetResourcePollTargetStatuses(t *testing.T) { Convey("Given a resourceInfo", t, func() { r := resourceInfo{} - Convey("When getResourcePollTargetStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-target-statuses'", func() { + Convey("When getResourcePollTargetStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-completed-statuses'", func() { expectedTarget := "deployed" extensions := spec.Extensions{} extensions.Add(extTfResourcePollTargetStatuses, expectedTarget) @@ -1462,7 +1462,7 @@ func TestGetResourcePollPendingStatuses(t *testing.T) { func TestGetPollingStatuses(t *testing.T) { Convey("Given a resourceInfo", t, func() { r := resourceInfo{} - Convey("When getPollingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-target-statuses'", func() { + Convey("When getPollingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-completed-statuses'", func() { expectedTarget := "deployed" extensions := spec.Extensions{} extensions.Add(extTfResourcePollTargetStatuses, expectedTarget) @@ -1486,7 +1486,7 @@ func TestGetPollingStatuses(t *testing.T) { }) }) - Convey("When getPollingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-target-statuses' containing multiple targets (comma separated with spaces)", func() { + Convey("When getPollingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-completed-statuses' containing multiple targets (comma separated with spaces)", func() { expectedTargets := "deployed, completed, done" extensions := spec.Extensions{} extensions.Add(extTfResourcePollTargetStatuses, expectedTargets) @@ -1513,7 +1513,7 @@ func TestGetPollingStatuses(t *testing.T) { }) }) - Convey("When getPollingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-target-statuses' containing multiple targets (comma separated with no spaces)", func() { + Convey("When getPollingStatuses method is called with a response that has a given extension 'x-terraform-resource-poll-completed-statuses' containing multiple targets (comma separated with no spaces)", func() { expectedTargets := "deployed,completed,done" extensions := spec.Extensions{} extensions.Add(extTfResourcePollTargetStatuses, expectedTargets) @@ -1539,7 +1539,7 @@ func TestGetPollingStatuses(t *testing.T) { }) }) - Convey("When getPollingStatuses method is called with a response that has does not have a given extension 'x-terraform-resource-poll-target-statuses'", func() { + Convey("When getPollingStatuses method is called with a response that has does not have a given extension 'x-terraform-resource-poll-completed-statuses'", func() { responses := spec.Responses{ ResponsesProps: spec.ResponsesProps{ StatusCodeResponses: map[int]spec.Response{ From 2f9841d862248315d4ec82adb4b26a3e2d743d47 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 12:22:12 -0700 Subject: [PATCH 27/60] add validation for readonly requirement in status fields (only applicable when performing polling) --- openapi/resource_info.go | 3 +++ openapi/resource_info_test.go | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/openapi/resource_info.go b/openapi/resource_info.go index 9bf219be5..b0fe75040 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -276,6 +276,9 @@ func (r resourceInfo) getStatusIdentifier() (string, error) { if statusProperty == "" { return "", fmt.Errorf("could not find any status property in the resource swagger definition. Please make sure the resource definition has either one property named 'status' or one property that contains %s metadata", extTfFieldStatus) } + if !r.schemaDefinition.Properties[statusProperty].ReadOnly { + return "", fmt.Errorf("schema definition status property '%s' must be readOnly", statusProperty) + } return statusProperty, nil } diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index ee93f5c2f..5bb02026f 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -1148,6 +1148,9 @@ func TestGetStatusIdentifier(t *testing.T) { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, }, }, }, @@ -1177,6 +1180,9 @@ func TestGetStatusIdentifier(t *testing.T) { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, }, }, }, @@ -1212,6 +1218,9 @@ func TestGetStatusIdentifier(t *testing.T) { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, }, }, }, @@ -1241,6 +1250,9 @@ func TestGetStatusIdentifier(t *testing.T) { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, }, }, }, @@ -1264,6 +1276,41 @@ func TestGetStatusIdentifier(t *testing.T) { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + "prop-that-is-not-status-and-does-not-have-status-metadata-either": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + _, err := r.getStatusIdentifier() + Convey("Then the error returned should NOT be nil", func() { + So(err, ShouldNotBeNil) + }) + }) + }) + + Convey("Given a swagger schema definition with a property configured with 'x-terraform-field-status' set to true but is not readonly", t, func() { + r := resourceInfo{ + schemaDefinition: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "prop-that-is-not-status": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: false, + }, }, "prop-that-is-not-status-and-does-not-have-status-metadata-either": { VendorExtensible: spec.VendorExtensible{}, From 94a788d6b0409dea0674a7e7c25f91f33fa2ad7d Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 13:44:51 -0700 Subject: [PATCH 28/60] better logging in example api server - log start and completion of request + payload in route middleware --- examples/swaggercodegen/api/api/cdn.go | 4 ---- examples/swaggercodegen/api/api/http_utils.go | 1 + examples/swaggercodegen/api/api/lb_api.go | 3 --- examples/swaggercodegen/api/api/logger.go | 15 ++++++++++++++- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/examples/swaggercodegen/api/api/cdn.go b/examples/swaggercodegen/api/api/cdn.go index 104063cf7..e2beafad1 100644 --- a/examples/swaggercodegen/api/api/cdn.go +++ b/examples/swaggercodegen/api/api/cdn.go @@ -25,7 +25,6 @@ func ContentDeliveryNetworkCreateV1(w http.ResponseWriter, r *http.Request) { } cdn.Id = uuid.New() db[cdn.Id] = cdn - log.Printf("POST [%+v\n]", cdn) sendResponse(http.StatusCreated, w, cdn) } @@ -34,7 +33,6 @@ func ContentDeliveryNetworkGetV1(w http.ResponseWriter, r *http.Request) { return } cdn, err := retrieveCdn(r) - log.Printf("GET [%+v\n]", cdn) if err != nil { sendErrorResponse(http.StatusNotFound, err.Error(), w) return @@ -58,7 +56,6 @@ func ContentDeliveryNetworkUpdateV1(w http.ResponseWriter, r *http.Request) { return } newCDN.Id = cdn.Id - log.Printf("UPDATE [%+v\n]", newCDN) db[cdn.Id] = newCDN sendResponse(http.StatusOK, w, newCDN) } @@ -73,7 +70,6 @@ func ContentDeliveryNetworkDeleteV1(w http.ResponseWriter, r *http.Request) { return } delete(db, cdn.Id) - log.Printf("DELETE [%s]", cdn.Id) sendResponse(http.StatusNoContent, w, nil) } diff --git a/examples/swaggercodegen/api/api/http_utils.go b/examples/swaggercodegen/api/api/http_utils.go index cc2db7f3d..0c01f0017 100644 --- a/examples/swaggercodegen/api/api/http_utils.go +++ b/examples/swaggercodegen/api/api/http_utils.go @@ -32,6 +32,7 @@ func sendResponse(httpResponseStatusCode int, w http.ResponseWriter, out interfa if len(resBody) > 0 { w.Write(resBody) } + log.Printf("Response sent '%+v'", out) } func sendErrorResponse(httpStatusCode int, message string, w http.ResponseWriter) { diff --git a/examples/swaggercodegen/api/api/lb_api.go b/examples/swaggercodegen/api/api/lb_api.go index 78655b8dd..732ad835b 100644 --- a/examples/swaggercodegen/api/api/lb_api.go +++ b/examples/swaggercodegen/api/api/lb_api.go @@ -43,7 +43,6 @@ func LBGetV1(w http.ResponseWriter, r *http.Request) { sendErrorResponse(http.StatusNotFound, err.Error(), w) return } - log.Printf("GET Response [%+v\n]", lb) sendResponse(http.StatusOK, w, lb) } @@ -55,7 +54,6 @@ func LBCreateV1(w http.ResponseWriter, r *http.Request) { return } UpdateLBV1(lb, uuid.New(), lb.Name, lb.Backends, lb.SimulateFailure, lb.TimeToProcess, deployPending) - log.Printf("POST Response [%+v\n]", lb) go pretendResourceOperationIsProcessing(lb, deployPendingStatuses, deployed, deployFailed) @@ -75,7 +73,6 @@ func LBUpdateV1(w http.ResponseWriter, r *http.Request) { return } UpdateLBV1(lb, lb.Id, newLB.Name, newLB.Backends, newLB.SimulateFailure, newLB.TimeToProcess, deployPending) - log.Printf("UPDATE Response [%+v\n]", lb) go pretendResourceOperationIsProcessing(lb, deployPendingStatuses, deployed, deployFailed) diff --git a/examples/swaggercodegen/api/api/logger.go b/examples/swaggercodegen/api/api/logger.go index c97fd7fac..f3768a0a5 100644 --- a/examples/swaggercodegen/api/api/logger.go +++ b/examples/swaggercodegen/api/api/logger.go @@ -10,14 +10,27 @@ func Logger(inner http.Handler, name string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() + uuid := time.Now().Nanosecond() + log.Printf( + "[%d] Started %s %s %s", + uuid, + r.Method, + r.RequestURI, + name, + ) + inner.ServeHTTP(w, r) log.Printf( - "%s %s %s %s", + "[%d] Completed %s %s %s %s", + uuid, r.Method, r.RequestURI, name, time.Since(start), ) + + log.Println() + }) } From 895cd92c1cef89eedac609624da7f0d0fa381d74 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 16:59:14 -0700 Subject: [PATCH 29/60] check errors when reading and if error is NotFound then return nil as expected - this change is needed specially when terraform handles updates/deletions doing a refresh first (performing a read call) in which case if an error is thrown then updates/deletes will not work as terraform will stop the execution right away. The reading handler will wipe out the state of a resource if its not found remotely which is the expected behaviour - allow 404 when checking returned status code on deletion. if 404 that means resource has been deleted already...also even if polling is enabled, the polling mechanism will also capture that and return successfully - increased the timeout duration to meet service providers APIs where operations take more than 1 min. Setting it to 10 tentatively for now. --- openapi/resource_factory.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index 316a92cc8..1dab9a62b 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -23,7 +23,7 @@ type resourceFactory struct { apiAuthenticator apiAuthenticator } -var defaultTimeout = time.Duration(60 * time.Second) +var defaultTimeout = time.Duration(10 * time.Minute) func (r resourceFactory) createSchemaResource() (*schema.Resource, error) { s, err := r.resourceInfo.createTerraformResourceSchema() @@ -87,9 +87,16 @@ func (r resourceFactory) create(resourceLocalData *schema.ResourceData, i interf func (r resourceFactory) read(resourceLocalData *schema.ResourceData, i interface{}) error { providerConfig := i.(providerConfig) remoteData, err := r.readRemote(resourceLocalData.Id(), providerConfig) + if err != nil { + if openapiErr, ok := err.(openapierr.Error); ok { + if openapierr.NotFound == openapiErr.Code() { + return nil + } + } return fmt.Errorf("GET %s/%s failed: %s", r.resourceInfo.path, resourceLocalData.Id(), err) } + return r.updateStateWithPayloadData(remoteData, resourceLocalData) } @@ -184,7 +191,7 @@ func (r resourceFactory) delete(resourceLocalData *schema.ResourceData, i interf if err != nil { return err } - if err := r.checkHTTPStatusCode(res, []int{http.StatusNoContent, http.StatusOK, http.StatusAccepted}); err != nil { + if err := r.checkHTTPStatusCode(res, []int{http.StatusNoContent, http.StatusOK, http.StatusAccepted, http.StatusNotFound}); err != nil { return fmt.Errorf("DELETE %s failed: %s", resourceIDURL, err) } From e33a57dbae2f1748c258b82d509da3ccc6b08cf0 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 6 Sep 2018 18:12:27 -0700 Subject: [PATCH 30/60] save extra api call by calling updateStateWithPayloadData instead - operations were making an extra GET call right after the corresponding operation was performed - e,g: POST, PUT. This was redundant as the actual call already had the payload populated so the local state could be updated accordingly. --- openapi/resource_factory.go | 75 ++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index 1dab9a62b..74175221d 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -76,12 +76,11 @@ func (r resourceFactory) create(resourceLocalData *schema.ResourceData, i interf } log.Printf("[INFO] Resource '%s' ID: %s", r.resourceInfo.name, resourceLocalData.Id()) - err = r.handlePollingIfConfigured(resourceLocalData, providerConfig, operation.Responses, res.StatusCode, schema.TimeoutCreate) + err = r.handlePollingIfConfigured(&responsePayload, resourceLocalData, providerConfig, operation.Responses, res.StatusCode, schema.TimeoutCreate) if err != nil { return fmt.Errorf("polling mechanism failed after POST %s call with response status code (%d): %s", resourceURL, res.StatusCode, err) } - - return r.read(resourceLocalData, i) + return r.updateStateWithPayloadData(responsePayload, resourceLocalData) } func (r resourceFactory) read(resourceLocalData *schema.ResourceData, i interface{}) error { @@ -162,12 +161,11 @@ func (r resourceFactory) update(resourceLocalData *schema.ResourceData, i interf return fmt.Errorf("UPDATE %s failed: %s", resourceIDURL, err) } - err = r.handlePollingIfConfigured(resourceLocalData, providerConfig, operation.Responses, res.StatusCode, schema.TimeoutUpdate) + err = r.handlePollingIfConfigured(&responsePayload, resourceLocalData, providerConfig, operation.Responses, res.StatusCode, schema.TimeoutUpdate) if err != nil { return fmt.Errorf("polling mechanism failed after PUT %s call with response status code (%d): %s", resourceIDURL, res.StatusCode, err) } - - return r.read(resourceLocalData, i) + return r.updateStateWithPayloadData(responsePayload, resourceLocalData) } func (r resourceFactory) delete(resourceLocalData *schema.ResourceData, i interface{}) error { @@ -195,7 +193,7 @@ func (r resourceFactory) delete(resourceLocalData *schema.ResourceData, i interf return fmt.Errorf("DELETE %s failed: %s", resourceIDURL, err) } - err = r.handlePollingIfConfigured(resourceLocalData, providerConfig, operation.Responses, res.StatusCode, schema.TimeoutDelete) + err = r.handlePollingIfConfigured(nil, resourceLocalData, providerConfig, operation.Responses, res.StatusCode, schema.TimeoutDelete) if err != nil { return fmt.Errorf("polling mechanism failed after DELETE %s call with response status code (%d): %s", resourceIDURL, res.StatusCode, err) } @@ -203,35 +201,42 @@ func (r resourceFactory) delete(resourceLocalData *schema.ResourceData, i interf return nil } -func (r resourceFactory) handlePollingIfConfigured(resourceLocalData *schema.ResourceData, providerConfig providerConfig, responses *spec.Responses, responseStatusCode int, timeoutFor string) error { - if pollingEnabled, response := r.resourceInfo.isResourcePollingEnabled(responses, responseStatusCode); pollingEnabled { - targetStatuses, err := r.resourceInfo.getResourcePollTargetStatuses(*response) - if err != nil { - return err - } - pendingStatuses, err := r.resourceInfo.getResourcePollPendingStatuses(*response) - if err != nil { - return err - } +func (r resourceFactory) handlePollingIfConfigured(responsePayload *map[string]interface{}, resourceLocalData *schema.ResourceData, providerConfig providerConfig, responses *spec.Responses, responseStatusCode int, timeoutFor string) error { + pollingEnabled, response := r.resourceInfo.isResourcePollingEnabled(responses, responseStatusCode) - log.Printf("[DEBUG] target statuses (%s); pending statuses (%s)", targetStatuses, pendingStatuses) - log.Printf("[INFO] Waiting for resource '%s' to reach a completion status (%s)", r.resourceInfo.name, targetStatuses) - - stateConf := &resource.StateChangeConf{ - Pending: pendingStatuses, - Target: targetStatuses, - Refresh: r.resourceStateRefreshFunc(resourceLocalData, providerConfig), - Timeout: resourceLocalData.Timeout(timeoutFor), - PollInterval: 5 * time.Second, - MinTimeout: 10 * time.Second, - Delay: 1 * time.Second, - } + if !pollingEnabled { + return nil + } - // Wait, catching any errors - _, err = stateConf.WaitForState() - if err != nil { - return fmt.Errorf("error waiting for resource to reach a completion status (%s) [valid pending statuses (%s)]: %s", targetStatuses, pendingStatuses, err) - } + targetStatuses, err := r.resourceInfo.getResourcePollTargetStatuses(*response) + if err != nil { + return err + } + pendingStatuses, err := r.resourceInfo.getResourcePollPendingStatuses(*response) + if err != nil { + return err + } + + log.Printf("[DEBUG] target statuses (%s); pending statuses (%s)", targetStatuses, pendingStatuses) + log.Printf("[INFO] Waiting for resource '%s' to reach a completion status (%s)", r.resourceInfo.name, targetStatuses) + + stateConf := &resource.StateChangeConf{ + Pending: pendingStatuses, + Target: targetStatuses, + Refresh: r.resourceStateRefreshFunc(resourceLocalData, providerConfig), + Timeout: resourceLocalData.Timeout(timeoutFor), + PollInterval: 5 * time.Second, + MinTimeout: 10 * time.Second, + Delay: 1 * time.Second, + } + + // Wait, catching any errors + remoteData, err := stateConf.WaitForState() + if err != nil { + return fmt.Errorf("error waiting for resource to reach a completion status (%s) [valid pending statuses (%s)]: %s", targetStatuses, pendingStatuses, err) + } + if responsePayload != nil { + *responsePayload = remoteData.(map[string]interface{}) } return nil } @@ -243,7 +248,7 @@ func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.Reso if err != nil { if openapiErr, ok := err.(openapierr.Error); ok { if openapierr.NotFound == openapiErr.Code() { - return "", "destroyed", nil + return remoteData, "destroyed", nil } } return nil, "", fmt.Errorf("error on retrieving resource '%s' (%s) when waiting: %s", r.resourceInfo.name, resourceLocalData.Id(), err) From 742b915cba0fbee7e41aaad7d88c34e895b4b451 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 7 Sep 2018 10:54:39 -0700 Subject: [PATCH 31/60] add missing mandatory description field for 202 lb delete response --- examples/swaggercodegen/api/resources/swagger.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 17f0c45ba..9782075e3 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -241,6 +241,7 @@ paths: type: "string" responses: 202: + description: "LB v1 deletion" x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' x-terraform-resource-poll-completed-statuses: "destroyed" # [type (string)] - This value is the default value returned by the refresh function when the read call returns a 404/ This is needed since it is expected that when the resource is deleted in the API, subsequent GET API calls to the resource instance will return 404 Not Found acknowledging that the resource instance no longer exists x-terraform-resource-poll-pending-statuses: "delete_pending,delete_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying From b9e426ba518a416f1c4c06c3d6084568d260d4c3 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 7 Sep 2018 11:48:03 -0700 Subject: [PATCH 32/60] do not expect x-terraform-resource-poll-completed-statuses for delete operations - update docs to explain behaviour for delete operations, slightly different than POST and PUT due to the nature of the operation itself that is deleting the resource and hence the mechanism to discover whether the resource is in a complete status (destroyed) differs from POST/PUt operation in that the resource will be considered destroyed when the GET operation returns 404. --- docs/how_to.md | 6 ++++++ .../swaggercodegen/api/resources/swagger.yaml | 12 ++++++------ openapi/resource_factory.go | 19 ++++++++++++++++++- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/how_to.md b/docs/how_to.md index 33a7a2b85..b13fa8368 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -348,6 +348,12 @@ way they desire. More details about the 'x-terraform-field-status' extension can pending statuses. These are: - **x-terraform-resource-poll-completed-statuses**: (type: string) Comma separated values - Defines the statuses on which the resource state will be considered 'completed' +*Note: For DELETE operations, the expected behaviour is that when the resource has been deleted, GET requests to the deleted +resource would return a 404 HTTP response status code back. This means that no payload will be returned in the response, +and hence there won't be any status field to check against to. Therefore, the OpenAPI Terraform provider handle deletes +target statuses in a different way not expecting the service provide to populate this extension. Behind the scenes, the +OpenAPI Terraform provider will handle the polling accordingly until the resource is no longer available at which point +the resource will be considered destroyed. If the extension is present with a value, it wil be ignored in the backend.* - **x-terraform-resource-poll-pending-statuses**: (type: string) Comma separated values - Defines the statuses on which the resource state will be considered 'in progress'. Any other state returned that returned but is not part of this list will be considered as a failure and the polling mechanism will stop its execution accordingly. diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 9782075e3..616d2716c 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -218,9 +218,9 @@ paths: $ref: "#/definitions/LBV1" responses: 202: # Accepted - x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' - x-terraform-resource-poll-completed-statuses: "deployed" # [type (string)] - Comma separated values with the states that will considered this resource creation done/completed - x-terraform-resource-poll-pending-statuses: "deploy_pending,deploy_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying + x-terraform-resource-poll-enabled: true + x-terraform-resource-poll-completed-statuses: "deployed" + x-terraform-resource-poll-pending-statuses: "deploy_pending,deploy_in_progress" schema: $ref: "#/definitions/LBV1" description: "this operation is asynchronous, to check the status of the deployment call GET operation and check the status field returned in the payload" @@ -242,9 +242,9 @@ paths: responses: 202: description: "LB v1 deletion" - x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' - x-terraform-resource-poll-completed-statuses: "destroyed" # [type (string)] - This value is the default value returned by the refresh function when the read call returns a 404/ This is needed since it is expected that when the resource is deleted in the API, subsequent GET API calls to the resource instance will return 404 Not Found acknowledging that the resource instance no longer exists - x-terraform-resource-poll-pending-statuses: "delete_pending,delete_in_progress" # [type (string)] - Comma separated values with the states that are "allowed" and will continue trying + x-terraform-resource-poll-enabled: true + #x-terraform-resource-poll-completed-statuses: "destroyed-crazy-nusts!!!" #This extension is not needed in DELETE operations. This is due to the fact that when the resource is destroyed, it is expected that http GET calls made by the polling mechanism will get a NotFound response status code back wit no payload whatsoever. And the OpenAPI Terraform provider will internally know how to handle this particular cases without this extension being present. If the extension is present with certain value, the latter will be ignored. + x-terraform-resource-poll-pending-statuses: "delete_pending,delete_in_progress" 400: $ref: "#/responses/Unauthorized" 404: diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index 74175221d..d201154da 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -23,6 +23,9 @@ type resourceFactory struct { apiAuthenticator apiAuthenticator } +// only applicable when remote resource no longer exists and GET operations return 404 NotFound +var defaultDestroyStatus = "destroyed" + var defaultTimeout = time.Duration(10 * time.Minute) func (r resourceFactory) createSchemaResource() (*schema.Resource, error) { @@ -212,6 +215,20 @@ func (r resourceFactory) handlePollingIfConfigured(responsePayload *map[string]i if err != nil { return err } + + // This is a use case where payload does not contain payload data and hence status field is not available; e,g: DELETE operations + // The default behaviour for this case is to consider the resource as destroyed. Hence, the below code pre-populates + // the target extension with the expected status that the polling mechanism expects when dealing with NotFound resources (should only happen on delete operations). + // Since this is internal behaviour it is not expected that the service provider will populate this field; and if so, it + // will be overridden + if responsePayload == nil { + if value, exists := response.Extensions.GetString(extTfResourcePollTargetStatuses); exists { + log.Printf("[WARN] service provider speficied '%s': %s for a DELETE operation. This is not expected as the normal behaviour is the resource to no longer exists once the DELETE operation is completed; hence subsequent GET calls should return 404 NotFound instead", extTfResourcePollTargetStatuses, value) + } + log.Printf("[WARN] setting extension '%s' with default value '%s'", extTfResourcePollTargetStatuses, defaultDestroyStatus) + targetStatuses = []string{defaultDestroyStatus} + } + pendingStatuses, err := r.resourceInfo.getResourcePollPendingStatuses(*response) if err != nil { return err @@ -248,7 +265,7 @@ func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.Reso if err != nil { if openapiErr, ok := err.(openapierr.Error); ok { if openapierr.NotFound == openapiErr.Code() { - return remoteData, "destroyed", nil + return remoteData, defaultDestroyStatus, nil } } return nil, "", fmt.Errorf("error on retrieving resource '%s' (%s) when waiting: %s", r.resourceInfo.name, resourceLocalData.Id(), err) From 7d76c9fff7342881e64587169c974e943ad38b4a Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 7 Sep 2018 12:12:51 -0700 Subject: [PATCH 33/60] fix bug where if extenson was not present the target status validation would fail - updating the operation target status key with default value instead --- examples/swaggercodegen/api/resources/swagger.yaml | 2 +- openapi/resource_factory.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 616d2716c..5306449d2 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -243,7 +243,7 @@ paths: 202: description: "LB v1 deletion" x-terraform-resource-poll-enabled: true - #x-terraform-resource-poll-completed-statuses: "destroyed-crazy-nusts!!!" #This extension is not needed in DELETE operations. This is due to the fact that when the resource is destroyed, it is expected that http GET calls made by the polling mechanism will get a NotFound response status code back wit no payload whatsoever. And the OpenAPI Terraform provider will internally know how to handle this particular cases without this extension being present. If the extension is present with certain value, the latter will be ignored. + #x-terraform-resource-poll-completed-statuses: "destroyed-crazy-nusts!!!" #This extension is not needed in DELETE operations and will be ignored if present. This is due to the fact that when the resource is destroyed, it is expected that http GET calls made by the polling mechanism will get a NotFound response status code back wit no payload whatsoever. And the OpenAPI Terraform provider will internally know how to handle this particular cases without this extension being present. x-terraform-resource-poll-pending-statuses: "delete_pending,delete_in_progress" 400: $ref: "#/responses/Unauthorized" diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index d201154da..cc3a475f0 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -211,11 +211,6 @@ func (r resourceFactory) handlePollingIfConfigured(responsePayload *map[string]i return nil } - targetStatuses, err := r.resourceInfo.getResourcePollTargetStatuses(*response) - if err != nil { - return err - } - // This is a use case where payload does not contain payload data and hence status field is not available; e,g: DELETE operations // The default behaviour for this case is to consider the resource as destroyed. Hence, the below code pre-populates // the target extension with the expected status that the polling mechanism expects when dealing with NotFound resources (should only happen on delete operations). @@ -226,7 +221,12 @@ func (r resourceFactory) handlePollingIfConfigured(responsePayload *map[string]i log.Printf("[WARN] service provider speficied '%s': %s for a DELETE operation. This is not expected as the normal behaviour is the resource to no longer exists once the DELETE operation is completed; hence subsequent GET calls should return 404 NotFound instead", extTfResourcePollTargetStatuses, value) } log.Printf("[WARN] setting extension '%s' with default value '%s'", extTfResourcePollTargetStatuses, defaultDestroyStatus) - targetStatuses = []string{defaultDestroyStatus} + response.Extensions.Add(extTfResourcePollTargetStatuses, defaultDestroyStatus) + } + + targetStatuses, err := r.resourceInfo.getResourcePollTargetStatuses(*response) + if err != nil { + return err } pendingStatuses, err := r.resourceInfo.getResourcePollPendingStatuses(*response) From b31f7833ea67cb06a17c33d52197795947b1e08b Mon Sep 17 00:00:00 2001 From: dikhan Date: Mon, 10 Sep 2018 10:52:57 -0700 Subject: [PATCH 34/60] add support for x-terraform-resource-timeout extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - If the extension is not present default timeout value for all CRUD operations will be 10 minutes - Timeout values must follow the following format: // A duration string is a possibly signed sequence of // decimal numbers, each with optional fraction and a unit suffix, // such as "300ms", "-1.5h" or "2h45m". // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". --- .../swaggercodegen/api/resources/swagger.yaml | 2 + openapi/resource_factory.go | 45 +++++++++++++++---- openapi/resource_info.go | 21 +++++++++ 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 5306449d2..9c19c85ec 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -165,6 +165,7 @@ paths: required: true schema: $ref: "#/definitions/LBV1" + x-terraform-resource-timeout: "30s" responses: 202: # Accepted x-terraform-resource-poll-enabled: true # [type (bool)] - this flags the response as trully async. Some resources might be async too but may require manual intervention from operators to complete the creation workflow. This flag will be used by the OpenAPI Service provider to detect whether the polling mechanism should be used or not. The flags below will only be applicable if this one is present with value 'true' @@ -216,6 +217,7 @@ paths: required: true schema: $ref: "#/definitions/LBV1" +# x-terraform-resource-timeout: "30s" If a given operation does not have the 'x-terraform-resource-timeout' extension; the resource operation timeout will default to 10m (10 minutes) responses: 202: # Accepted x-terraform-resource-poll-enabled: true diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index cc3a475f0..6a28ac9c6 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -33,15 +33,44 @@ func (r resourceFactory) createSchemaResource() (*schema.Resource, error) { if err != nil { return nil, err } + timeouts, err := r.createSchemaResourceTimeout() + if err != nil { + return nil, err + } return &schema.Resource{ - Schema: s, - Create: r.create, - Read: r.read, - Delete: r.delete, - Update: r.update, - Timeouts: &schema.ResourceTimeout{ - Default: &defaultTimeout, - }, + Schema: s, + Create: r.create, + Read: r.read, + Delete: r.delete, + Update: r.update, + Timeouts: timeouts, + }, nil +} + +func (r resourceFactory) createSchemaResourceTimeout() (*schema.ResourceTimeout, error) { + var postTimeout *time.Duration + var getTimeout *time.Duration + var putTimeout *time.Duration + var deleteTimeout *time.Duration + var err error + if postTimeout, err = r.resourceInfo.getResourceTimeout(r.resourceInfo.createPathInfo.Post); err != nil { + return nil, err + } + if getTimeout, err = r.resourceInfo.getResourceTimeout(r.resourceInfo.pathInfo.Get); err != nil { + return nil, err + } + if putTimeout, err = r.resourceInfo.getResourceTimeout(r.resourceInfo.pathInfo.Put); err != nil { + return nil, err + } + if deleteTimeout, err = r.resourceInfo.getResourceTimeout(r.resourceInfo.pathInfo.Delete); err != nil { + return nil, err + } + return &schema.ResourceTimeout{ + Create: postTimeout, + Read: getTimeout, + Update: putTimeout, + Delete: deleteTimeout, + Default: &defaultTimeout, }, nil } diff --git a/openapi/resource_info.go b/openapi/resource_info.go index b0fe75040..e59946022 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -9,6 +9,7 @@ import ( "github.com/dikhan/terraform-provider-openapi/openapi/terraformutils" "github.com/go-openapi/spec" "github.com/hashicorp/terraform/helper/schema" + "time" ) // Definition level extensions @@ -24,6 +25,7 @@ const extTfExcludeResource = "x-terraform-exclude-resource" const extTfResourcePollEnabled = "x-terraform-resource-poll-enabled" const extTfResourcePollTargetStatuses = "x-terraform-resource-poll-completed-statuses" const extTfResourcePollPendingStatuses = "x-terraform-resource-poll-pending-statuses" +const extTfResourceTimeout = "x-terraform-resource-timeout" const idDefaultPropertyName = "id" const statusDefaultPropertyName = "status" @@ -325,6 +327,25 @@ func (r resourceInfo) getPollingStatuses(response spec.Response, extension strin return statuses, nil } +func (r resourceInfo) getResourceTimeout(operation *spec.Operation) (*time.Duration, error) { + if operation == nil { + return nil, nil + } + return r.getTimeDuration(operation.Extensions, extTfResourceTimeout) +} + +func (r resourceInfo) getTimeDuration(extensions spec.Extensions, extension string) (*time.Duration, error) { + if value, exists := extensions.GetString(extension); exists { + return r.getDuration(value) + } + return nil, nil +} + +func (r resourceInfo) getDuration(t string) (*time.Duration, error) { + duration, err := time.ParseDuration(t) + return &duration, err +} + func (r resourceInfo) isIDProperty(propertyName string) bool { return r.propertyNameMatchesDefaultName(propertyName, idDefaultPropertyName) } From 669e59d7f2fb03d93862856ad96b6b8562886e87 Mon Sep 17 00:00:00 2001 From: dikhan Date: Mon, 10 Sep 2018 14:21:58 -0700 Subject: [PATCH 35/60] add unit tests --- openapi/resource_info_test.go | 78 +++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 5bb02026f..77e5ee4ff 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -9,6 +9,7 @@ import ( "reflect" "strings" "testing" + "time" ) func TestGetResourceURL(t *testing.T) { @@ -1606,6 +1607,83 @@ func TestGetPollingStatuses(t *testing.T) { }) } +func TestGetResourceTimeout(t *testing.T) { + Convey("Given a resourceInfo", t, func() { + r := resourceInfo{} + Convey(fmt.Sprintf("When getResourceTimeout method is called with an operation that has the extension '%s'", extTfResourceTimeout), func() { + expectedTimeout := "30s" + extensions := spec.Extensions{} + extensions.Add(extTfResourceTimeout, expectedTimeout) + post := &spec.Operation{ + VendorExtensible: spec.VendorExtensible{ + Extensions: extensions, + }, + } + duration, err := r.getResourceTimeout(post) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the duration returned should contain", func() { + So(*duration, ShouldEqual, time.Duration(30 * time.Second)) + }) + }) + }) +} + +func TestGetTimeDuration(t *testing.T) { + Convey("Given a resourceInfo", t, func() { + r := resourceInfo{} + Convey(fmt.Sprintf("When getTimeDuration method is called with a list of extensions that contains the extension passed in '%s'", extTfResourceTimeout), func() { + expectedTimeout := "30s" + extensions := spec.Extensions{} + extensions.Add(extTfResourceTimeout, expectedTimeout) + duration, err := r.getTimeDuration(extensions, extTfResourceTimeout) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the duration returned should contain", func() { + So(*duration, ShouldEqual, time.Duration(30 * time.Second)) + }) + }) + Convey(fmt.Sprintf("When getTimeDuration method is called with a list of extensions that DOES NOT contain the extension passed in '%s'", extTfResourceTimeout), func() { + expectedTimeout := "30s" + extensions := spec.Extensions{} + extensions.Add(extTfResourceTimeout, expectedTimeout) + duration, err := r.getTimeDuration(extensions, "nonExistingExtension") + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the duration returned should be nil", func() { + So(duration, ShouldBeNil) + }) + }) + }) +} + + +func TestGetDuration(t *testing.T) { + Convey("Given a resourceInfo", t, func() { + r := resourceInfo{} + Convey("When getDuration method is called a valid formatted time'", func() { + duration, err := r.getDuration("30s") + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the statuses returned should contain", func() { + fmt.Println(duration) + So(*duration, ShouldEqual, time.Duration(30 * time.Second)) + }) + }) + Convey("When getDuration method is called a invalid formatted time'", func() { + _, err := r.getDuration("some invalid formatted time") + Convey("Then the error returned should be nil", func() { + So(err, ShouldNotBeNil) + }) + }) + }) +} + + func TestShouldIgnoreResource(t *testing.T) { Convey("Given a terraform compliant resource that has a POST operation containing the x-terraform-exclude-resource with value true", t, func() { r := resourceInfo{ From 75aac0d6496430c631a845aac68eb61ea5daa317 Mon Sep 17 00:00:00 2001 From: dikhan Date: Mon, 10 Sep 2018 16:02:41 -0700 Subject: [PATCH 36/60] handle special use cases better - throw an error when value from extension is empty or does not match valid units (s, m or h) - add more tests to cover those use cases --- openapi/resource_info.go | 8 ++++ openapi/resource_info_test.go | 76 ++++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/openapi/resource_info.go b/openapi/resource_info.go index e59946022..377e502c3 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -9,6 +9,7 @@ import ( "github.com/dikhan/terraform-provider-openapi/openapi/terraformutils" "github.com/go-openapi/spec" "github.com/hashicorp/terraform/helper/schema" + "regexp" "time" ) @@ -336,6 +337,13 @@ func (r resourceInfo) getResourceTimeout(operation *spec.Operation) (*time.Durat func (r resourceInfo) getTimeDuration(extensions spec.Extensions, extension string) (*time.Duration, error) { if value, exists := extensions.GetString(extension); exists { + regex, err := regexp.Compile("^[\\d]+([\\.]{1}[\\d]+)?[smh]{1}$") + if err != nil { + return nil, err + } + if !regex.Match([]byte(value)) { + return nil, fmt.Errorf("invalid duration value: '%s'. The value must be a sequence of decimal numbers each with optional fraction and a unit suffix (negative durations are not allowed). The value must be formatted either in seconds (s), minutes (m) or hours (h)", value) + } return r.getDuration(value) } return nil, nil diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 77e5ee4ff..0b4f0e7b0 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -1624,7 +1624,7 @@ func TestGetResourceTimeout(t *testing.T) { So(err, ShouldBeNil) }) Convey("Then the duration returned should contain", func() { - So(*duration, ShouldEqual, time.Duration(30 * time.Second)) + So(*duration, ShouldEqual, time.Duration(30*time.Second)) }) }) }) @@ -1633,7 +1633,7 @@ func TestGetResourceTimeout(t *testing.T) { func TestGetTimeDuration(t *testing.T) { Convey("Given a resourceInfo", t, func() { r := resourceInfo{} - Convey(fmt.Sprintf("When getTimeDuration method is called with a list of extensions that contains the extension passed in '%s'", extTfResourceTimeout), func() { + Convey(fmt.Sprintf("When getTimeDuration method is called with a list of extensions that contains the extension passed in '%s' with value in seconds", extTfResourceTimeout), func() { expectedTimeout := "30s" extensions := spec.Extensions{} extensions.Add(extTfResourceTimeout, expectedTimeout) @@ -1642,7 +1642,31 @@ func TestGetTimeDuration(t *testing.T) { So(err, ShouldBeNil) }) Convey("Then the duration returned should contain", func() { - So(*duration, ShouldEqual, time.Duration(30 * time.Second)) + So(*duration, ShouldEqual, time.Duration(30*time.Second)) + }) + }) + Convey(fmt.Sprintf("When getTimeDuration method is called with a list of extensions that contains the extension passed in '%s' with value in minutes (using fractions)", extTfResourceTimeout), func() { + expectedTimeout := "20.5m" + extensions := spec.Extensions{} + extensions.Add(extTfResourceTimeout, expectedTimeout) + duration, err := r.getTimeDuration(extensions, extTfResourceTimeout) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the duration returned should contain", func() { + So(*duration, ShouldEqual, time.Duration((20*time.Minute)+(30*time.Second))) + }) + }) + Convey(fmt.Sprintf("When getTimeDuration method is called with a list of extensions that contains the extension passed in '%s' with value in hours", extTfResourceTimeout), func() { + expectedTimeout := "1h" + extensions := spec.Extensions{} + extensions.Add(extTfResourceTimeout, expectedTimeout) + duration, err := r.getTimeDuration(extensions, extTfResourceTimeout) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the duration returned should contain", func() { + So(*duration, ShouldEqual, time.Duration(1*time.Hour)) }) }) Convey(fmt.Sprintf("When getTimeDuration method is called with a list of extensions that DOES NOT contain the extension passed in '%s'", extTfResourceTimeout), func() { @@ -1657,10 +1681,51 @@ func TestGetTimeDuration(t *testing.T) { So(duration, ShouldBeNil) }) }) + Convey(fmt.Sprintf("When getTimeDuration method is called with a list of extensions that DOES contain the extension passed in '%s' BUT the value is an empty string", extTfResourceTimeout), func() { + extensions := spec.Extensions{} + extensions.Add(extTfResourceTimeout, "") + duration, err := r.getTimeDuration(extensions, extTfResourceTimeout) + Convey("Then the error returned should be nil", func() { + So(err, ShouldNotBeNil) + }) + Convey("Then the duration returned should be nil", func() { + So(duration, ShouldBeNil) + }) + Convey("And the error message should be", func() { + So(err.Error(), ShouldContainSubstring, "invalid duration value: ''. The value must be a sequence of decimal numbers each with optional fraction and a unit suffix (negative durations are not allowed). The value must be formatted either in seconds (s), minutes (m) or hours (h)") + }) + }) + Convey(fmt.Sprintf("When getTimeDuration method is called with a list of extensions that DOES contain the extension passed in '%s' BUT the value is a negative duration", extTfResourceTimeout), func() { + extensions := spec.Extensions{} + extensions.Add(extTfResourceTimeout, "-1.5h") + duration, err := r.getTimeDuration(extensions, extTfResourceTimeout) + Convey("Then the error returned should be nil", func() { + So(err, ShouldNotBeNil) + }) + Convey("Then the duration returned should be nil", func() { + So(duration, ShouldBeNil) + }) + Convey("And the error message should be", func() { + So(err.Error(), ShouldContainSubstring, "invalid duration value: '-1.5h'. The value must be a sequence of decimal numbers each with optional fraction and a unit suffix (negative durations are not allowed). The value must be formatted either in seconds (s), minutes (m) or hours (h)") + }) + }) + Convey(fmt.Sprintf("When getTimeDuration method is called with a list of extensions that DOES contain the extension passed in '%s' BUT the value is NOT supported (distinct than s,m and h)", extTfResourceTimeout), func() { + extensions := spec.Extensions{} + extensions.Add(extTfResourceTimeout, "300ms") + duration, err := r.getTimeDuration(extensions, extTfResourceTimeout) + Convey("Then the error returned should be nil", func() { + So(err, ShouldNotBeNil) + }) + Convey("Then the duration returned should be nil", func() { + So(duration, ShouldBeNil) + }) + Convey("And the error message should be", func() { + So(err.Error(), ShouldContainSubstring, "invalid duration value: '300ms'. The value must be a sequence of decimal numbers each with optional fraction and a unit suffix (negative durations are not allowed). The value must be formatted either in seconds (s), minutes (m) or hours (h)") + }) + }) }) } - func TestGetDuration(t *testing.T) { Convey("Given a resourceInfo", t, func() { r := resourceInfo{} @@ -1671,7 +1736,7 @@ func TestGetDuration(t *testing.T) { }) Convey("Then the statuses returned should contain", func() { fmt.Println(duration) - So(*duration, ShouldEqual, time.Duration(30 * time.Second)) + So(*duration, ShouldEqual, time.Duration(30*time.Second)) }) }) Convey("When getDuration method is called a invalid formatted time'", func() { @@ -1683,7 +1748,6 @@ func TestGetDuration(t *testing.T) { }) } - func TestShouldIgnoreResource(t *testing.T) { Convey("Given a terraform compliant resource that has a POST operation containing the x-terraform-exclude-resource with value true", t, func() { r := resourceInfo{ From 65856d240123a40072f1ad06e661fa75b7fb2c52 Mon Sep 17 00:00:00 2001 From: dikhan Date: Mon, 10 Sep 2018 16:03:00 -0700 Subject: [PATCH 37/60] update documentation --- docs/how_to.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/how_to.md b/docs/how_to.md index b13fa8368..87265c725 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -262,11 +262,12 @@ The following extensions can be used in path operations. Read the according exte Extension Name | Type | Description ---|:---:|--- [x-terraform-exclude-resource](#xTerraformExcludeResource) | bool | Only available in resource root's POST operation. Defines whether a given terraform compliant resource should be exposed to the OpenAPI Terraform provider or ignored. +[x-terraform-resource-timeout](#xTerraformResourceTimeout) | string | Only available in operation level. Defines the timeout for a given operation. This value overrides the default timeout operation value which is 10 minutes. [x-terraform-header](#xTerraformHeader) | string | Only available in operation level parameters at the moment. Defines that he given header should be passed as part of the request. -[x-terraform-resource-poll-enabled](#xTerraformResourcePollEnabled) | bool | Only supported in operation response (202). Defines that if the API responds with the given HTTP Status code (202), the polling mechanism will be enabled. This allows the OpenAPI Terraform provider to perform read calls to the remote API and check the resource state. The polling mechanism finalises if the remote resource state arrives at completion, failure state or times-out (60s) +[x-terraform-resource-poll-enabled](#xTerraformResourcePollEnabled) | bool | Only supported in operation responses (e,g: 202). Defines that if the API responds with the given HTTP Status code (e,g: 202), the polling mechanism will be enabled. This allows the OpenAPI Terraform provider to perform read calls to the remote API and check the resource state. The polling mechanism finalises if the remote resource state arrives at completion, failure state or times-out (60s) ###### x-terraform-exclude-resource - + Service providers might not want to expose certain resources to Terraform (e,g: admin resources). This can be achieved by adding the following swagger extension to the resource root POST operation (in the example below ```/v1/resource:```): @@ -289,6 +290,32 @@ this extension. If the extension is not present or has value 'false' then the re *Note: This extension is only interpreted and handled in resource root POST operations (e,g: /v1/resource) in the above example* +###### x-terraform-resource-timeout + +This extension allows service providers to override the default timeout value for CRUD operations with a different value. + +The value must comply with the duration type format. A duration string is a sequence of decimal positive numbers (negative numbers are not allowed), +each with optional fraction and a unit suffix, such as "300s", "20.5m", "1.5h" or "2h45m". + +Valid time units are "s", "m", "h". + +```` +paths: + /v1/resource: + post: + ... + x-terraform-resource-timeout: "15m" # this means the max timeout for the post operation to finish is 15 minues. This overrides the deafult timeout per operation which is 10 minutes + ... + /v1/resource/{id}: + get: # will have default value of 10 minutes as the 'x-terraform-resource-timeout' is not present for this operation + ... + delete: + x-terraform-resource-timeout: "20m" # this means the max timeout for the post operation to finish is 15 minues. This overrides the deafult timeout per operation which is 10 minutes + ... +```` + +*Note: This extension is only supported at the operation level* + ###### x-terraform-header Certain operations may specify other type of parameters besides a 'body' type parameter which defines the payload expected @@ -400,7 +427,7 @@ definitions: - deleted ```` -*Note: This extension is only supported at the response level.* +*Note: This extension is only supported at the operation's response level.* #### Definitions From 35130ab543f7fe6ab0b26574c5169a0e88dd16a6 Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 11 Sep 2018 09:40:04 -0700 Subject: [PATCH 38/60] fix typos --- docs/how_to.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/how_to.md b/docs/how_to.md index 87265c725..7a55b4632 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -304,14 +304,14 @@ paths: /v1/resource: post: ... - x-terraform-resource-timeout: "15m" # this means the max timeout for the post operation to finish is 15 minues. This overrides the deafult timeout per operation which is 10 minutes + x-terraform-resource-timeout: "15m" # this means the max timeout for the post operation to finish is 15 minutes. This overrides the default timeout per operation which is 10 minutes ... /v1/resource/{id}: get: # will have default value of 10 minutes as the 'x-terraform-resource-timeout' is not present for this operation ... delete: - x-terraform-resource-timeout: "20m" # this means the max timeout for the post operation to finish is 15 minues. This overrides the deafult timeout per operation which is 10 minutes - ... + x-terraform-resource-timeout: "20m" # this means the max timeout for the delete operation to finish is 20 minutes. This overrides the default timeout per operation which is 10 minutes + ... ```` *Note: This extension is only supported at the operation level* From 53865f5a9a25f83176405dd411251a40d2abfb57 Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 11 Sep 2018 10:10:59 -0700 Subject: [PATCH 39/60] update regex to a more simple one as suggested in the PR --- openapi/resource_info.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi/resource_info.go b/openapi/resource_info.go index 377e502c3..cdec2427f 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -337,7 +337,7 @@ func (r resourceInfo) getResourceTimeout(operation *spec.Operation) (*time.Durat func (r resourceInfo) getTimeDuration(extensions spec.Extensions, extension string) (*time.Duration, error) { if value, exists := extensions.GetString(extension); exists { - regex, err := regexp.Compile("^[\\d]+([\\.]{1}[\\d]+)?[smh]{1}$") + regex, err := regexp.Compile("^\\d+(\\.\\d+)?[smh]{1}$") if err != nil { return nil, err } From fceb4ce6e920021dae4d1d26e34a5adb91559456 Mon Sep 17 00:00:00 2001 From: dikhan Date: Wed, 12 Sep 2018 12:05:41 -0700 Subject: [PATCH 40/60] add support for x-terraform-resource-host and multiregion resources - resources deemed multi region will have one resource per region (similar to aws resources) - resources that override the global host, should make API calls to that overriden host instead - resources that do not meet any of the above will continue to use the global host - an error will be thrown if multi region is not configured properly but the overrdie url is still parametrized --- openapi/api_spec_analyser.go | 33 +++- openapi/api_spec_analyser_test.go | 206 ++++++++++++++++++++- openapi/openapiutils/openapi_utils.go | 21 +++ openapi/openapiutils/openapi_utils_test.go | 46 +++++ openapi/resource_info.go | 64 +++++++ openapi/resource_info_test.go | 175 +++++++++++++++++ 6 files changed, 541 insertions(+), 4 deletions(-) diff --git a/openapi/api_spec_analyser.go b/openapi/api_spec_analyser.go index 405ebd394..12d5853ac 100644 --- a/openapi/api_spec_analyser.go +++ b/openapi/api_spec_analyser.go @@ -41,6 +41,7 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) { log.Printf("[DEBUG] resource not figure out valid terraform resource name for '%s': %s", resourcePath, err) continue } + r := resourceInfo{ name: resourceName, basePath: asa.d.BasePath(), @@ -51,9 +52,37 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) { createPathInfo: *resourceRoot, pathInfo: pathItem, } - if !r.shouldIgnoreResource() { - resources[resourceName] = r + + if r.shouldIgnoreResource() { + continue + } + + isMultiRegion, regions := r.isMultiRegionResource(asa.d.Spec().Extensions) + if isMultiRegion { + log.Printf("[INFO] resource '%s' is configured with host override AND multi region; creating reasource per region", r.name) + for regionName, regionHost := range regions { + resourceRegionName := fmt.Sprintf("%s_%s", resourceName, regionName) + regionResource := resourceInfo{} + regionResource = r + regionResource.name = resourceRegionName + regionResource.host = regionHost + log.Printf("[INFO] multi region resource name for region %s = %s", regionName, resourceRegionName) + resources[resourceRegionName] = regionResource + } + continue + } + + hostOverride := r.getResourceOverrideHost() + // if the override host is multi region then something must be wrong with the multi region configuration, failing to let the user know so they can fix the configuration + if isMultiRegionHost, _ := r.isMultiRegionHost(hostOverride); isMultiRegionHost { + return nil, fmt.Errorf("multi region configuration for resource '%s' is wrong, please check the multi region configuration in the swagger file is right for that resource", resourceName) + } + // Fall back to override the host if value is not empty; otherwise global host will be used as usual + if hostOverride != "" { + log.Printf("[INFO] resource '%s' is configured with host override, API calls will be made against '%s' instead of '%s'", r.name, hostOverride, asa.d.Spec().Host) + r.host = hostOverride } + resources[resourceName] = r } return resources, nil } diff --git a/openapi/api_spec_analyser_test.go b/openapi/api_spec_analyser_test.go index 2970842d5..5d2cd6fed 100644 --- a/openapi/api_spec_analyser_test.go +++ b/openapi/api_spec_analyser_test.go @@ -2,6 +2,7 @@ package openapi import ( "encoding/json" + "fmt" "github.com/go-openapi/loads" "github.com/go-openapi/spec" . "github.com/smartystreets/goconvey/convey" @@ -1106,7 +1107,9 @@ paths: func TestGetResourcesInfo(t *testing.T) { Convey("Given an apiSpecAnalyser loaded with a swagger file containing a compliant terraform resource /v1/cdns and some non compliant paths", t, func() { - swaggerContent := `swagger: "2.0" + expectedHost := "some.api.domain.com" + swaggerContent := fmt.Sprintf(`swagger: "2.0" +host: %s paths: /v1/cdns: post: @@ -1186,7 +1189,7 @@ definitions: properties: id: type: "string" - readOnly: true` + readOnly: true`, expectedHost) a := initAPISpecAnalyser(swaggerContent) Convey("When getResourcesInfo method is called ", func() { resourcesInfo, err := a.getResourcesInfo() @@ -1197,6 +1200,9 @@ definitions: So(len(resourcesInfo), ShouldEqual, 1) So(resourcesInfo, ShouldContainKey, "cdns_v1") }) + Convey("And the resources info map only element should have global host", func() { + So(resourcesInfo["cdns_v1"].host, ShouldEqual, expectedHost) + }) }) }) @@ -1295,6 +1301,202 @@ definitions: }) }) }) + + // Tests that if the override host is present for a given resource and it is not multi region, the host for that given resource should be the override one + Convey("Given an apiSpecAnalyser loaded with a swagger file containing a compliant terraform resource that has the 'x-terraform-resource-host' extension and therefore overrides the global host value", t, func() { + expectedHost := "some.api.domain.com" + var swaggerJSON = fmt.Sprintf(` +{ + "swagger":"2.0", + "paths":{ + "/v1/cdns":{ + "post":{ + "x-terraform-resource-host": "%s", + "summary":"Create cdn", + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Created CDN", + "schema":{ + "$ref":"#/definitions/ContentDeliveryNetwork" + } + } + ] + } + }, + "/v1/cdns/{id}":{ + "get":{ + "summary":"Get cdn by id" + }, + "put":{ + "summary":"Updated cdn" + }, + "delete":{ + "summary":"Delete cdn" + } + } + }, + "definitions":{ + "ContentDeliveryNetwork":{ + "type":"object", + "properties":{ + "id":{ + "type":"string" + } + } + } + } +}`, expectedHost) + a := initAPISpecAnalyser(swaggerJSON) + Convey("When getResourcesInfo method is called ", func() { + resourceInfo, err := a.getResourcesInfo() + expectedResourceName := "cdns_v1" + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the resourceInfo map should not be empty", func() { + So(resourceInfo, ShouldNotBeEmpty) + }) + Convey(fmt.Sprintf("And the resourceInfo map should contain key %s", expectedResourceName), func() { + So(resourceInfo, ShouldContainKey, expectedResourceName) + }) + Convey(fmt.Sprintf("And the host value for that resource should be '%s'", expectedHost), func() { + So(resourceInfo[expectedResourceName].host, ShouldEqual, expectedHost) + }) + }) + }) + + // Tests that if the override host is present for a given resource and multi region configuration is correct, the appropriate resources per region should be created + Convey("Given an apiSpecAnalyser loaded with a swagger file containing a compliant terraform resource that has the 'x-terraform-resource-host' extension with a parametrized value some.api.${serviceProviderName}.domain.com and a matching root level 'x-terraform-resource-regions-serviceProviderName' extension populated with the different API regions", t, func() { + serviceProviderName := "serviceProviderName" + expectedHost := fmt.Sprintf("some.api.${%s}.domain.com", serviceProviderName) + var swaggerJSON = fmt.Sprintf(` +{ + "swagger":"2.0", + "x-terraform-resource-regions-%s": "uswest, useast", + "paths":{ + "/v1/cdns":{ + "post":{ + "x-terraform-resource-host": "%s", + "summary":"Create cdn", + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Created CDN", + "schema":{ + "$ref":"#/definitions/ContentDeliveryNetwork" + } + } + ] + } + }, + "/v1/cdns/{id}":{ + "get":{ + "summary":"Get cdn by id" + }, + "put":{ + "summary":"Updated cdn" + }, + "delete":{ + "summary":"Delete cdn" + } + } + }, + "definitions":{ + "ContentDeliveryNetwork":{ + "type":"object", + "properties":{ + "id":{ + "type":"string" + } + } + } + } +}`, serviceProviderName, expectedHost) + a := initAPISpecAnalyser(swaggerJSON) + Convey("When getResourcesInfo method is called ", func() { + resourceInfo, err := a.getResourcesInfo() + expectedResourceName1 := "cdns_v1_uswest" + expectedResourceName2 := "cdns_v1_useast" + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the resourceInfo map should not be empty", func() { + So(resourceInfo, ShouldNotBeEmpty) + }) + Convey(fmt.Sprintf("And the resourceInfo map should contain key %s", expectedResourceName1), func() { + So(resourceInfo, ShouldContainKey, expectedResourceName1) + }) + Convey(fmt.Sprintf("And the host value for that resource should be '%s'", "some.api.uswest.domain.com"), func() { + So(resourceInfo[expectedResourceName1].host, ShouldEqual, "some.api.uswest.domain.com") + }) + Convey(fmt.Sprintf("And the resourceInfo map should contain key %s", expectedResourceName2), func() { + So(resourceInfo, ShouldContainKey, expectedResourceName2) + }) + Convey(fmt.Sprintf("And the host value for that resource should be '%s'", "some.api.useast.domain.com"), func() { + So(resourceInfo[expectedResourceName2].host, ShouldEqual, "some.api.useast.domain.com") + }) + }) + }) + + // Tests that if the override host is present and the value is multi region but multi region requirements are not met, an error should be expected (so the user can fix the swagger config accordingly) + Convey("Given an apiSpecAnalyser loaded with a swagger file containing a compliant terraform resource that has the 'x-terraform-resource-host' extension with a parametrized value some.api.${serviceProviderName}.domain.com and NON matching root level 'x-terraform-resource-regions-serviceProviderName' extension", t, func() { + serviceProviderName := "serviceProviderName" + expectedHost := fmt.Sprintf("some.api.${%s}.domain.com", serviceProviderName) + var swaggerJSON = fmt.Sprintf(` +{ + "swagger":"2.0", + "x-terraform-resource-regions-%s": "uswest, useast", + "paths":{ + "/v1/cdns":{ + "post":{ + "x-terraform-resource-host": "%s", + "summary":"Create cdn", + "parameters":[ + { + "in":"body", + "name":"body", + "description":"Created CDN", + "schema":{ + "$ref":"#/definitions/ContentDeliveryNetwork" + } + } + ] + } + }, + "/v1/cdns/{id}":{ + "get":{ + "summary":"Get cdn by id" + }, + "put":{ + "summary":"Updated cdn" + }, + "delete":{ + "summary":"Delete cdn" + } + } + }, + "definitions":{ + "ContentDeliveryNetwork":{ + "type":"object", + "properties":{ + "id":{ + "type":"string" + } + } + } + } +}`, "someOtherProviderName", expectedHost) + a := initAPISpecAnalyser(swaggerJSON) + Convey("When getResourcesInfo method is called ", func() { + _, err := a.getResourcesInfo() + Convey("Then the error returned should be nil", func() { + So(err, ShouldNotBeNil) + }) + }) + }) } func initAPISpecAnalyser(swaggerContent string) apiSpecAnalyser { diff --git a/openapi/openapiutils/openapi_utils.go b/openapi/openapiutils/openapi_utils.go index 830057460..c48c4fd62 100644 --- a/openapi/openapiutils/openapi_utils.go +++ b/openapi/openapiutils/openapi_utils.go @@ -4,6 +4,7 @@ import ( "github.com/dikhan/terraform-provider-openapi/openapi/terraformutils" "github.com/go-openapi/spec" "regexp" + "strings" ) const fqdnInURLRegex = `\b(?:(?:[^.-/]{0,1})[\w-]{1,63}[-]{0,1}[.]{1})+(?:[a-zA-Z]{2,63})?|localhost(?:[:]\d+)?\b` @@ -92,3 +93,23 @@ func appendOperationParametersIfPresent(parametersGroups parameterGroups, operat } return parametersGroups } + +// StringExtensionExists tries to find a match using the built-in extensions GetString method and if there is no match +// it will try to find a match without converting the key lower case (as done behind the scenes by GetString method). +// Context: The Extensions look up methods tweaks the given key making it lower case and then trying to match against +// the keys in the map. However this may not always work as the Extensions might have been added without going through +// the Add method which lower cases the key, though in situations where the struct was un-marshaled directly instead this +// translation would not have happened and therefore the look up queiry will not find matches +func StringExtensionExists(extensions spec.Extensions, key string) (string, bool) { + var value string + value, exists := extensions.GetString(key) + if !exists { + // Fall back to look up with actual given key name (without converting to lower case as the GetString method from extensions does behind the scenes) + for k, v := range extensions { + if strings.ToLower(k) == strings.ToLower(key) { + return v.(string), true + } + } + } + return value, exists +} diff --git a/openapi/openapiutils/openapi_utils_test.go b/openapi/openapiutils/openapi_utils_test.go index c44593ff7..be5c594dc 100644 --- a/openapi/openapiutils/openapi_utils_test.go +++ b/openapi/openapiutils/openapi_utils_test.go @@ -332,5 +332,51 @@ func TestGetHostFromURL(t *testing.T) { So(domain, ShouldEqual, expectedResult) }) }) + + }) +} + +func TestStringExtensionExists(t *testing.T) { + Convey("Given a list of extensions", t, func() { + extensions := spec.Extensions{ + "directlyUnmarshaled": "value1", + } + extensions.Add("addedViaAddMethod", "value2") + Convey("When StringExtensionExists method is called to look up a key that is not lower case", func() { + value, exists := StringExtensionExists(extensions, "directlyUnmarshaled") + Convey("Then the key should exists", func() { + So(exists, ShouldBeTrue) + }) + Convey("And Then the value should be", func() { + So(value, ShouldEqual, "value1") + }) + }) + Convey("When StringExtensionExists method is called to look up a key that added cammel case but the lookup key is lower cased", func() { + value, exists := StringExtensionExists(extensions, "directlyunmarshaled") + Convey("Then the key should exists", func() { + So(exists, ShouldBeTrue) + }) + Convey("And Then the value should be", func() { + So(value, ShouldEqual, "value1") + }) + }) + Convey("When StringExtensionExists method is called to look up a key that was added via the Add extensions method", func() { + value, exists := StringExtensionExists(extensions, "addedViaAddMethod") + Convey("Then the key should exists", func() { + So(exists, ShouldBeTrue) + }) + Convey("And Then the value should be", func() { + So(value, ShouldEqual, "value2") + }) + }) + Convey("When StringExtensionExists method is called to look up a lower case key that was added via the Add extensions method", func() { + value, exists := StringExtensionExists(extensions, "addedviaaddmethod") + Convey("Then the key should exists", func() { + So(exists, ShouldBeTrue) + }) + Convey("And Then the value should be", func() { + So(value, ShouldEqual, "value2") + }) + }) }) } diff --git a/openapi/resource_info.go b/openapi/resource_info.go index cdec2427f..860442efe 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -6,6 +6,7 @@ import ( "log" "strings" + "github.com/dikhan/terraform-provider-openapi/openapi/openapiutils" "github.com/dikhan/terraform-provider-openapi/openapi/terraformutils" "github.com/go-openapi/spec" "github.com/hashicorp/terraform/helper/schema" @@ -27,6 +28,8 @@ const extTfResourcePollEnabled = "x-terraform-resource-poll-enabled" const extTfResourcePollTargetStatuses = "x-terraform-resource-poll-completed-statuses" const extTfResourcePollPendingStatuses = "x-terraform-resource-poll-pending-statuses" const extTfResourceTimeout = "x-terraform-resource-timeout" +const extTfResourceURL = "x-terraform-resource-host" +const extTfResourceRegionsFmt = "x-terraform-resource-regions-%s" const idDefaultPropertyName = "id" const statusDefaultPropertyName = "status" @@ -295,6 +298,67 @@ func (r resourceInfo) shouldIgnoreResource() bool { return false } +// getResourceOverrideHost checks if the x-terraform-resource-host extension is present and if so returns its value. This +// value will override the global host value, and the API calls for this resource will be made agasint the value returned +func (r resourceInfo) getResourceOverrideHost() string { + if resourceURL, exists := r.createPathInfo.Post.Extensions.GetString(extTfResourceURL); exists && resourceURL != "" { + return resourceURL + } + return "" +} + +func (r resourceInfo) isMultiRegionHost(overrideHost string) (bool, *regexp.Regexp) { + regex, err := regexp.Compile("(\\S+)(\\$\\{(\\S+)\\})(\\S+)") + log.Printf("[DEBUG] failed to compile region identifier regex: %s", err) + if err != nil { + return false, nil + } + return len(regex.FindStringSubmatch(overrideHost)) != 0, regex +} + +// isMultiRegionResource returns true on ly if: +// - the value is parametrized following the pattern: some.subdomain.${keyword}.domain.com, where ${keyword} must be present in the string, otherwise the resource will not be considered multi region +// - there is a matching 'x-terraform-resource-regions-${keyword}' extension defined in the swagger root level (extensions passed in), where ${keyword} will be the value of the parameter in the above URL +// - and finally the value of the extension is an array of strings containing the different regions where the resource can be created +func (r resourceInfo) isMultiRegionResource(extensions spec.Extensions) (bool, map[string]string) { + overrideHost := r.getResourceOverrideHost() + if overrideHost == "" { + return false, nil + } + isMultiRegionHost, regex := r.isMultiRegionHost(overrideHost) + if !isMultiRegionHost { + return false, nil + } + region := regex.FindStringSubmatch(overrideHost) + if len(region) != 5 { + log.Printf("[DEBUG] override host %s provided does not comply with expected regex format", overrideHost) + return false, nil + } + regionIdentifier := region[3] + regionExtensionValue := fmt.Sprintf(extTfResourceRegionsFmt, regionIdentifier) + if resourceRegions, exists := openapiutils.StringExtensionExists(extensions, regionExtensionValue); exists { + resourceRegions = strings.Replace(resourceRegions, " ", "", -1) + regions := strings.Split(resourceRegions, ",") + if len(regions) < 1 { + log.Printf("[DEBUG] could not find any region for '%s' matching region extension %s: '%s'", regionIdentifier, regionExtensionValue, resourceRegions) + return false, nil + } + apiRegionsMap := map[string]string{} + for _, region := range regions { + repStr := fmt.Sprintf("${1}%s$4", region) + apiRegionsMap[region] = regex.ReplaceAllString(overrideHost, repStr) + } + if len(apiRegionsMap) < 1 { + log.Printf("[DEBUG] could not build properly the resource region map for '%s' matching region extension %s: '%s'", regionIdentifier, regionExtensionValue, resourceRegions) + return false, nil + + } + return true, apiRegionsMap + } + log.Printf("missing '%s' root level region extension %s", regionIdentifier, regionExtensionValue) + return false, nil +} + // isResourcePollingEnabled checks whether there is any response code defined for the given responseStatusCode and if so // whether that response contains the extension 'x-terraform-resource-poll-enabled' set to true returning true; // otherwise false is returned diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 0b4f0e7b0..41ad2d3b3 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -1811,3 +1811,178 @@ func TestShouldIgnoreResource(t *testing.T) { }) }) } + +func TestGetResourceOverrideHost(t *testing.T) { + Convey("Given a terraform compliant resource that has a POST operation containing the x-terraform-resource-host with a non parametrized host containing the host to use", t, func() { + expectedHost := "some.api.domain.com" + r := resourceInfo{ + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{ + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfResourceURL: expectedHost, + }, + }, + }, + }, + }, + } + Convey("When getResourceOverrideHost method is called", func() { + host := r.getResourceOverrideHost() + Convey("Then the value returned should be the host value", func() { + So(host, ShouldEqual, expectedHost) + }) + }) + }) + + Convey("Given a terraform compliant resource that has a POST operation containing the x-terraform-resource-host with a parametrized host containing the host to use", t, func() { + expectedHost := "some.api.${serviceProviderName}.domain.com" + r := resourceInfo{ + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{ + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfResourceURL: expectedHost, + }, + }, + }, + }, + }, + } + Convey("When getResourceOverrideHost method is called", func() { + host := r.getResourceOverrideHost() + Convey("Then the value returned should be the host value", func() { + So(host, ShouldEqual, expectedHost) + }) + }) + }) + + Convey("Given a terraform compliant resource that has a POST operation containing the x-terraform-resource-host with an empty string value", t, func() { + expectedHost := "" + r := resourceInfo{ + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{ + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfResourceURL: expectedHost, + }, + }, + }, + }, + }, + } + Convey("When getResourceOverrideHost method is called", func() { + host := r.getResourceOverrideHost() + Convey("Then the value returned should be the host value", func() { + So(host, ShouldEqual, expectedHost) + }) + }) + }) +} + +func TestIsMultiRegionHost(t *testing.T) { + Convey("Given a resourceInfo", t, func() { + r := resourceInfo{} + Convey("When isMultiRegionHost method is called with a non multi region host", func() { + expectedHost := "some.api.domain.com" + isMultiRegion, _ := r.isMultiRegionHost(expectedHost) + Convey("Then the value returned should be false", func() { + So(isMultiRegion, ShouldBeFalse) + }) + }) + Convey("When isMultiRegionHost method is called with a multi region host", func() { + expectedHost := "some.api.${%s}.domain.com" + isMultiRegion, _ := r.isMultiRegionHost(expectedHost) + Convey("Then the value returned should be true", func() { + So(isMultiRegion, ShouldBeTrue) + }) + }) + Convey("When isMultiRegionHost method is called with a multi region host that has region at the beginning", func() { + expectedHost := "${%s}.domain.com" + isMultiRegion, _ := r.isMultiRegionHost(expectedHost) + Convey("Then the value returned should be false", func() { + So(isMultiRegion, ShouldBeFalse) + }) + }) + }) +} + +func TestIsMultiRegionResource(t *testing.T) { + Convey("Given a terraform compliant resource that has a POST operation containing the x-terraform-resource-host with a parametrized host containing region variable", t, func() { + serviceProviderName := "serviceProviderName" + r := resourceInfo{ + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{ + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfResourceURL: fmt.Sprintf("some.api.${%s}.domain.com", serviceProviderName), + }, + }, + }, + }, + }, + } + Convey("When isMultiRegionResource method is called with a set of extensions where one matches the region for which the above 's-terraform-resource-host' extension is for", func() { + rootLevelExtensions := spec.Extensions{} + rootLevelExtensions.Add(fmt.Sprintf(extTfResourceRegionsFmt, serviceProviderName), "uswest,useast") + isMultiRegion, regions := r.isMultiRegionResource(rootLevelExtensions) + Convey("Then the value returned should be true", func() { + So(isMultiRegion, ShouldBeTrue) + }) + Convey("And the map returned should contain uswest", func() { + So(regions, ShouldContainKey, "uswest") + So(regions["uswest"], ShouldEqual, "some.api.uswest.domain.com") + }) + Convey("And the map returned should contain useast", func() { + So(regions, ShouldContainKey, "useast") + So(regions["useast"], ShouldEqual, "some.api.useast.domain.com") + }) + }) + + Convey("When isMultiRegionResource method is called with a set of extensions where NONE matches the region for which the above 's-terraform-resource-host' extension is for", func() { + rootLevelExtensions := spec.Extensions{} + rootLevelExtensions.Add(fmt.Sprintf(extTfResourceRegionsFmt, "someOtherServiceProvider"), "rst, dub") + isMultiRegion, regions := r.isMultiRegionResource(rootLevelExtensions) + Convey("Then the value returned should be true", func() { + So(isMultiRegion, ShouldBeFalse) + }) + Convey("And the regions map returned should be empty", func() { + So(regions, ShouldBeEmpty) + }) + }) + + Convey("When isMultiRegionResource method is called with a set of extensions where one matches the region for which the above 's-terraform-resource-host' extension is for BUT the values are not comma separated", func() { + rootLevelExtensions := spec.Extensions{} + rootLevelExtensions.Add(fmt.Sprintf(extTfResourceRegionsFmt, serviceProviderName), "uswest useast") + isMultiRegion, regions := r.isMultiRegionResource(rootLevelExtensions) + Convey("Then the value returned should be true", func() { + So(isMultiRegion, ShouldBeTrue) + }) + Convey("And the map returned should contain uswest", func() { + So(regions, ShouldContainKey, "uswestuseast") + So(regions["uswestuseast"], ShouldEqual, "some.api.uswestuseast.domain.com") + }) + }) + + Convey("When isMultiRegionResource method is called with a set of extensions where one matches the region for which the above 's-terraform-resource-host' extension is for BUT the values are comma separated with spaces", func() { + rootLevelExtensions := spec.Extensions{} + rootLevelExtensions.Add(fmt.Sprintf(extTfResourceRegionsFmt, serviceProviderName), "uswest, useast") + isMultiRegion, regions := r.isMultiRegionResource(rootLevelExtensions) + Convey("Then the value returned should be true", func() { + So(isMultiRegion, ShouldBeTrue) + }) + Convey("And the map returned should contain uswest", func() { + So(regions, ShouldContainKey, "uswest") + So(regions["uswest"], ShouldEqual, "some.api.uswest.domain.com") + }) + Convey("And the map returned should contain useast", func() { + So(regions, ShouldContainKey, "useast") + So(regions["useast"], ShouldEqual, "some.api.useast.domain.com") + }) + }) + }) +} From e86a832a5eaab20b744802922c28782e32bc5377 Mon Sep 17 00:00:00 2001 From: dikhan Date: Wed, 12 Sep 2018 13:17:26 -0700 Subject: [PATCH 41/60] move debug log to error section --- openapi/resource_info.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi/resource_info.go b/openapi/resource_info.go index 860442efe..cec5058d0 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -309,8 +309,8 @@ func (r resourceInfo) getResourceOverrideHost() string { func (r resourceInfo) isMultiRegionHost(overrideHost string) (bool, *regexp.Regexp) { regex, err := regexp.Compile("(\\S+)(\\$\\{(\\S+)\\})(\\S+)") - log.Printf("[DEBUG] failed to compile region identifier regex: %s", err) if err != nil { + log.Printf("[DEBUG] failed to compile region identifier regex: %s", err) return false, nil } return len(regex.FindStringSubmatch(overrideHost)) != 0, regex @@ -355,7 +355,7 @@ func (r resourceInfo) isMultiRegionResource(extensions spec.Extensions) (bool, m } return true, apiRegionsMap } - log.Printf("missing '%s' root level region extension %s", regionIdentifier, regionExtensionValue) + log.Printf("missing matching '%s' root level region extension '%s'", regionIdentifier, regionExtensionValue) return false, nil } From e81b12bb0ca3c854210c8550bb69d7c57a511fdd Mon Sep 17 00:00:00 2001 From: dikhan Date: Wed, 12 Sep 2018 13:53:15 -0700 Subject: [PATCH 42/60] update log output to include overriden host --- openapi/api_spec_analyser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi/api_spec_analyser.go b/openapi/api_spec_analyser.go index 12d5853ac..4cb970b26 100644 --- a/openapi/api_spec_analyser.go +++ b/openapi/api_spec_analyser.go @@ -66,7 +66,7 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) { regionResource = r regionResource.name = resourceRegionName regionResource.host = regionHost - log.Printf("[INFO] multi region resource name for region %s = %s", regionName, resourceRegionName) + log.Printf("[INFO] multi region resource: name = %s, region = %s, host = %s", regionName, resourceRegionName, regionHost) resources[resourceRegionName] = regionResource } continue From 03e4fac333c98fc24379f978d278732a23e6278a Mon Sep 17 00:00:00 2001 From: dikhan Date: Wed, 12 Sep 2018 13:53:40 -0700 Subject: [PATCH 43/60] add x-terraform-resource-host to the example --- examples/swaggercodegen/api/resources/swagger.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 9c19c85ec..4ac9c282b 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -48,6 +48,7 @@ paths: /v1/cdns: post: + x-terraform-resource-host: localhost:8443 tags: - "cdn" summary: "Create cdn" From 050fecd746da9a77146fbc8028aee9990b375d06 Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 13 Sep 2018 10:03:36 -0700 Subject: [PATCH 44/60] add 'x-terraform-resource-host' and 'x-terraform-resource-regions-%s' docs --- docs/how_to.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/docs/how_to.md b/docs/how_to.md index 7a55b4632..6c31336e8 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -265,6 +265,8 @@ Extension Name | Type | Description [x-terraform-resource-timeout](#xTerraformResourceTimeout) | string | Only available in operation level. Defines the timeout for a given operation. This value overrides the default timeout operation value which is 10 minutes. [x-terraform-header](#xTerraformHeader) | string | Only available in operation level parameters at the moment. Defines that he given header should be passed as part of the request. [x-terraform-resource-poll-enabled](#xTerraformResourcePollEnabled) | bool | Only supported in operation responses (e,g: 202). Defines that if the API responds with the given HTTP Status code (e,g: 202), the polling mechanism will be enabled. This allows the OpenAPI Terraform provider to perform read calls to the remote API and check the resource state. The polling mechanism finalises if the remote resource state arrives at completion, failure state or times-out (60s) +[x-terraform-resource-host](#xTerraformResourceHost) | string | Only supported in resource root's POST operation. Defines the host that should be used when managing this specific resource. The value of this extension effectively overrides the global host configuration, making the OpenAPI Terraform provider client make thje API calls against the host specified in this extension value instead of the global host configuration. The protocols (HTTP/HTTPS) and base path (if anything other than "/") used when performing the API calls will still come from the global configuration. +[x-terraform-resource-regions-%s](#xTerraformResourceRegions) | string | Only supported in the root level. Defines the regions supported by a given resource identified by the %s variable. This extension only works if the ```x-terraform-resource-host``` extension contains a value that is parametrized and identifies the matching ```x-terraform-resource-regions-%s``` extension. The values of this extension must be comma separated strings. ###### x-terraform-exclude-resource @@ -429,6 +431,82 @@ definitions: *Note: This extension is only supported at the operation's response level.* + +###### x-terraform-resource-host + +This extension allows resources to override the global host configuration with a different host. This is handy when +a given swagger file may combine resources provided by different service providers. + +```` +swagger: "2.0" +host: "some.domain.com" +paths: + /v1/cdns: + post: + x-terraform-resource-host: cdn.api.otherdomain.com +```` + +The above configuration will make the OpenAPI Terraform provider client make API CRUD requests (POST/GET/PUT/DELETE) to +the overridden host instead, in this case ```cdn.api.otherdomain.com```. + +*Note: This extension is only supported at the operation's POST operation level. The other operations available for the +resource such as GET/PUT/DELETE will used the overridden host value too.* + +####### Multi-region resources + +Additionally, if the resource is using multi region domains, meaning there's one sub-domain for each region where the resource +can be created into (similar to how aws resources are created per region), this can be configured as follows: + +```` +swagger: "2.0" +host: "some.domain.com" +x-terraform-resource-regions-cdn: "dub1,sea1" +paths: + /v1/cdns: + post: + x-terraform-resource-host: cdn.${cdn}.api.otherdomain.com +```` + +If the ``x-terraform-resource-host`` extension has a value parametrised in the form where the following pattern ```${identifier}``` + is found (identifier being any string with no whitspaces - spaces,tabs, line breaks, etc) AND there is a matching + extension 'x-terraform-resource-regions-**identifier**' defined in the root level that refers to the same identifier + then the resource will be considered multi region. +For instance, in the above example, the ```x-terraform-resource-host``` value is parametrised as the ```${identifier}``` pattern +is found, and the identifier in this case is ```cdn```. Moreover, there is a matching ```x-terraform-resource-regions-cdn``` +extension containing a list of regions where this resource can be created in. + +The regions found in the ```x-terraform-resource-regions-cdn``` will be used as follows: + +- The OpenAPI Terraform provider will expose one resource per region enlisted in the extension. In the case above, the +following resources will become available in the Terraform configuration (the provider name chosen here is 'swaggercodegen'): + +```` +resource "swaggercodegen_cdn_v1_dub1" "my_cdn" { + label = "label" + ips = ["127.0.0.1"] + hostnames = ["origin.com"] +} + +resource "swaggercodegen_cdn_v1_sea1" "my_cdn" { + label = "label" + ips = ["127.0.0.1"] + hostnames = ["origin.com"] +} +```` + +As shown above, the resources that are multi-region will have extra information in their name that identifies the region +where tha resource should be managed. + +- The OpenAPI Terraform provider client will make the API call against the specific resource region when the resource +is configured with multi-region support. + +- As far as the resource configuration is concerned, the swagger configuration remains the same for that specific resource +(parameters, operations, polling support, etc) and the same configuration will be applicable to all the regions that resource +supports. + +*Note: This extension is only supported at the root level and can be used exclusively along with the 'x-terraform-resource-host' +extension* + #### Definitions - **Field Name:** definitions From 9f7837e0ac64a0804ee4a4b9090d8d9cbc75228c Mon Sep 17 00:00:00 2001 From: dikhan Date: Thu, 13 Sep 2018 17:38:05 -0700 Subject: [PATCH 45/60] add support for x-terraform-resource-name extension - updated cdn example provider to reflect this feature - moved getResourceName from api spec analyser to resource info --- .../swaggercodegen/api/resources/swagger.yaml | 1 + examples/swaggercodegen/main.tf | 2 +- openapi/api_spec_analyser.go | 50 ++--- openapi/api_spec_analyser_test.go | 52 ------ openapi/provider_factory.go | 2 + openapi/resource_factory.go | 14 +- openapi/resource_info.go | 53 +++++- openapi/resource_info_test.go | 176 ++++++++++++++++++ 8 files changed, 250 insertions(+), 100 deletions(-) diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 9c19c85ec..6436126e7 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -48,6 +48,7 @@ paths: /v1/cdns: post: + x-terraform-resource-name: "cdn" tags: - "cdn" summary: "Create cdn" diff --git a/examples/swaggercodegen/main.tf b/examples/swaggercodegen/main.tf index dc3cfc810..addfd7b93 100644 --- a/examples/swaggercodegen/main.tf +++ b/examples/swaggercodegen/main.tf @@ -5,7 +5,7 @@ provider "swaggercodegen" { x_request_id = "request header value for POST /v1/cdns" } -resource "swaggercodegen_cdns_v1" "my_cdn" { +resource "swaggercodegen_cdn_v1" "my_cdn" { label = "label" ## This is an immutable property (refer to swagger file) ips = ["127.0.0.1"] ## This is a force-new property (refer to swagger file) hostnames = ["origin.com"] diff --git a/openapi/api_spec_analyser.go b/openapi/api_spec_analyser.go index 405ebd394..d574b080a 100644 --- a/openapi/api_spec_analyser.go +++ b/openapi/api_spec_analyser.go @@ -10,8 +10,6 @@ import ( "github.com/go-openapi/spec" ) -const resourceVersionRegex = "(/v[0-9]*/)" -const resourceNameRegex = "((/\\w*/){\\w*})+$" const resourceInstanceRegex = "((?:.*)){.*}" const swaggerResourcePayloadDefinitionRegex = "(\\w+)[^//]*$" @@ -33,16 +31,11 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) { for resourcePath, pathItem := range asa.d.Spec().Paths.Paths { resourceRootPath, resourceRoot, resourcePayloadSchemaDef, err := asa.isEndPointFullyTerraformResourceCompliant(resourcePath) if err != nil { - log.Printf("[DEBUG] resource paht '%s' not terraform compliant: %s", resourcePath, err) - continue - } - resourceName, err := asa.getResourceName(resourcePath) - if err != nil { - log.Printf("[DEBUG] resource not figure out valid terraform resource name for '%s': %s", resourcePath, err) + log.Printf("[DEBUG] resource path '%s' not terraform compliant: %s", resourcePath, err) continue } + r := resourceInfo{ - name: resourceName, basePath: asa.d.BasePath(), path: resourceRootPath, host: asa.d.Spec().Host, @@ -51,9 +44,19 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) { createPathInfo: *resourceRoot, pathInfo: pathItem, } - if !r.shouldIgnoreResource() { - resources[resourceName] = r + + resourceName, err := r.getResourceName() + if err != nil { + log.Printf("[DEBUG] could not figure out the resource name for '%s': %s", resourcePath, err) + continue + } + + if r.shouldIgnoreResource() { + continue } + + log.Printf("[INFO] resource info created '%s'", resourceName) + resources[resourceName] = r } return resources, nil } @@ -268,31 +271,6 @@ func (asa apiSpecAnalyser) isResourceInstanceEndPoint(p string) (bool, error) { return r.MatchString(p), nil } -// getResourceName gets the name of the resource from a path /resource/{id} -func (asa apiSpecAnalyser) getResourceName(resourcePath string) (string, error) { - nameRegex, err := regexp.Compile(resourceNameRegex) - if err != nil { - return "", fmt.Errorf("an error occurred while compiling the resourceNameRegex regex '%s': %s", resourceNameRegex, err) - } - var resourceName string - matches := nameRegex.FindStringSubmatch(resourcePath) - if len(matches) < 2 { - return "", fmt.Errorf("could not find a valid name for resource instance path '%s'", resourcePath) - } - resourceName = strings.Replace(matches[len(matches)-1], "/", "", -1) - versionRegex, err := regexp.Compile(resourceVersionRegex) - if err != nil { - return "", fmt.Errorf("an error occurred while compiling the resourceVersionRegex regex '%s': %s", resourceVersionRegex, err) - } - versionMatches := versionRegex.FindStringSubmatch(resourcePath) - if len(versionMatches) != 0 { - version := strings.Replace(versionRegex.FindStringSubmatch(resourcePath)[1], "/", "", -1) - resourceNameWithVersion := fmt.Sprintf("%s_%s", resourceName, version) - return resourceNameWithVersion, nil - } - return resourceName, nil -} - // findMatchingResourceRootPath returns the corresponding POST root and path for a given end point // Example: Given 'resourcePath' being "/users/{username}" the result could be "/users" or "/users/" depending on // how the POST operation (resourceRootPath) of the given resource is defined in swagger. diff --git a/openapi/api_spec_analyser_test.go b/openapi/api_spec_analyser_test.go index 2970842d5..4000bb202 100644 --- a/openapi/api_spec_analyser_test.go +++ b/openapi/api_spec_analyser_test.go @@ -518,58 +518,6 @@ paths: } -func TestGetResourceName(t *testing.T) { - Convey("Given an apiSpecAnalyser", t, func() { - a := apiSpecAnalyser{} - Convey("When getResourceName method is called with a valid resource instance path such as '/users/{id}'", func() { - resourceName, err := a.getResourceName("/users/{id}") - Convey("Then the error returned should be nil", func() { - So(err, ShouldBeNil) - }) - Convey("And the value returned should be 'users'", func() { - So(resourceName, ShouldEqual, "users") - }) - }) - - Convey("When getResourceName method is called with an invalid resource instance path such as '/resource/not/instance/path'", func() { - _, err := a.getResourceName("/resource/not/instance/path") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldNotBeNil) - }) - }) - - Convey("When getResourceName method is called with a valid resource instance path that is versioned such as '/v1/users/{id}'", func() { - resourceName, err := a.getResourceName("/v1/users/{id}") - Convey("Then the error returned should be nil", func() { - So(err, ShouldBeNil) - }) - Convey("And the value returned should be 'users_v1'", func() { - So(resourceName, ShouldEqual, "users_v1") - }) - }) - - Convey("When getResourceName method is called with a valid resource instance long path that is versioned such as '/v1/something/users/{id}'", func() { - resourceName, err := a.getResourceName("/v1/something/users/{id}") - Convey("Then the error returned should be nil", func() { - So(err, ShouldBeNil) - }) - Convey("And the value returned should still be 'users_v1'", func() { - So(resourceName, ShouldEqual, "users_v1") - }) - }) - - Convey("When getResourceName method is called with resource instance which has path parameters '/api/v1/nodes/{name}/proxy/{path}'", func() { - resourceName, err := a.getResourceName("/api/v1/nodes/{name}/proxy/{path}") - Convey("Then the error returned should be nil", func() { - So(err, ShouldBeNil) - }) - Convey("And the value returned should still be 'proxy_v1'", func() { - So(resourceName, ShouldEqual, "proxy_v1") - }) - }) - }) -} - func TestPostIsPresent(t *testing.T) { Convey("Given an apiSpecAnalyser with a path '/users' that has a post operation", t, func() { diff --git a/openapi/provider_factory.go b/openapi/provider_factory.go index b94f54711..14f9c819b 100644 --- a/openapi/provider_factory.go +++ b/openapi/provider_factory.go @@ -12,6 +12,7 @@ import ( "github.com/go-openapi/loads" "github.com/go-openapi/spec" "github.com/hashicorp/terraform/helper/schema" + "log" ) type providerFactory struct { @@ -69,6 +70,7 @@ func (p providerFactory) generateProviderFromAPISpec(apiSpecAnalyser *apiSpecAna return nil, err } resourceName := p.getProviderResourceName(resourceName) + log.Printf("[INFO] open api terraform compatible resource registered with the provider '%s'", resourceName) resourceMap[resourceName] = resource } provider := &schema.Provider{ diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index 6a28ac9c6..da8b00167 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -106,7 +106,7 @@ func (r resourceFactory) create(resourceLocalData *schema.ResourceData, i interf if err != nil { return err } - log.Printf("[INFO] Resource '%s' ID: %s", r.resourceInfo.name, resourceLocalData.Id()) + log.Printf("[INFO] Resource '%s' ID: %s", r.resourceInfo.path, resourceLocalData.Id()) err = r.handlePollingIfConfigured(&responsePayload, resourceLocalData, providerConfig, operation.Responses, res.StatusCode, schema.TimeoutCreate) if err != nil { @@ -164,7 +164,7 @@ func (r resourceFactory) update(resourceLocalData *schema.ResourceData, i interf providerConfig := i.(providerConfig) operation := r.resourceInfo.pathInfo.Put if operation == nil { - return fmt.Errorf("%s resource does not support PUT opperation, check the swagger file exposed on '%s'", r.resourceInfo.name, r.resourceInfo.host) + return fmt.Errorf("%s resource does not support PUT opperation, check the swagger file exposed on '%s'", r.resourceInfo.path, r.resourceInfo.host) } input := r.createPayloadFromLocalStateData(resourceLocalData) responsePayload := map[string]interface{}{} @@ -204,7 +204,7 @@ func (r resourceFactory) delete(resourceLocalData *schema.ResourceData, i interf providerConfig := i.(providerConfig) operation := r.resourceInfo.pathInfo.Delete if operation == nil { - return fmt.Errorf("%s resource does not support DELETE opperation, check the swagger file exposed on '%s'", r.resourceInfo.name, r.resourceInfo.host) + return fmt.Errorf("%s resource does not support DELETE opperation, check the swagger file exposed on '%s'", r.resourceInfo.path, r.resourceInfo.host) } resourceIDURL, err := r.resourceInfo.getResourceIDURL(resourceLocalData.Id()) if err != nil { @@ -264,7 +264,7 @@ func (r resourceFactory) handlePollingIfConfigured(responsePayload *map[string]i } log.Printf("[DEBUG] target statuses (%s); pending statuses (%s)", targetStatuses, pendingStatuses) - log.Printf("[INFO] Waiting for resource '%s' to reach a completion status (%s)", r.resourceInfo.name, targetStatuses) + log.Printf("[INFO] Waiting for resource '%s' to reach a completion status (%s)", r.resourceInfo.path, targetStatuses) stateConf := &resource.StateChangeConf{ Pending: pendingStatuses, @@ -297,12 +297,12 @@ func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.Reso return remoteData, defaultDestroyStatus, nil } } - return nil, "", fmt.Errorf("error on retrieving resource '%s' (%s) when waiting: %s", r.resourceInfo.name, resourceLocalData.Id(), err) + return nil, "", fmt.Errorf("error on retrieving resource '%s' (%s) when waiting: %s", r.resourceInfo.path, resourceLocalData.Id(), err) } statusIdentifier, err := r.resourceInfo.getStatusIdentifier() if err != nil { - return nil, "", fmt.Errorf("error occurred while retrieving status identifier for resource '%s' (%s): %s", r.resourceInfo.name, resourceLocalData.Id(), err) + return nil, "", fmt.Errorf("error occurred while retrieving status identifier for resource '%s' (%s): %s", r.resourceInfo.path, resourceLocalData.Id(), err) } value, statusIdentifierPresentInResponse := remoteData[statusIdentifier] @@ -310,7 +310,7 @@ func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.Reso return nil, "", fmt.Errorf("response payload received from GET /%s/%s missing the status identifier field", r.resourceInfo.path, resourceLocalData.Id()) } newStatus := value.(string) - log.Printf("[DEBUG] resource '%s' status (%s): %s", r.resourceInfo.name, resourceLocalData.Id(), newStatus) + log.Printf("[DEBUG] resource '%s' status (%s): %s", r.resourceInfo.path, resourceLocalData.Id(), newStatus) return remoteData, newStatus, nil } } diff --git a/openapi/resource_info.go b/openapi/resource_info.go index cdec2427f..47dd8b536 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -13,6 +13,9 @@ import ( "time" ) +const resourceVersionRegex = "(/v[0-9]*/)" +const resourceNameRegex = "((/\\w*[/]?))+$" + // Definition level extensions const extTfImmutable = "x-terraform-immutable" const extTfForceNew = "x-terraform-force-new" @@ -27,6 +30,7 @@ const extTfResourcePollEnabled = "x-terraform-resource-poll-enabled" const extTfResourcePollTargetStatuses = "x-terraform-resource-poll-completed-statuses" const extTfResourcePollPendingStatuses = "x-terraform-resource-poll-pending-statuses" const extTfResourceTimeout = "x-terraform-resource-timeout" +const extTfResourceName = "x-terraform-resource-name" const idDefaultPropertyName = "id" const statusDefaultPropertyName = "status" @@ -35,7 +39,6 @@ type resourcesInfo map[string]resourceInfo // resourceInfo serves as translator between swagger definitions and terraform schemas type resourceInfo struct { - name string basePath string // path contains relative path to the resource e,g: /v1/resource path string @@ -87,10 +90,10 @@ func (r resourceInfo) validateFunc(propertyName string, property spec.Schema) sc if property.Default != nil { if property.ReadOnly { err := fmt.Errorf( - "'%s.%s' is configured as 'readOnly' and can not have a default value. The value is expected to be computed by the API. To fix the issue, pick one of the following options:\n"+ + "'%s' is configured as 'readOnly' and can not have a default value. The value is expected to be computed by the API. To fix the issue, pick one of the following options:\n"+ "1. Remove the 'readOnly' attribute from %s in the swagger file so the default value '%v' can be applied. Default must be nil if computed\n"+ "OR\n"+ - "2. Remove the 'default' attribute from %s in the swagger file, this means that the API will compute the value as specified by the 'readOnly' attribute\n", r.name, k, k, property.Default, k) + "2. Remove the 'default' attribute from %s in the swagger file, this means that the API will compute the value as specified by the 'readOnly' attribute\n", k, k, property.Default, k) errors = append(errors, err) } } @@ -164,7 +167,7 @@ func (r resourceInfo) createTerraformPropertyBasicSchema(propertyName string, pr if property.ReadOnly { // Below we just log a warn message; however, the validateFunc will take care of throwing an error if the following happens // Check r.validateFunc which will handle this use case on runtime and provide the user with a detail description of the error - log.Printf("[WARN] '%s.%s' is readOnly and can not have a default value. The value is expected to be computed by the API. Terraform will fail on runtime when performing the property validation check", r.name, propertyName) + log.Printf("[WARN] '%s' is readOnly and can not have a default value. The value is expected to be computed by the API. Terraform will fail on runtime when performing the property validation check", propertyName) } else { propertySchema.Default = property.Default } @@ -354,6 +357,17 @@ func (r resourceInfo) getDuration(t string) (*time.Duration, error) { return &duration, err } +func (r resourceInfo) getResourceTerraformName() string { + return r.getExtensionStringValue(r.createPathInfo.Post.Extensions, extTfResourceName) +} + +func (r resourceInfo) getExtensionStringValue(extensions spec.Extensions, key string) string { + if value, exists := extensions.GetString(key); exists && value != "" { + return value + } + return "" +} + func (r resourceInfo) isIDProperty(propertyName string) bool { return r.propertyNameMatchesDefaultName(propertyName, idDefaultPropertyName) } @@ -365,3 +379,34 @@ func (r resourceInfo) isStatusProperty(propertyName string) bool { func (r resourceInfo) propertyNameMatchesDefaultName(propertyName, expectedPropertyName string) bool { return terraformutils.ConvertToTerraformCompliantName(propertyName) == expectedPropertyName } + +// getResourceName gets the name of the resource from a path /resource/{id} +func (r resourceInfo) getResourceName() (string, error) { + nameRegex, err := regexp.Compile(resourceNameRegex) + if err != nil { + return "", fmt.Errorf("an error occurred while compiling the resourceNameRegex regex '%s': %s", resourceNameRegex, err) + } + var resourceName string + resourcePath := r.path + matches := nameRegex.FindStringSubmatch(resourcePath) + if len(matches) < 2 { + return "", fmt.Errorf("could not find a valid name for resource instance path '%s'", resourcePath) + } + resourceName = strings.Replace(matches[len(matches)-1], "/", "", -1) + + if preferredName := r.getResourceTerraformName(); preferredName != "" { + resourceName = preferredName + } + + versionRegex, err := regexp.Compile(resourceVersionRegex) + if err != nil { + return "", fmt.Errorf("an error occurred while compiling the resourceVersionRegex regex '%s': %s", resourceVersionRegex, err) + } + versionMatches := versionRegex.FindStringSubmatch(resourcePath) + if len(versionMatches) != 0 { + version := strings.Replace(versionRegex.FindStringSubmatch(resourcePath)[1], "/", "", -1) + resourceNameWithVersion := fmt.Sprintf("%s_%s", resourceName, version) + return resourceNameWithVersion, nil + } + return resourceName, nil +} diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 0b4f0e7b0..95376f060 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -1811,3 +1811,179 @@ func TestShouldIgnoreResource(t *testing.T) { }) }) } + +func TestGetResourceTerraformName(t *testing.T) { + Convey("Given a resource info", t, func() { + expectedResourceName := "cdn" + r := resourceInfo{ + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{ + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfResourceName: expectedResourceName, + }, + }, + }, + }, + }, + } + Convey("When getResourceTerraformName method is called with a map of extensions", func() { + value := r.getResourceTerraformName() + Convey(fmt.Sprintf("Then the value returned should equal %s", expectedResourceName), func() { + So(value, ShouldEqual, expectedResourceName) + }) + }) + }) +} + +func TestGetExtensionStringValue(t *testing.T) { + Convey("Given a resource info", t, func() { + r := resourceInfo{} + Convey("When getExtensionStringValue method is called with a map of extensions and an existing key in the map", func() { + expectedResourceName := "cdn" + extensions := spec.Extensions{ + extTfResourceName: expectedResourceName, + } + value := r.getExtensionStringValue(extensions, extTfResourceName) + Convey("Then the value returned should equal", func() { + So(value, ShouldEqual, expectedResourceName) + }) + }) + Convey("When getExtensionStringValue method is called with a map of extensions and a NON existing key in the map", func() { + expectedResourceName := "cdn" + extensions := spec.Extensions{ + extTfResourceName: expectedResourceName, + } + value := r.getExtensionStringValue(extensions, "nonExistingKey") + Convey("Then the value returned should equal", func() { + So(value, ShouldBeEmpty) + }) + }) + }) +} + +func TestGetResourceName(t *testing.T) { + Convey("Given a resourceInfo configured with a path /users/ containing a trailing slash", t, func() { + r := resourceInfo{ + path: "/users/", + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{}, + }, + }, + } + Convey("When getResourceName method is called", func() { + resourceName, err := r.getResourceName() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the value returned should be 'users'", func() { + So(resourceName, ShouldEqual, "users") + }) + }) + }) + Convey("Given a resourceInfo configured with a path /users with no trailing slash", t, func() { + r := resourceInfo{ + path: "/users", + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{}, + }, + }, + } + Convey("When getResourceName method is called", func() { + resourceName, err := r.getResourceName() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the value returned should be 'users'", func() { + So(resourceName, ShouldEqual, "users") + }) + }) + }) + Convey("Given a resourceInfo configured with a valid resource path that is versioned such as '/v1/users/'", t, func() { + r := resourceInfo{ + path: "/v1/users/", + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{}, + }, + }, + } + Convey("When getResourceName method is called", func() { + resourceName, err := r.getResourceName() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the value returned should be 'users_v1'", func() { + So(resourceName, ShouldEqual, "users_v1") + }) + }) + }) + Convey("Given a resourceInfo configured with a valid resource long path that is versioned such as '/v1/something/users'", t, func() { + r := resourceInfo{ + path: "/v1/something/users", + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{}, + }, + }, + } + Convey("When getResourceName method is called", func() { + resourceName, err := r.getResourceName() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the value returned should still be 'users_v1'", func() { + So(resourceName, ShouldEqual, "users_v1") + }) + }) + }) + Convey("Given a resourceInfo configured with resource which has path parameters '/api/v1/nodes/{name}/proxy'", t, func() { + r := resourceInfo{ + path: "/api/v1/nodes/{name}/proxy", + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{}, + }, + }, + } + Convey("When getResourceName method is called", func() { + resourceName, err := r.getResourceName() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the value returned should still be 'proxy_v1'", func() { + So(resourceName, ShouldEqual, "proxy_v1") + }) + }) + }) + Convey("Given a resourceInfo configured with resource with path '/users' and the create operation has the extension 'x-terraform-resource-name' ", t, func() { + expectedResourceName := "user" + r := resourceInfo{ + path: "/v1/users", + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{ + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + extTfResourceName: expectedResourceName, + }, + }, + }, + }, + }, + } + Convey("When getResourceName method is called", func() { + resourceName, err := r.getResourceName() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + expectedTerraformName := fmt.Sprintf("%s_v1", expectedResourceName) + Convey(fmt.Sprintf("And the value returned should still be '%s'", expectedTerraformName), func() { + So(resourceName, ShouldEqual, expectedTerraformName) + }) + }) + }) +} From 7bed18f452da398beb4ba7564209575376c99ba2 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 14 Sep 2018 10:47:40 -0700 Subject: [PATCH 46/60] update docs including x-terraform-resource-name extension info --- docs/how_to.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/docs/how_to.md b/docs/how_to.md index 7a55b4632..008bfb6ed 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -265,6 +265,7 @@ Extension Name | Type | Description [x-terraform-resource-timeout](#xTerraformResourceTimeout) | string | Only available in operation level. Defines the timeout for a given operation. This value overrides the default timeout operation value which is 10 minutes. [x-terraform-header](#xTerraformHeader) | string | Only available in operation level parameters at the moment. Defines that he given header should be passed as part of the request. [x-terraform-resource-poll-enabled](#xTerraformResourcePollEnabled) | bool | Only supported in operation responses (e,g: 202). Defines that if the API responds with the given HTTP Status code (e,g: 202), the polling mechanism will be enabled. This allows the OpenAPI Terraform provider to perform read calls to the remote API and check the resource state. The polling mechanism finalises if the remote resource state arrives at completion, failure state or times-out (60s) +[x-terraform-resource-name](#xTerraformResourceName) | string | Only available in resource root's POST operation. Defines the name that will be used for the resource in the Terraform configuration. If the extension is not preset, default value will be the name of the resource in the path. For instance, a path such as /v1/users will translate into a terraform resource name users_v1 ###### x-terraform-exclude-resource @@ -429,6 +430,48 @@ definitions: *Note: This extension is only supported at the operation's response level.* + +###### x-terraform-resource-name + +This extension enables service providers to write a preferred resource name for the terraform configuration. + +```` +paths: + /cdns: + post: + x-terraform-resource-name: "cdn" +```` + +In the example above, the resource POST operation contains the extension ``x-terraform-resource-name`` with value ``cdn``. +This value will be the name used in the terraform configuration``cdn``. + +```` +resource "swaggercodegen_cdn" "my_cdn" {...} # ==> 'cdn' name is used as specified by the `x-terraform-resource-name` extension +```` + +The preferred name only applies to the name itself, if the resource is versioned like the example below +using version path ``/v1/cdns``, the appropriate postfix including the version will be attached automatically to the resource name. + +```` +paths: + /v1/cdns: + post: + x-terraform-resource-name: "cdn" +```` + +The corresponding terraform configuration in this case will be (note the ``_v1`` after the resource name): + +```` +resource "swaggercodegen_cdn_v1" "my_cdn" {...} # ==> 'cdn' name is used instead of 'cdns' +```` + +If the ``x-terraform-resource-name`` extension is not present in the resource root POST operation, the default resource +name will be picked from the resource root POST path. In the above example ``/v1/cdns`` would translate into ``cdns_v1`` +resource name. + +*Note: This extension is only interpreted and handled in resource root POST operations (e,g: /v1/resource) in the +above example* + #### Definitions - **Field Name:** definitions From d97713e2e0623d6e91b8ae2bc4d436b6a125e699 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 14 Sep 2018 10:50:26 -0700 Subject: [PATCH 47/60] quick fix in pr template --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 73d455334..abf9bd57a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,7 @@ ## Proposed changes -Please add as many details as possible about the change here. Does this Pull Request resolve any open issue? If so, please -make sure to link to that issue: +Please add as many details as possible about the change here. Does this Pull Request resolve any open issue? +If so, please make sure to link to that issue: Fixes: # From f6c8cee64b3a5745c46255b77005a8441f5630b2 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 14 Sep 2018 12:22:54 -0700 Subject: [PATCH 48/60] add test for version higher than 9 --- openapi/resource_info_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 95376f060..cceaeeb89 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -1921,6 +1921,25 @@ func TestGetResourceName(t *testing.T) { }) }) }) + Convey("Given a resourceInfo configured with a valid resource path that is versioned with nuber higher than 9 such as '/v12/users/'", t, func() { + r := resourceInfo{ + path: "/v12/users/", + createPathInfo: spec.PathItem{ + PathItemProps: spec.PathItemProps{ + Post: &spec.Operation{}, + }, + }, + } + Convey("When getResourceName method is called", func() { + resourceName, err := r.getResourceName() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the value returned should be 'users_v1'", func() { + So(resourceName, ShouldEqual, "users_v12") + }) + }) + }) Convey("Given a resourceInfo configured with a valid resource long path that is versioned such as '/v1/something/users'", t, func() { r := resourceInfo{ path: "/v1/something/users", From b544574156043deb4571b27f2c270c54346a2d7c Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 14 Sep 2018 15:11:48 -0700 Subject: [PATCH 49/60] make makefile fail on error for fmt, vet, lint and test --- Makefile | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 34c84ab63..570355ca1 100644 --- a/Makefile +++ b/Makefile @@ -26,10 +26,7 @@ fmt: # make vet vet: @echo "[INFO] Running go vet on the current directory" - @go vet $(TEST_PACKAGES) ; if [ $$? -eq 1 ]; then \ - echo "[ERROR] Vet found suspicious constructs. Please fix the reported constructs before submitting code for review"; \ - exit 1; \ - fi + @go vet $(TEST_PACKAGES) # make lint lint: @@ -40,7 +37,7 @@ lint: # make test test: fmt vet lint @echo "[INFO] Testing $(TF_OPENAPI_PROVIDER_PLUGIN_NAME)" - @go test -v -cover $(TEST_PACKAGES) ; if [ $$? -eq 1 ]; then \ + @go test -v -cover $(TEST_PACKAGES) ; if [ $$? -ne 1 ]; then \ echo "[ERROR] Test returned with failures. Please go through the different scenarios and fix the tests that are failing"; \ exit 1; \ fi From 96d50e66c0e25e07a88ee5f1f25611e4e5ca8229 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 14 Sep 2018 15:12:06 -0700 Subject: [PATCH 50/60] fix merging compile errors --- openapi/api_spec_analyser.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openapi/api_spec_analyser.go b/openapi/api_spec_analyser.go index cb02cdd3c..59941fb67 100644 --- a/openapi/api_spec_analyser.go +++ b/openapi/api_spec_analyser.go @@ -55,15 +55,13 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) { continue } - isMultiRegion, regions := r.isMultiRegionResource(asa.d.Spec().Extensions) if isMultiRegion { - log.Printf("[INFO] resource '%s' is configured with host override AND multi region; creating reasource per region", r.name) + log.Printf("[INFO] resource '%s' is configured with host override AND multi region; creating reasource per region", r.path) for regionName, regionHost := range regions { resourceRegionName := fmt.Sprintf("%s_%s", resourceName, regionName) regionResource := resourceInfo{} regionResource = r - regionResource.name = resourceRegionName regionResource.host = regionHost log.Printf("[INFO] multi region resource: name = %s, region = %s, host = %s", regionName, resourceRegionName, regionHost) resources[resourceRegionName] = regionResource @@ -78,7 +76,7 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) { } // Fall back to override the host if value is not empty; otherwise global host will be used as usual if hostOverride != "" { - log.Printf("[INFO] resource '%s' is configured with host override, API calls will be made against '%s' instead of '%s'", r.name, hostOverride, asa.d.Spec().Host) + log.Printf("[INFO] resource '%s' is configured with host override, API calls will be made against '%s' instead of '%s'", r.path, hostOverride, asa.d.Spec().Host) r.host = hostOverride } resources[resourceName] = r From 54370ed8326f02b6ca8dfedfd69319b5f06a31d6 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 14 Sep 2018 15:13:08 -0700 Subject: [PATCH 51/60] remove not needed if statement --- Makefile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 570355ca1..6cf36b183 100644 --- a/Makefile +++ b/Makefile @@ -37,10 +37,7 @@ lint: # make test test: fmt vet lint @echo "[INFO] Testing $(TF_OPENAPI_PROVIDER_PLUGIN_NAME)" - @go test -v -cover $(TEST_PACKAGES) ; if [ $$? -ne 1 ]; then \ - echo "[ERROR] Test returned with failures. Please go through the different scenarios and fix the tests that are failing"; \ - exit 1; \ - fi + @go test -v -cover $(TEST_PACKAGES) pre-requirements: @echo "[INFO] Creating $(TF_INSTALLED_PLUGINS_PATH) if it does not exist" From bdfbdbbe74634b96dbc8997966b7d96ee974e2c2 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 14 Sep 2018 15:19:00 -0700 Subject: [PATCH 52/60] pump up feature version --- scripts/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install.sh b/scripts/install.sh index b9c8b8919..fd8b3f62e 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -58,7 +58,7 @@ fi ARCH=$(uname) # installation variables -LATEST_RELEASE_VERSION=0.2.0 +LATEST_RELEASE_VERSION=0.3.0 TF_PROVIDER_BASE_NAME="terraform-provider-" TF_OPENAPI_PROVIDER_PLUGIN_NAME="${TF_PROVIDER_BASE_NAME}openapi" From 4169a9e7a33b22bbe89a515ded4991dae3d00a84 Mon Sep 17 00:00:00 2001 From: "Daniel I. Khan Ramiro" Date: Fri, 14 Sep 2018 15:30:48 -0700 Subject: [PATCH 53/60] Update how_to.md --- docs/how_to.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how_to.md b/docs/how_to.md index 99679808b..feec45141 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -495,7 +495,7 @@ the overridden host instead, in this case ```cdn.api.otherdomain.com```. *Note: This extension is only supported at the operation's POST operation level. The other operations available for the resource such as GET/PUT/DELETE will used the overridden host value too.* -####### Multi-region resources +###### Multi-region resources Additionally, if the resource is using multi region domains, meaning there's one sub-domain for each region where the resource can be created into (similar to how aws resources are created per region), this can be configured as follows: From af048bc08ec5a51182371d970a17439fa994c2a3 Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 21 Sep 2018 16:07:34 -0700 Subject: [PATCH 54/60] update example to include object type support --- .../swaggercodegen/api/api/content_delivery_network.go | 2 ++ examples/swaggercodegen/api/resources/swagger.yaml | 7 +++++++ examples/swaggercodegen/main.tf | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/examples/swaggercodegen/api/api/content_delivery_network.go b/examples/swaggercodegen/api/api/content_delivery_network.go index 2cbd3b95a..aa7d95bb9 100644 --- a/examples/swaggercodegen/api/api/content_delivery_network.go +++ b/examples/swaggercodegen/api/api/content_delivery_network.go @@ -25,4 +25,6 @@ type ContentDeliveryNetwork struct { ExampleNumber float32 `json:"exampleNumber,omitempty"` ExampleBoolean bool `json:"example_boolean,omitempty"` + + ObjectProperty *ObjectProperty `json:"object_property,omitempty"` } diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 2b40d1415..4c23862f5 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -291,6 +291,13 @@ definitions: x-terraform-field-name: betterExampleNumberFieldName # overriding exampleNumber with a different name 'betterExampleNumberFieldName'; the preferred name is not terraform compliant either so the provider will perform the name conversion automatically when translating the name into the provider resource configuration and when saving the field into the state file example_boolean: type: boolean + object_property: + $ref: "#/definitions/ObjectProperty" + ObjectProperty: + type: object + properties: + message: + type: string LBV1: type: "object" required: diff --git a/examples/swaggercodegen/main.tf b/examples/swaggercodegen/main.tf index addfd7b93..40b71a4ac 100644 --- a/examples/swaggercodegen/main.tf +++ b/examples/swaggercodegen/main.tf @@ -13,6 +13,11 @@ resource "swaggercodegen_cdn_v1" "my_cdn" { example_int = 12 better_example_number_field_name = 1.12 example_boolean = true + + object_property = { + message = "" + } + } resource "swaggercodegen_lbs_v1" "my_lb" { From 8b7c079d4e8f02033fcd4eae1a6ec6e4a5c151cb Mon Sep 17 00:00:00 2001 From: dikhan Date: Fri, 21 Sep 2018 16:08:31 -0700 Subject: [PATCH 55/60] add object_property model --- .../swaggercodegen/api/api/object_property.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 examples/swaggercodegen/api/api/object_property.go diff --git a/examples/swaggercodegen/api/api/object_property.go b/examples/swaggercodegen/api/api/object_property.go new file mode 100644 index 000000000..007246143 --- /dev/null +++ b/examples/swaggercodegen/api/api/object_property.go @@ -0,0 +1,16 @@ +/* + * Dummy Service Provider generated using 'swaggercodegen' that has two resources 'cdns' and 'lbs' which are terraform compliant + * + * This service provider allows the creation of fake 'cdns' and 'lbs' resources + * + * API version: 1.0.0 + * Contact: apiteam@serviceprovider.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ + +package api + +type ObjectProperty struct { + + Message string `json:"message,omitempty"` +} From 44ec8951686e532b3dbef7e78b868334494a9522 Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 25 Sep 2018 15:51:00 -0700 Subject: [PATCH 56/60] update examples with various forms of object properties - readoonly object property with nested schema - required object property with ref to a schema - object support for statues --- examples/swaggercodegen/api/api/cdn.go | 24 ++++++++++++------ .../api/api/content_delivery_network.go | 8 +++--- ...etwork_v1_object_nested_scheme_property.go | 16 ++++++++++++ examples/swaggercodegen/api/api/lb_api.go | 10 +++++--- examples/swaggercodegen/api/api/lbv1.go | 2 ++ examples/swaggercodegen/api/api/status.go | 18 +++++++++++++ .../swaggercodegen/api/resources/swagger.yaml | 25 ++++++++++++++++--- examples/swaggercodegen/main.tf | 2 +- 8 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 examples/swaggercodegen/api/api/content_delivery_network_v1_object_nested_scheme_property.go create mode 100644 examples/swaggercodegen/api/api/status.go diff --git a/examples/swaggercodegen/api/api/cdn.go b/examples/swaggercodegen/api/api/cdn.go index e2beafad1..a4cd5c69b 100644 --- a/examples/swaggercodegen/api/api/cdn.go +++ b/examples/swaggercodegen/api/api/cdn.go @@ -9,7 +9,7 @@ import ( "log" ) -var db = map[string]*ContentDeliveryNetwork{} +var db = map[string]*ContentDeliveryNetworkV1{} func ContentDeliveryNetworkCreateV1(w http.ResponseWriter, r *http.Request) { if AuthenticateRequest(r, w) != nil { @@ -17,13 +17,16 @@ func ContentDeliveryNetworkCreateV1(w http.ResponseWriter, r *http.Request) { } xRequestID := r.Header.Get("X-Request-ID") log.Printf("Header [X-Request-ID]: %s", xRequestID) - cdn := &ContentDeliveryNetwork{} + cdn := &ContentDeliveryNetworkV1{} err := readRequest(r, cdn) if err != nil { sendErrorResponse(http.StatusBadRequest, err.Error(), w) return } cdn.Id = uuid.New() + cdn.ObjectNestedSchemeProperty = &ContentDeliveryNetworkV1ObjectNestedSchemeProperty{ + Name: "autogenerated name", + } db[cdn.Id] = cdn sendResponse(http.StatusCreated, w, cdn) } @@ -49,15 +52,22 @@ func ContentDeliveryNetworkUpdateV1(w http.ResponseWriter, r *http.Request) { sendErrorResponse(http.StatusNotFound, err.Error(), w) return } - newCDN := &ContentDeliveryNetwork{} + newCDN := &ContentDeliveryNetworkV1{} err = readRequest(r, newCDN) if err != nil { sendErrorResponse(http.StatusBadRequest, err.Error(), w) return } - newCDN.Id = cdn.Id - db[cdn.Id] = newCDN - sendResponse(http.StatusOK, w, newCDN) + + cdn.Ips = newCDN.Ips + cdn.Hostnames = newCDN.Hostnames + cdn.ExampleInt = newCDN.ExampleInt + cdn.ExampleNumber = newCDN.ExampleNumber + cdn.ExampleBoolean = newCDN.ExampleBoolean + cdn.ObjectProperty = newCDN.ObjectProperty + + db[cdn.Id] = cdn + sendResponse(http.StatusOK, w, cdn) } func ContentDeliveryNetworkDeleteV1(w http.ResponseWriter, r *http.Request) { @@ -73,7 +83,7 @@ func ContentDeliveryNetworkDeleteV1(w http.ResponseWriter, r *http.Request) { sendResponse(http.StatusNoContent, w, nil) } -func retrieveCdn(r *http.Request) (*ContentDeliveryNetwork, error) { +func retrieveCdn(r *http.Request) (*ContentDeliveryNetworkV1, error) { id := strings.TrimPrefix(r.URL.Path, "/v1/cdns/") if id == "" { return nil, fmt.Errorf("cdn id path param not provided") diff --git a/examples/swaggercodegen/api/api/content_delivery_network.go b/examples/swaggercodegen/api/api/content_delivery_network.go index aa7d95bb9..1df1c7d62 100644 --- a/examples/swaggercodegen/api/api/content_delivery_network.go +++ b/examples/swaggercodegen/api/api/content_delivery_network.go @@ -10,7 +10,7 @@ package api -type ContentDeliveryNetwork struct { +type ContentDeliveryNetworkV1 struct { Id string `json:"id,omitempty"` @@ -26,5 +26,7 @@ type ContentDeliveryNetwork struct { ExampleBoolean bool `json:"example_boolean,omitempty"` - ObjectProperty *ObjectProperty `json:"object_property,omitempty"` -} + ObjectProperty *ObjectProperty `json:"object_property"` + + ObjectNestedSchemeProperty *ContentDeliveryNetworkV1ObjectNestedSchemeProperty `json:"object_nested_scheme_property,omitempty"` +} \ No newline at end of file diff --git a/examples/swaggercodegen/api/api/content_delivery_network_v1_object_nested_scheme_property.go b/examples/swaggercodegen/api/api/content_delivery_network_v1_object_nested_scheme_property.go new file mode 100644 index 000000000..a64dbb29d --- /dev/null +++ b/examples/swaggercodegen/api/api/content_delivery_network_v1_object_nested_scheme_property.go @@ -0,0 +1,16 @@ +/* + * Dummy Service Provider generated using 'swaggercodegen' that has two resources 'cdns' and 'lbs' which are terraform compliant + * + * This service provider allows the creation of fake 'cdns' and 'lbs' resources + * + * API version: 1.0.0 + * Contact: apiteam@serviceprovider.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ + +package api + +type ContentDeliveryNetworkV1ObjectNestedSchemeProperty struct { + + Name string `json:"name"` +} diff --git a/examples/swaggercodegen/api/api/lb_api.go b/examples/swaggercodegen/api/api/lb_api.go index 732ad835b..a58e5c4a8 100644 --- a/examples/swaggercodegen/api/api/lb_api.go +++ b/examples/swaggercodegen/api/api/lb_api.go @@ -47,7 +47,9 @@ func LBGetV1(w http.ResponseWriter, r *http.Request) { } func LBCreateV1(w http.ResponseWriter, r *http.Request) { - lb := &Lbv1{} + lb := &Lbv1{ + NewStatus: &Status{}, + } err := readRequest(r, lb) if err != nil { sendErrorResponse(http.StatusBadRequest, err.Error(), w) @@ -146,9 +148,9 @@ func sleepAndDestroyLB(lb *Lbv1, waitTime int32) { } func updateLBStatus(lb *Lbv1, newStatus status) { - oldStatus := lb.Status - lb.Status = string(newStatus) - log.Printf("LB [%s] status updated '%s' => '%s'", lb.Id, oldStatus, newStatus) + oldStatus := lb.NewStatus.Status + lb.NewStatus.Status = string(newStatus) + log.Printf("LB [%s] status updated '%s' => '%s'", lb.Id, oldStatus, lb.NewStatus.Status) } func retrieveLB(r *http.Request) (*Lbv1, error) { diff --git a/examples/swaggercodegen/api/api/lbv1.go b/examples/swaggercodegen/api/api/lbv1.go index 3f3c0747c..3b35a9209 100644 --- a/examples/swaggercodegen/api/api/lbv1.go +++ b/examples/swaggercodegen/api/api/lbv1.go @@ -24,4 +24,6 @@ type Lbv1 struct { // lb resource status Status string `json:"status,omitempty"` + + NewStatus *Status `json:"newStatus,omitempty"` } diff --git a/examples/swaggercodegen/api/api/status.go b/examples/swaggercodegen/api/api/status.go new file mode 100644 index 000000000..1599d039a --- /dev/null +++ b/examples/swaggercodegen/api/api/status.go @@ -0,0 +1,18 @@ +/* + * Dummy Service Provider generated using 'swaggercodegen' that has two resources 'cdns' and 'lbs' which are terraform compliant + * + * This service provider allows the creation of fake 'cdns' and 'lbs' resources + * + * API version: 1.0.0 + * Contact: apiteam@serviceprovider.io + * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) + */ + +package api + +type Status struct { + + Message string `json:"message,omitempty"` + + Status string `json:"status,omitempty"` +} diff --git a/examples/swaggercodegen/api/resources/swagger.yaml b/examples/swaggercodegen/api/resources/swagger.yaml index 4c23862f5..1b94c4a40 100644 --- a/examples/swaggercodegen/api/resources/swagger.yaml +++ b/examples/swaggercodegen/api/resources/swagger.yaml @@ -267,6 +267,7 @@ definitions: - label - ips - hostnames + - object_property properties: id: type: "string" @@ -292,9 +293,18 @@ definitions: example_boolean: type: boolean object_property: + #type: object - type is optional for properties of object type that use $ref $ref: "#/definitions/ObjectProperty" + object_nested_scheme_property: + type: object # nested properties required type equal object to be considered as object + readOnly: true + properties: + name: + type: string ObjectProperty: type: object + required: + - message properties: message: type: string @@ -314,8 +324,7 @@ definitions: type: "array" items: type: "string" - status: - x-terraform-field-status: true # identifies the field that should be used as status for async operations. This is handy when the field name is not status but some other name the service provider might have chosen and enables the provider to identify the field as the status field that will be used to track progress for the async operations + status: # no longer used, using the object status instead for the sake of testing description: lb resource status type: string readOnly: true @@ -328,12 +337,22 @@ definitions: - delete_in_progress - delete_failed - deleted + newStatus: + $ref: "#/definitions/Status" + x-terraform-field-status: true # identifies the field that should be used as status for async operations. This is handy when the field name is not status but some other name the service provider might have chosen and enables the provider to identify the field as the status field that will be used to track progress for the async operations + readOnly: true timeToProcess: # time that the resource will take to be processed in seconds type: integer default: 60 # it will take two minute to process the resource operation (POST/PUT/READ/DELETE) simulate_failure: # allows user to set it to true and force an error on the API when the given operation (POST/PUT/READ/DELETE) is being performed type: boolean - + Status: + type: object + properties: + message: + type: string + status: + type: string # Schema for error response body Error: type: object diff --git a/examples/swaggercodegen/main.tf b/examples/swaggercodegen/main.tf index 40b71a4ac..3b01410ac 100644 --- a/examples/swaggercodegen/main.tf +++ b/examples/swaggercodegen/main.tf @@ -15,7 +15,7 @@ resource "swaggercodegen_cdn_v1" "my_cdn" { example_boolean = true object_property = { - message = "" + message = "some message news" } } From e194ed128fc75810638a54816ee1c141130b954e Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 25 Sep 2018 15:52:53 -0700 Subject: [PATCH 57/60] add helper method GetSchemaDefinition to assist with ref look ups --- openapi/openapiutils/openapi_utils.go | 29 +++++ openapi/openapiutils/openapi_utils_test.go | 123 +++++++++++++++++++++ openapi/terraformutils/terraform_utils.go | 1 - 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/openapi/openapiutils/openapi_utils.go b/openapi/openapiutils/openapi_utils.go index c48c4fd62..a15871770 100644 --- a/openapi/openapiutils/openapi_utils.go +++ b/openapi/openapiutils/openapi_utils.go @@ -1,12 +1,14 @@ package openapiutils import ( + "fmt" "github.com/dikhan/terraform-provider-openapi/openapi/terraformutils" "github.com/go-openapi/spec" "regexp" "strings" ) +const swaggerResourcePayloadDefinitionRegex = "(\\w+)[^//]*$" const fqdnInURLRegex = `\b(?:(?:[^.-/]{0,1})[\w-]{1,63}[-]{0,1}[.]{1})+(?:[a-zA-Z]{2,63})?|localhost(?:[:]\d+)?\b` const extTfHeader = "x-terraform-header" @@ -113,3 +115,30 @@ func StringExtensionExists(extensions spec.Extensions, key string) (string, bool } return value, exists } + +// getPayloadDefName only supports references to the same document. External references like URLs is not supported at the moment +func getPayloadDefName(ref string) (string, error) { + reg, err := regexp.Compile(swaggerResourcePayloadDefinitionRegex) + if err != nil { + return "", fmt.Errorf("an error occurred while compiling the swaggerResourcePayloadDefinitionRegex regex '%s': %s", swaggerResourcePayloadDefinitionRegex, err) + } + payloadDefName := reg.FindStringSubmatch(ref)[0] + if payloadDefName == "" { + return "", fmt.Errorf("could not find a valid definition name for '%s'", ref) + } + return payloadDefName, nil +} + +// GetSchemaDefinition queries the definitions and tries to find the schema definition for the given ref. If the schema +// definition the ref value is pointing at does not exist and error is returned. Otherwise, the corresponding schema definition is returned. +func GetSchemaDefinition(definitions map[string]spec.Schema, ref string) (*spec.Schema, error) { + payloadDefName, err := getPayloadDefName(ref) + if err != nil { + return nil, err + } + payloadDefinition, exists := definitions[payloadDefName] + if !exists { + return nil, fmt.Errorf("missing schema definition in the swagger file with the supplied ref '%s'", ref) + } + return &payloadDefinition, nil +} diff --git a/openapi/openapiutils/openapi_utils_test.go b/openapi/openapiutils/openapi_utils_test.go index be5c594dc..09e51dc91 100644 --- a/openapi/openapiutils/openapi_utils_test.go +++ b/openapi/openapiutils/openapi_utils_test.go @@ -1,7 +1,9 @@ package openapiutils import ( + "encoding/json" "fmt" + "github.com/go-openapi/loads" "github.com/go-openapi/spec" . "github.com/smartystreets/goconvey/convey" "testing" @@ -380,3 +382,124 @@ func TestStringExtensionExists(t *testing.T) { }) }) } + +func TestGetPayloadDefName(t *testing.T) { + Convey("Given a valid internal definition path", t, func() { + ref := "#/definitions/ContentDeliveryNetworkV1" + // Local Reference use cases + Convey("When getPayloadDefName method is called with a valid internal definition path", func() { + defName, err := getPayloadDefName(ref) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the value returned should be true", func() { + So(defName, ShouldEqual, "ContentDeliveryNetworkV1") + }) + }) + }) + + Convey("Given a ref URL (not supported)", t, func() { + ref := "http://path/to/your/resource.json#myElement" + Convey("When getPayloadDefName method is called ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) + + Convey("Given an element of the document located on the same server (not supported)", t, func() { + ref := "document.json#/myElement" + // Remote Reference use cases + Convey("When getPayloadDefName method is called with ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) + + Convey("Given an element of the document located in the parent folder (not supported)", t, func() { + ref := "../document.json#/myElement" + Convey("When getPayloadDefName method is called with ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) + + Convey("Given an specific element of the document stored on the different server (not supported)", t, func() { + ref := "http://path/to/your/resource.json#myElement" + Convey("When getPayloadDefName method is called with ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) + + Convey("Given an element of the document located in another folder (not supported)", t, func() { + ref := "../another-folder/document.json#/myElement" + // URL Reference use case + Convey("When getPayloadDefName method is called with ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) + + Convey("Given a document on the different server, which uses the same protocol (not supported)", t, func() { + ref := "//anotherserver.com/files/example.json" + Convey("When getPayloadDefName method is called with ", func() { + _, err := getPayloadDefName(ref) + Convey("Then the error returned should not be nil", func() { + So(err, ShouldBeNil) + }) + }) + }) +} + +func TestGetResourcePayloadSchemaDef(t *testing.T) { + Convey("Given a swagger doc", t, func() { + swaggerContent := `swagger: "2.0" +definitions: + Users: + type: "object" + required: + - name + properties: + id: + type: "string" + readOnly: true` + spec := initSwagger(swaggerContent) + Convey("When getResourcePayloadSchemaDef method is called with an operation containing a valid ref: '#/definitions/Users'", func() { + ref := "#/definitions/Users" + resourcePayloadSchemaDef, err := GetSchemaDefinition(spec.Definitions, ref) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should be a valid schema def", func() { + So(len(resourcePayloadSchemaDef.Type), ShouldEqual, 1) + So(resourcePayloadSchemaDef.Type, ShouldContain, "object") + }) + }) + Convey("When getResourcePayloadSchemaDef method is called with schema that is missing the definition the ref is pointing at", func() { + ref := "#/definitions/NonExistingDef" + _, err := GetSchemaDefinition(spec.Definitions, ref) + Convey("Then the error returned should NOT be nil", func() { + So(err, ShouldNotBeNil) + }) + Convey("And the error message should be", func() { + So(err.Error(), ShouldContainSubstring, "missing schema definition in the swagger file with the supplied ref '#/definitions/NonExistingDef'") + }) + }) + }) +} + +func initSwagger(swaggerContent string) *spec.Swagger { + swagger := json.RawMessage([]byte(swaggerContent)) + d, _ := loads.Analyzed(swagger, "2.0") + return d.Spec() +} diff --git a/openapi/terraformutils/terraform_utils.go b/openapi/terraformutils/terraform_utils.go index df9ee28da..239ec2f3f 100644 --- a/openapi/terraformutils/terraform_utils.go +++ b/openapi/terraformutils/terraform_utils.go @@ -37,6 +37,5 @@ func (t *TerraformUtils) GetTerraformPluginsVendorDir() (string, error) { // Terraform's snake case field name convention (lower case and snake case). func ConvertToTerraformCompliantName(name string) string { compliantName := strcase.ToSnake(name) - log.Printf("[DEBUG] ConvertToTerraformCompliantName - originalName = %s; compliantName = %s)", name, compliantName) return compliantName } From 82551e2c4a001dc29cc3d83101494078d29e541c Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 25 Sep 2018 15:54:35 -0700 Subject: [PATCH 58/60] pass asa.d.Spec().Definitions to resourceInfo. This is neeeded so resource info is able to look up ref by object type properties inside the resource schemaDefinition - move getPayloadDefName to utils --- openapi/api_spec_analyser.go | 40 +++++--------------- openapi/api_spec_analyser_test.go | 61 ------------------------------- 2 files changed, 10 insertions(+), 91 deletions(-) diff --git a/openapi/api_spec_analyser.go b/openapi/api_spec_analyser.go index 59941fb67..7b5607355 100644 --- a/openapi/api_spec_analyser.go +++ b/openapi/api_spec_analyser.go @@ -2,6 +2,7 @@ package openapi import ( "fmt" + "github.com/dikhan/terraform-provider-openapi/openapi/openapiutils" "log" "regexp" "strings" @@ -11,7 +12,6 @@ import ( ) const resourceInstanceRegex = "((?:.*)){.*}" -const swaggerResourcePayloadDefinitionRegex = "(\\w+)[^//]*$" // apiSpecAnalyser analyses the swagger doc and provides helper methods to retrieve all the end points that can // be used as terraform resources. These endpoints have to meet certain criteria to be considered eligible resources @@ -36,13 +36,14 @@ func (asa apiSpecAnalyser) getResourcesInfo() (resourcesInfo, error) { } r := resourceInfo{ - basePath: asa.d.BasePath(), - path: resourceRootPath, - host: asa.d.Spec().Host, - httpSchemes: asa.d.Spec().Schemes, - schemaDefinition: *resourcePayloadSchemaDef, - createPathInfo: *resourceRoot, - pathInfo: pathItem, + basePath: asa.d.BasePath(), + path: resourceRootPath, + host: asa.d.Spec().Host, + httpSchemes: asa.d.Spec().Schemes, + schemaDefinition: resourcePayloadSchemaDef, + createPathInfo: *resourceRoot, + pathInfo: pathItem, + schemaDefinitions: asa.d.Spec().Definitions, } if r.shouldIgnoreResource() { @@ -220,15 +221,7 @@ func (asa apiSpecAnalyser) getResourcePayloadSchemaDef(resourceRootPostOperation if err != nil { return nil, err } - payloadDefName, err := asa.getPayloadDefName(ref) - if err != nil { - return nil, err - } - payloadDefinition, exists := asa.d.Spec().Definitions[payloadDefName] - if !exists { - return nil, fmt.Errorf("missing schema definition in the swagger file with the supplied ref '%s'", ref) - } - return &payloadDefinition, nil + return openapiutils.GetSchemaDefinition(asa.d.Spec().Definitions, ref) } func (asa apiSpecAnalyser) getResourcePayloadSchemaRef(resourceRootPostOperation *spec.Operation) (string, error) { @@ -261,19 +254,6 @@ func (asa apiSpecAnalyser) getResourcePayloadSchemaRef(resourceRootPostOperation return payloadDefinitionSchemaRef.Ref.String(), nil } -// getPayloadDefName only supports references to the same document. External references like URLs is not supported at the moment -func (asa apiSpecAnalyser) getPayloadDefName(ref string) (string, error) { - reg, err := regexp.Compile(swaggerResourcePayloadDefinitionRegex) - if err != nil { - return "", fmt.Errorf("an error occurred while compiling the swaggerResourcePayloadDefinitionRegex regex '%s': %s", swaggerResourcePayloadDefinitionRegex, err) - } - payloadDefName := reg.FindStringSubmatch(ref)[0] - if payloadDefName == "" { - return "", fmt.Errorf("could not find a valid definition name for '%s'", ref) - } - return payloadDefName, nil -} - // resourceInstanceRegex loads up the regex specified in const resourceInstanceRegex // If the regex is not able to compile the regular expression the function exists calling os.Exit(1) as // there is the regex is completely busted diff --git a/openapi/api_spec_analyser_test.go b/openapi/api_spec_analyser_test.go index c80df3d52..066007625 100644 --- a/openapi/api_spec_analyser_test.go +++ b/openapi/api_spec_analyser_test.go @@ -66,67 +66,6 @@ func TestResourceInstanceEndPoint(t *testing.T) { }) } -func TestGetPayloadDefName(t *testing.T) { - Convey("Given an apiSpecAnalyser", t, func() { - a := apiSpecAnalyser{} - - // Local Reference use cases - Convey("When getPayloadDefName method is called with a valid internal definition path", func() { - defName, err := a.getPayloadDefName("#/definitions/ContentDeliveryNetworkV1") - Convey("Then the error returned should be nil", func() { - So(err, ShouldBeNil) - }) - Convey("And the value returned should be true", func() { - So(defName, ShouldEqual, "ContentDeliveryNetworkV1") - }) - }) - - Convey("When getPayloadDefName method is called with a URL (not supported)", func() { - _, err := a.getPayloadDefName("http://path/to/your/resource.json#myElement") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - - // Remote Reference use cases - Convey("When getPayloadDefName method is called with an element of the document located on the same server (not supported)", func() { - _, err := a.getPayloadDefName("document.json#/myElement") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - - Convey("When getPayloadDefName method is called with an element of the document located in the parent folder (not supported)", func() { - _, err := a.getPayloadDefName("../document.json#/myElement") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - - Convey("When getPayloadDefName method is called with an specific element of the document stored on the different server (not supported)", func() { - _, err := a.getPayloadDefName("http://path/to/your/resource.json#myElement") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - - // URL Reference use case - Convey("When getPayloadDefName method is called with an element of the document located in another folder (not supported)", func() { - _, err := a.getPayloadDefName("../another-folder/document.json#/myElement") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - - Convey("When getPayloadDefName method is called with document on the different server, which uses the same protocol (not supported)", func() { - _, err := a.getPayloadDefName("//anotherserver.com/files/example.json") - Convey("Then the error returned should not be nil", func() { - So(err, ShouldBeNil) - }) - }) - }) -} - func TestGetResourcePayloadSchemaRef(t *testing.T) { Convey("Given an apiSpecAnalyser", t, func() { a := apiSpecAnalyser{} From ac22a09a6bb2154986e0c0b285b5cea90377aacf Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 25 Sep 2018 15:58:25 -0700 Subject: [PATCH 59/60] add support for object types - support for objects when building terraform resource schema - support for objects when building payload data from resource Data - support for objects in status fields - add units tests --- openapi/resource_factory.go | 14 +- openapi/resource_info.go | 153 ++++++-- openapi/resource_info_test.go | 679 +++++++++++++++++++++++++++------- 3 files changed, 672 insertions(+), 174 deletions(-) diff --git a/openapi/resource_factory.go b/openapi/resource_factory.go index da8b00167..d3f9b4772 100644 --- a/openapi/resource_factory.go +++ b/openapi/resource_factory.go @@ -290,7 +290,6 @@ func (r resourceFactory) handlePollingIfConfigured(responsePayload *map[string]i func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.ResourceData, providerConfig providerConfig) resource.StateRefreshFunc { return func() (interface{}, string, error) { remoteData, err := r.readRemote(resourceLocalData.Id(), providerConfig) - if err != nil { if openapiErr, ok := err.(openapierr.Error); ok { if openapierr.NotFound == openapiErr.Code() { @@ -299,17 +298,10 @@ func (r resourceFactory) resourceStateRefreshFunc(resourceLocalData *schema.Reso } return nil, "", fmt.Errorf("error on retrieving resource '%s' (%s) when waiting: %s", r.resourceInfo.path, resourceLocalData.Id(), err) } - - statusIdentifier, err := r.resourceInfo.getStatusIdentifier() + newStatus, err := r.resourceInfo.getStatusValueFromPayload(remoteData) if err != nil { - return nil, "", fmt.Errorf("error occurred while retrieving status identifier for resource '%s' (%s): %s", r.resourceInfo.path, resourceLocalData.Id(), err) - } - - value, statusIdentifierPresentInResponse := remoteData[statusIdentifier] - if !statusIdentifierPresentInResponse { - return nil, "", fmt.Errorf("response payload received from GET /%s/%s missing the status identifier field", r.resourceInfo.path, resourceLocalData.Id()) + return nil, "", fmt.Errorf("failed to get the status value after receiving response from GET /%s/%s: %s", r.resourceInfo.path, resourceLocalData.Id(), err) } - newStatus := value.(string) log.Printf("[DEBUG] resource '%s' status (%s): %s", r.resourceInfo.path, resourceLocalData.Id(), newStatus) return remoteData, newStatus, nil } @@ -428,6 +420,8 @@ func (r resourceFactory) createPayloadFromLocalStateData(resourceLocalData *sche } if dataValue, ok := r.getResourceDataOKExists(propertyName, resourceLocalData); ok { switch reflect.TypeOf(dataValue).Kind() { + case reflect.Map: + input[propertyName] = dataValue.(map[string]interface{}) case reflect.Slice: input[propertyName] = dataValue.([]interface{}) case reflect.String: diff --git a/openapi/resource_info.go b/openapi/resource_info.go index f13ab0e3e..ec51a305e 100644 --- a/openapi/resource_info.go +++ b/openapi/resource_info.go @@ -2,6 +2,7 @@ package openapi import ( "fmt" + "reflect" "log" "strings" @@ -47,20 +48,33 @@ type resourceInfo struct { path string host string httpSchemes []string - schemaDefinition spec.Schema + schemaDefinition *spec.Schema // createPathInfo contains info about /resource, including the POST operation createPathInfo spec.PathItem // pathInfo contains info about /resource/{id}, including GET, PUT and REMOVE operations if applicable pathInfo spec.PathItem + + // schemaDefinitions contains all the definitions which might be needed in case the resource schema contains properties + // of type object which in turn refer to other definitions + schemaDefinitions map[string]spec.Schema } func (r resourceInfo) createTerraformResourceSchema() (map[string]*schema.Schema, error) { + return r.terraformSchema(r.schemaDefinition, true) +} + +// terraformSchema returns the terraform schema for the given schema definition. if ignoreID is true then properties named +// id will be ignored, this is because terraform already has an ID field reserved that identifies uniquely the resource and +// root level schema can not contain a property named ID. For other levels, in case there are properties of type object +// id named properties is allowed as there won't be a conflict with terraform in that case. +func (r resourceInfo) terraformSchema(schemaDefinition *spec.Schema, ignoreID bool) (map[string]*schema.Schema, error) { s := map[string]*schema.Schema{} - for propertyName, property := range r.schemaDefinition.Properties { - if r.isIDProperty(propertyName) { + for propertyName, property := range schemaDefinition.Properties { + // ID should only be ignored when looping through the root level properties of the schema definition + if r.isIDProperty(propertyName) && ignoreID { continue } - tfSchema, err := r.createTerraformPropertySchema(propertyName, property) + tfSchema, err := r.createTerraformPropertyBasicSchema(propertyName, property, schemaDefinition.Required) if err != nil { return nil, err } @@ -76,18 +90,6 @@ func (r resourceInfo) convertToTerraformCompliantFieldName(propertyName string, return terraformutils.ConvertToTerraformCompliantName(propertyName) } -func (r resourceInfo) createTerraformPropertySchema(propertyName string, property spec.Schema) (*schema.Schema, error) { - propertySchema, err := r.createTerraformPropertyBasicSchema(propertyName, property) - if err != nil { - return nil, err - } - // ValidateFunc is not yet supported on lists or sets - if !r.isArrayProperty(property) { - propertySchema.ValidateFunc = r.validateFunc(propertyName, property) - } - return propertySchema, nil -} - func (r resourceInfo) validateFunc(propertyName string, property spec.Schema) schema.SchemaValidateFunc { return func(v interface{}, k string) (ws []string, errors []error) { if property.Default != nil { @@ -114,10 +116,23 @@ func (r resourceInfo) isRequired(propertyName string, requiredProps []string) bo return required } -func (r resourceInfo) createTerraformPropertyBasicSchema(propertyName string, property spec.Schema) (*schema.Schema, error) { +func (r resourceInfo) createTerraformPropertyBasicSchema(propertyName string, property spec.Schema, requiredProperties []string) (*schema.Schema, error) { var propertySchema *schema.Schema - // Arrays only support 'string' items at the moment - if r.isArrayProperty(property) { + if isObject, schemaDefinition, err := r.isObjectProperty(property); isObject { + if err != nil { + return nil, err + } + s, err := r.terraformSchema(schemaDefinition, false) + if err != nil { + return nil, err + } + propertySchema = &schema.Schema{ + Type: schema.TypeMap, + Elem: &schema.Resource{ + Schema: s, + }, + } + } else if r.isArrayProperty(property) { // Arrays only support 'string' items at the moment propertySchema = &schema.Schema{ Type: schema.TypeList, Elem: &schema.Schema{Type: schema.TypeString}, @@ -141,7 +156,7 @@ func (r resourceInfo) createTerraformPropertyBasicSchema(propertyName string, pr } // Set the property as required or optional - required := r.isRequired(propertyName, r.schemaDefinition.Required) + required := r.isRequired(propertyName, requiredProperties) if required { propertySchema.Required = true } else { @@ -175,11 +190,41 @@ func (r resourceInfo) createTerraformPropertyBasicSchema(propertyName string, pr propertySchema.Default = property.Default } } + + // ValidateFunc is not yet supported on lists or sets + if !r.isArrayProperty(property) { + propertySchema.ValidateFunc = r.validateFunc(propertyName, property) + } + return propertySchema, nil } +func (r resourceInfo) isObjectProperty(property spec.Schema) (bool, *spec.Schema, error) { + + if r.isObjectTypeProperty(property) && len(property.Properties) != 0 { + return true, &property, nil + } + + if property.Ref.Ref.GetURL() != nil { + schema, err := openapiutils.GetSchemaDefinition(r.schemaDefinitions, property.Ref.String()) + if err != nil { + return true, nil, err + } + return true, schema, nil + } + return false, nil, nil +} + func (r resourceInfo) isArrayProperty(property spec.Schema) bool { - return property.Type.Contains("array") + return r.isOfType(property, "array") +} + +func (r resourceInfo) isObjectTypeProperty(property spec.Schema) bool { + return r.isOfType(property, "object") +} + +func (r resourceInfo) isOfType(property spec.Schema, propertyType string) bool { + return property.Type.Contains(propertyType) } func (r resourceInfo) getImmutableProperties() []string { @@ -256,17 +301,25 @@ func (r resourceInfo) getResourceIdentifier() (string, error) { return identifierProperty, nil } -// getStatusIdentifier returns the property name that is supposed to be used as the status field. The status field -// is selected as follows: +// getStatusIdentifier loops through the schema definition given and tries to find the status id. This method supports both simple structures +// where the status field is at the schema definition root level or complex structures where status field is meant to be +// a sub-property of an object type property // 1.If the given schema definition contains a property configured with metadata 'x-terraform-field-status' set to true, that property // will be used to check the different statues for the asynchronous pooling mechanism. // 2. If none of the properties of the given schema definition contain such metadata, it is expected that the payload // will have a property named 'status' -// 3. If none of the above requirements is met, an error will be returned -func (r resourceInfo) getStatusIdentifier() (string, error) { - statusProperty := "" - for propertyName, property := range r.schemaDefinition.Properties { - if r.isIDProperty(propertyName) { +// 3. If the status field is NOT an object, then the array returned will contain one element with the property name that identifies +// the status field. +// 3. If the schema definition contains a deemed status field (as described above) and the property is of object type, the same logic +// as above will be applied to identify the status field to be used within the object property. In this case the result will +// be an array containing the property hierarchy, starting from the root and ending with the actual status field. This is needed +// so the correct status field can be extracted from payloads. +// 4. If none of the above requirements is met, an error will be returned +func (r resourceInfo) getStatusIdentifier(schemaDefinition *spec.Schema, shouldIgnoreID, shouldEnforceReadOnly bool) ([]string, error) { + var statusProperty string + var statusHierarchy []string + for propertyName, property := range schemaDefinition.Properties { + if r.isIDProperty(propertyName) && shouldIgnoreID { continue } if r.isStatusProperty(propertyName) { @@ -283,12 +336,48 @@ func (r resourceInfo) getStatusIdentifier() (string, error) { // if the id field is missing and there isn't any properties set with extTfFieldStatus, there is not way for the resource // to be identified and therefore an error is returned if statusProperty == "" { - return "", fmt.Errorf("could not find any status property in the resource swagger definition. Please make sure the resource definition has either one property named 'status' or one property that contains %s metadata", extTfFieldStatus) + return nil, fmt.Errorf("could not find any status property in the resource swagger definition. Please make sure the resource definition has either one property named 'status' or one property that contains %s metadata", extTfFieldStatus) + } + if !schemaDefinition.Properties[statusProperty].ReadOnly && shouldEnforceReadOnly { + return nil, fmt.Errorf("schema definition status property '%s' must be readOnly", statusProperty) + } + + statusHierarchy = append(statusHierarchy, statusProperty) + if isObject, propertySchemaDefinition, err := r.isObjectProperty(schemaDefinition.Properties[statusProperty]); isObject { + if err != nil { + return nil, err + } + statusIdentifier, err := r.getStatusIdentifier(propertySchemaDefinition, false, false) + if err != nil { + return nil, err + } + statusHierarchy = append(statusHierarchy, statusIdentifier...) } - if !r.schemaDefinition.Properties[statusProperty].ReadOnly { - return "", fmt.Errorf("schema definition status property '%s' must be readOnly", statusProperty) + + return statusHierarchy, nil +} + +func (r resourceInfo) getStatusValueFromPayload(payload map[string]interface{}) (string, error) { + statuses, err := r.getStatusIdentifier(r.schemaDefinition, true, true) + if err != nil { + return "", err + } + var property = payload + for _, statusField := range statuses { + propertyValue, statusExistsInPayload := property[statusField] + if !statusExistsInPayload { + return "", fmt.Errorf("payload does not match resouce schema, could not find the status field: %s", statuses) + } + switch reflect.TypeOf(propertyValue).Kind() { + case reflect.Map: + property = propertyValue.(map[string]interface{}) + case reflect.String: + return propertyValue.(string), nil + default: + return "", fmt.Errorf("status property value '%s' does not have a supported type [string/map]", statuses) + } } - return statusProperty, nil + return "", fmt.Errorf("could not find status value [%s] in the payload provided", statuses) } // shouldIgnoreResource checks whether the POST operation for a given resource as the 'x-terraform-exclude-resource' extension diff --git a/openapi/resource_info_test.go b/openapi/resource_info_test.go index 3fec45a97..254216204 100644 --- a/openapi/resource_info_test.go +++ b/openapi/resource_info_test.go @@ -288,7 +288,7 @@ func TestGetImmutableProperties(t *testing.T) { extensions := spec.Extensions{} extensions.Add("x-terraform-immutable", true) r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "id": { @@ -314,7 +314,7 @@ func TestGetImmutableProperties(t *testing.T) { Convey("Given resource info is configured with schemaDefinition that DOES NOT contain immutable properties", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "id": { @@ -346,16 +346,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("string_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("string_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type string too", func() { So(err, ShouldBeNil) So(tfPropSchema.Type, ShouldEqual, schema.TypeString) @@ -371,16 +372,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "int_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("int_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("int_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type int too", func() { So(err, ShouldBeNil) So(tfPropSchema.Type, ShouldEqual, schema.TypeInt) @@ -396,16 +398,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "number_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("number_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("number_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type float too", func() { So(err, ShouldBeNil) So(tfPropSchema.Type, ShouldEqual, schema.TypeFloat) @@ -415,7 +418,7 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { Convey("Given a swagger schema definition that has a property of type 'boolean'", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "boolean_prop": { @@ -425,11 +428,12 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, }, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", r.schemaDefinition.Properties["boolean_prop"]) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", r.schemaDefinition.Properties["boolean_prop"], r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type int too", func() { So(err, ShouldBeNil) So(tfPropSchema.Type, ShouldEqual, schema.TypeBool) @@ -439,7 +443,7 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { Convey("Given a swagger schema definition that has a property of type 'array'", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "array_prop": { @@ -449,11 +453,12 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, }, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("array_prop", r.schemaDefinition.Properties["array_prop"]) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("array_prop", r.schemaDefinition.Properties["array_prop"], r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type array too", func() { So(err, ShouldBeNil) So(tfPropSchema.Type, ShouldEqual, schema.TypeList) @@ -465,9 +470,140 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }) }) + Convey("Given a swagger schema definition that has a property of type object and a ref pointing to the schema", t, func() { + expectedRef := "#/definitions/ObjectProperty" + ref := spec.MustCreateRef(expectedRef) + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "object_prop": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Ref: ref, + }, + }, + }, + }, + }, + schemaDefinitions: map[string]spec.Schema{ + "ObjectProperty": { + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "message": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + }, + } + Convey("When createTerraformResourceSchema method is called", func() { + tfPropSchema, err := r.createTerraformPropertyBasicSchema("object_prop", r.schemaDefinition.Properties["object_prop"], r.schemaDefinition.Required) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the tf resource schema returned should not be nil", func() { + So(tfPropSchema, ShouldNotBeNil) + }) + Convey("And the tf resource schema returned should contained the sub property - 'message'", func() { + So(tfPropSchema.Elem.(*schema.Resource).Schema, ShouldContainKey, "message") + }) + }) + }) + + Convey("Given a swagger schema definition that has a property of type object that has nested schema", t, func() { + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "object_prop": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "message": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + }, + }, + }, + schemaDefinitions: map[string]spec.Schema{}, + } + Convey("When createTerraformResourceSchema method is called", func() { + tfPropSchema, err := r.createTerraformPropertyBasicSchema("object_prop", r.schemaDefinition.Properties["object_prop"], r.schemaDefinition.Required) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the tf resource schema returned should not be nil", func() { + So(tfPropSchema, ShouldNotBeNil) + }) + Convey("And the tf resource schema returned should contained the sub property - 'message'", func() { + So(tfPropSchema.Elem.(*schema.Resource).Schema, ShouldContainKey, "message") + }) + }) + }) + + Convey("Given a swagger schema definition that has a property of type object that has nested schema and property named id", t, func() { + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "object_prop": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "message": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + }, + }, + }, + }, + }, + schemaDefinitions: map[string]spec.Schema{}, + } + Convey("When createTerraformResourceSchema method is called", func() { + tfPropSchema, err := r.createTerraformPropertyBasicSchema("object_prop", r.schemaDefinition.Properties["object_prop"], r.schemaDefinition.Required) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the tf resource schema returned should not be nil", func() { + So(tfPropSchema, ShouldNotBeNil) + }) + Convey("And the tf resource schema returned should contain the sub property - 'message'", func() { + So(tfPropSchema.Elem.(*schema.Resource).Schema, ShouldContainKey, "message") + }) + Convey("And the tf resource schema returned should contain the sub property - 'id' and should not be ignored", func() { + So(tfPropSchema.Elem.(*schema.Resource).Schema, ShouldContainKey, "id") + }) + }) + }) + Convey("Given a swagger schema definition that has a property 'string_prop' which is required", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": { @@ -482,7 +618,7 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("string_prop", r.schemaDefinition.Properties["string_prop"]) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("string_prop", r.schemaDefinition.Properties["string_prop"], r.schemaDefinition.Required) Convey("Then the returned value should be true", func() { So(err, ShouldBeNil) So(tfPropSchema.Required, ShouldBeTrue) @@ -500,16 +636,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "boolean_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be of type int too", func() { So(err, ShouldBeNil) So(tfPropSchema.ForceNew, ShouldBeTrue) @@ -526,16 +663,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { SwaggerSchemaProps: spec.SwaggerSchemaProps{ReadOnly: true}, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "boolean_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be configured as computed", func() { So(err, ShouldBeNil) So(tfPropSchema.Computed, ShouldBeTrue) @@ -554,16 +692,17 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "boolean_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be configured as forceNew and sensitive", func() { So(err, ShouldBeNil) So(tfPropSchema.ForceNew, ShouldBeTrue) @@ -582,22 +721,93 @@ func TestCreateTerraformPropertyBasicSchema(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "boolean_prop": propSchema, }, + Required: []string{}, }, }, } Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema) + tfPropSchema, err := r.createTerraformPropertyBasicSchema("boolean_prop", propSchema, r.schemaDefinition.Required) Convey("Then the resulted terraform property schema should be configured with the according default value, ", func() { So(err, ShouldBeNil) So(tfPropSchema.Default, ShouldEqual, expectedDefaultValue) }) }) }) + + Convey("Given a swagger schema definition that has a property 'string_prop' of type string, required, sensitive and has a default value 'defaultValue'", t, func() { + expectedDefaultValue := "defaultValue" + extensions := spec.Extensions{} + extensions.Add("x-terraform-sensitive", true) + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "string_prop": { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Default: expectedDefaultValue, + }, + }, + }, + Required: []string{"string_prop"}, // This array contains the list of properties that are required + }, + }, + } + Convey("When createTerraformPropertyBasicSchema method is called", func() { + tfPropSchema, err := r.createTerraformPropertyBasicSchema("string_prop", r.schemaDefinition.Properties["string_prop"], r.schemaDefinition.Required) + Convey("Then the returned tf tfPropSchema should be of type string", func() { + So(err, ShouldBeNil) + So(tfPropSchema.Type, ShouldEqual, schema.TypeString) + }) + Convey("And a validateFunc should be configured", func() { + So(tfPropSchema.ValidateFunc, ShouldNotBeNil) + }) + Convey("And be configured as required", func() { + So(tfPropSchema.Required, ShouldBeTrue) + }) + Convey("And be configured as sensitive", func() { + So(tfPropSchema.Sensitive, ShouldBeTrue) + }) + Convey("And the default value should match 'defaultValue'", func() { + So(tfPropSchema.Default, ShouldEqual, expectedDefaultValue) + }) + }) + }) + + Convey("Given a swagger schema definition that has a property 'array_prop' of type array", t, func() { + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "array_prop": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + }, + }, + }, + Required: []string{"array_prop"}, // This array contains the list of properties that are required + }, + }, + } + Convey("When createTerraformPropertyBasicSchema method is called", func() { + tfPropSchema, err := r.createTerraformPropertyBasicSchema("array_prop", r.schemaDefinition.Properties["array_prop"], r.schemaDefinition.Required) + Convey("Then the returned tf tfPropSchema should be of type array", func() { + So(err, ShouldBeNil) + So(tfPropSchema.Type, ShouldEqual, schema.TypeList) + }) + Convey("And there should not be any validation function attached to it", func() { + So(tfPropSchema.ValidateFunc, ShouldBeNil) + }) + }) + }) + } func TestIsArrayProperty(t *testing.T) { @@ -609,7 +819,7 @@ func TestIsArrayProperty(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "array_prop": propSchema, @@ -633,7 +843,7 @@ func TestIsArrayProperty(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": propSchema, @@ -659,7 +869,7 @@ func TestIsRequired(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": propSchema, @@ -684,7 +894,7 @@ func TestIsRequired(t *testing.T) { }, } r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": propSchema, @@ -701,81 +911,10 @@ func TestIsRequired(t *testing.T) { }) } -func TestCreateTerraformPropertySchema(t *testing.T) { - Convey("Given a swagger schema definition that has a property 'string_prop' of type string, required, sensitive and has a default value 'defaultValue'", t, func() { - expectedDefaultValue := "defaultValue" - extensions := spec.Extensions{} - extensions.Add("x-terraform-sensitive", true) - r := resourceInfo{ - schemaDefinition: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Properties: map[string]spec.Schema{ - "string_prop": { - VendorExtensible: spec.VendorExtensible{Extensions: extensions}, - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Default: expectedDefaultValue, - }, - }, - }, - Required: []string{"string_prop"}, // This array contains the list of properties that are required - }, - }, - } - Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertySchema("string_prop", r.schemaDefinition.Properties["string_prop"]) - Convey("Then the returned tf tfPropSchema should be of type string", func() { - So(err, ShouldBeNil) - So(tfPropSchema.Type, ShouldEqual, schema.TypeString) - }) - Convey("And a validateFunc should be configured", func() { - So(tfPropSchema.ValidateFunc, ShouldNotBeNil) - }) - Convey("And be configured as required, sensitive and the default value should match 'defaultValue'", func() { - So(tfPropSchema.Required, ShouldBeTrue) - }) - Convey("And be configured as sensitive", func() { - So(tfPropSchema.Sensitive, ShouldBeTrue) - }) - Convey("And the default value should match 'defaultValue'", func() { - So(tfPropSchema.Default, ShouldEqual, expectedDefaultValue) - }) - }) - }) - - Convey("Given a swagger schema definition that has a property 'array_prop' of type array", t, func() { - r := resourceInfo{ - schemaDefinition: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Properties: map[string]spec.Schema{ - "array_prop": { - VendorExtensible: spec.VendorExtensible{}, - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - }, - }, - }, - Required: []string{"array_prop"}, // This array contains the list of properties that are required - }, - }, - } - Convey("When createTerraformPropertyBasicSchema method is called", func() { - tfPropSchema, err := r.createTerraformPropertySchema("array_prop", r.schemaDefinition.Properties["array_prop"]) - Convey("Then the returned tf tfPropSchema should be of type array", func() { - So(err, ShouldBeNil) - So(tfPropSchema.Type, ShouldEqual, schema.TypeList) - }) - Convey("And there should not be any validation function attached to it", func() { - So(tfPropSchema.ValidateFunc, ShouldBeNil) - }) - }) - }) -} - func TestValidateFunc(t *testing.T) { Convey("Given a swagger schema definition that has one property", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "array_prop": { @@ -798,7 +937,7 @@ func TestValidateFunc(t *testing.T) { Convey("Given a swagger schema definition that has a property which is supposed to be computed but has a default value set", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "array_prop": { @@ -829,7 +968,7 @@ func TestValidateFunc(t *testing.T) { func TestCreateTerraformResourceSchema(t *testing.T) { Convey("Given a swagger schema definition that has multiple properties - 'string_prop', 'int_prop', 'number_prop', 'bool_prop' and 'array_prop'", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "string_prop": { @@ -885,13 +1024,79 @@ func TestCreateTerraformResourceSchema(t *testing.T) { }) }) }) + + Convey("Given a swagger schema definition that has object properties and a list of schema definitions containing the definition the object refs to", t, func() { + expectedRef := "#/definitions/ObjectProperty" + ref := spec.MustCreateRef(expectedRef) + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "string_prop": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + "object_prop": { // This prop does not have a terraform compliant name; however an automatic translation is performed behind the scenes to make it compliant + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Ref: ref, + }, + }, + }, + Required: []string{"string_prop", "object_prop"}, + }, + }, + schemaDefinitions: map[string]spec.Schema{ + "ObjectProperty": { + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "message": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + }, + }, + Required: []string{"message"}, + }, + }, + }, + } + Convey("When createTerraformResourceSchema method is called", func() { + resourceSchema, err := r.createTerraformResourceSchema() + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the tf resource schema returned should not be nil", func() { + So(resourceSchema, ShouldNotBeNil) + }) + Convey("And the tf resource schema returned should match the swagger props - 'string_prop' and 'object_prop'", func() { + So(resourceSchema, ShouldNotBeNil) + So(resourceSchema, ShouldContainKey, "string_prop") + So(resourceSchema, ShouldContainKey, "object_prop") + }) + Convey("And the properties 'string_prop' and 'object_prop' should be required", func() { + So(resourceSchema["string_prop"].Required, ShouldBeTrue) + So(resourceSchema["object_prop"].Required, ShouldBeTrue) + }) + Convey("And the tf resource schema ", func() { + So(resourceSchema["object_prop"].Elem.(*schema.Resource).Schema, ShouldContainKey, "message") + }) + Convey("And the properties 'message' should be required", func() { + So(resourceSchema["object_prop"].Elem.(*schema.Resource).Schema["message"].Required, ShouldBeTrue) + }) + }) + }) } func TestConvertToTerraformCompliantFieldName(t *testing.T) { Convey("Given a property with a name that is terraform field name compliant", t, func() { propertyName := "some_prop_name_that_is_terraform_field_name_compliant" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ propertyName: { @@ -915,7 +1120,7 @@ func TestConvertToTerraformCompliantFieldName(t *testing.T) { Convey("Given a property with a name that is NOT terraform field name compliant", t, func() { propertyName := "thisPropIsNotTerraformField_Compliant" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ propertyName: { @@ -940,7 +1145,7 @@ func TestConvertToTerraformCompliantFieldName(t *testing.T) { propertyName := "thisPropIsNotTerraformField_Compliant" expectedPropertyName := "this_property_is_now_terraform_field_compliant" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ propertyName: { @@ -968,7 +1173,7 @@ func TestConvertToTerraformCompliantFieldName(t *testing.T) { Convey("Given a property with a name that is NOT terraform field name compliant but has an extension that overrides it which in turn is also not terraform name compliant", t, func() { propertyName := "thisPropIsNotTerraformField_Compliant" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ propertyName: { @@ -997,7 +1202,7 @@ func TestConvertToTerraformCompliantFieldName(t *testing.T) { func TestGetResourceIdentifier(t *testing.T) { Convey("Given a swagger schema definition that has an id property", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ idDefaultPropertyName: { @@ -1025,7 +1230,7 @@ func TestGetResourceIdentifier(t *testing.T) { extensions := spec.Extensions{} extensions.Add(extTfID, true) r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "some-other-id": { @@ -1053,7 +1258,7 @@ func TestGetResourceIdentifier(t *testing.T) { extensions := spec.Extensions{} extensions.Add(extTfID, true) r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "id": { @@ -1087,7 +1292,7 @@ func TestGetResourceIdentifier(t *testing.T) { extensions := spec.Extensions{} extensions.Add(extTfID, false) r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "some-other-id": { @@ -1110,7 +1315,7 @@ func TestGetResourceIdentifier(t *testing.T) { Convey("Given a swagger schema definition that NEITHER HAS an 'id' property NOR a property configured with x-terraform-id set to true", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "prop-that-is-not-id": { @@ -1138,10 +1343,10 @@ func TestGetResourceIdentifier(t *testing.T) { }) } -func TestGetStatusIdentifier(t *testing.T) { - Convey("Given a swagger schema definition that has an status property", t, func() { +func TestGetStatusId(t *testing.T) { + Convey("Given a swagger schema definition that has an status property that is not an object", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ statusDefaultPropertyName: { @@ -1158,12 +1363,15 @@ func TestGetStatusIdentifier(t *testing.T) { }, } Convey("When getStatusIdentifier method is called", func() { - status, err := r.getStatusIdentifier() + status, err := r.getStatusIdentifier(r.schemaDefinition, true, true) Convey("Then the error returned should be nil", func() { So(err, ShouldBeNil) }) - Convey("Then the value returned should be 'status'", func() { - So(status, ShouldEqual, statusDefaultPropertyName) + Convey("And the status returned should not be empty'", func() { + So(status, ShouldNotBeEmpty) + }) + Convey("Then the value returned should contain the name of the property 'status'", func() { + So(status[0], ShouldEqual, statusDefaultPropertyName) }) }) }) @@ -1173,7 +1381,7 @@ func TestGetStatusIdentifier(t *testing.T) { extensions.Add(extTfFieldStatus, true) expectedStatusProperty := "some-other-property-holding-status" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ expectedStatusProperty: { @@ -1190,12 +1398,15 @@ func TestGetStatusIdentifier(t *testing.T) { }, } Convey("When getStatusIdentifier method is called", func() { - id, err := r.getStatusIdentifier() + status, err := r.getStatusIdentifier(r.schemaDefinition, true, true) Convey("Then the error returned should be nil", func() { So(err, ShouldBeNil) }) - Convey("Then the value returned should be 'some-other-property-holding-status'", func() { - So(id, ShouldEqual, expectedStatusProperty) + Convey("And the status returned should not be empty'", func() { + So(status, ShouldNotBeEmpty) + }) + Convey("Then the value returned should contain the name of the property 'some-other-property-holding-status'", func() { + So(status[0], ShouldEqual, expectedStatusProperty) }) }) }) @@ -1205,7 +1416,7 @@ func TestGetStatusIdentifier(t *testing.T) { extensions.Add(extTfFieldStatus, true) expectedStatusProperty := "some-other-property-holding-status" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "status": { @@ -1213,6 +1424,9 @@ func TestGetStatusIdentifier(t *testing.T) { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, }, expectedStatusProperty: { VendorExtensible: spec.VendorExtensible{Extensions: extensions}, @@ -1228,12 +1442,72 @@ func TestGetStatusIdentifier(t *testing.T) { }, } Convey("When getStatusIdentifier method is called", func() { - id, err := r.getStatusIdentifier() + status, err := r.getStatusIdentifier(r.schemaDefinition, true, true) Convey("Then the error returned should be nil", func() { So(err, ShouldBeNil) }) + Convey("And the status returned should not be empty'", func() { + So(status, ShouldNotBeEmpty) + }) Convey("Then the value returned should be 'some-other-property-holding-status' as it takes preference over the default 'status' property", func() { - So(id, ShouldEqual, expectedStatusProperty) + So(status[0], ShouldEqual, expectedStatusProperty) + }) + }) + }) + + Convey("Given a swagger schema definition that HAS an status field which is an object containing a status field", t, func() { + extensions := spec.Extensions{} + extensions.Add(extTfFieldStatus, true) + expectedStatusProperty := "actualStatus" + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + "status": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + expectedStatusProperty: { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + }, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + status, err := r.getStatusIdentifier(r.schemaDefinition, true, true) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("And the status returned should not be empty'", func() { + So(status, ShouldNotBeEmpty) + }) + Convey("Then the status array returned should contain the different the trace of property names (hierarchy) until the last one which is the one used as status, in this case 'actualStatus' on the last index", func() { + So(status[0], ShouldEqual, "status") + So(status[1], ShouldEqual, expectedStatusProperty) }) }) }) @@ -1243,7 +1517,7 @@ func TestGetStatusIdentifier(t *testing.T) { extensions.Add(extTfFieldStatus, false) expectedStatusProperty := "some-other-property-holding-status" r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ expectedStatusProperty: { @@ -1260,7 +1534,7 @@ func TestGetStatusIdentifier(t *testing.T) { }, } Convey("When getStatusIdentifier method is called", func() { - _, err := r.getStatusIdentifier() + _, err := r.getStatusIdentifier(r.schemaDefinition, true, true) Convey("Then the error returned should not be nil", func() { So(err, ShouldNotBeNil) }) @@ -1269,7 +1543,7 @@ func TestGetStatusIdentifier(t *testing.T) { Convey("Given a swagger schema definition that NEITHER HAS an 'status' property NOR a property configured with 'x-terraform-field-status' set to true", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ "prop-that-is-not-status": { @@ -1292,19 +1566,19 @@ func TestGetStatusIdentifier(t *testing.T) { }, } Convey("When getStatusIdentifier method is called", func() { - _, err := r.getStatusIdentifier() + _, err := r.getStatusIdentifier(r.schemaDefinition, true, true) Convey("Then the error returned should NOT be nil", func() { So(err, ShouldNotBeNil) }) }) }) - Convey("Given a swagger schema definition with a property configured with 'x-terraform-field-status' set to true but is not readonly", t, func() { + Convey("Given a swagger schema definition with a status property that is not readonly", t, func() { r := resourceInfo{ - schemaDefinition: spec.Schema{ + schemaDefinition: &spec.Schema{ SchemaProps: spec.SchemaProps{ Properties: map[string]spec.Schema{ - "prop-that-is-not-status": { + "status": { VendorExtensible: spec.VendorExtensible{}, SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -1313,7 +1587,24 @@ func TestGetStatusIdentifier(t *testing.T) { ReadOnly: false, }, }, - "prop-that-is-not-status-and-does-not-have-status-metadata-either": { + }, + }, + }, + } + Convey("When getStatusIdentifier method is called", func() { + _, err := r.getStatusIdentifier(r.schemaDefinition, true, true) + Convey("Then the error returned should NOT be nil", func() { + So(err, ShouldNotBeNil) + }) + }) + }) + + Convey("Given a swagger schema definition with a property configured with 'x-terraform-field-status' set to true and it is not readonly", t, func() { + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "status": { VendorExtensible: spec.VendorExtensible{}, SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -1323,13 +1614,137 @@ func TestGetStatusIdentifier(t *testing.T) { }, }, } - Convey("When getStatusIdentifier method is called", func() { - _, err := r.getStatusIdentifier() + Convey("When getStatusIdentifier method is called with a schema definition and forceReadOnly check is disabled (this happens when the method is called recursively)", func() { + status, err := r.getStatusIdentifier(r.schemaDefinition, false, false) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the status array returned should contain the status property even though it's not read only...readonly checks are only perform on root level properties", func() { + So(status[0], ShouldEqual, "status") + }) + }) + }) +} + +func TestGetStatusValueFromPayload(t *testing.T) { + Convey("Given a swagger schema definition that has an status property that is not an object", t, func() { + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + statusDefaultPropertyName: { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + }, + }, + }, + } + Convey("When getStatusValueFromPayload method is called with a payload that also has a 'status' field in the root level", func() { + expectedStatusValue := "someValue" + payload := map[string]interface{}{ + statusDefaultPropertyName: expectedStatusValue, + } + statusField, err := r.getStatusValueFromPayload(payload) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should contain the name of the property 'status'", func() { + So(statusField, ShouldEqual, expectedStatusValue) + }) + }) + + Convey("When getStatusValueFromPayload method is called with a payload that does not have status field", func() { + payload := map[string]interface{}{ + "someOtherPropertyName": "arggg", + } + _, err := r.getStatusValueFromPayload(payload) + Convey("Then the error returned should NOT be nil", func() { + So(err, ShouldNotBeNil) + }) + Convey("Then the error message should be", func() { + So(err.Error(), ShouldEqual, "payload does not match resouce schema, could not find the status field: [status]") + }) + }) + + Convey("When getStatusValueFromPayload method is called with a payload that has a status field but the value is not supported", func() { + payload := map[string]interface{}{ + statusDefaultPropertyName: 12, // this value is not supported, only strings and maps (for nested properties within an object) are supported + } + _, err := r.getStatusValueFromPayload(payload) Convey("Then the error returned should NOT be nil", func() { So(err, ShouldNotBeNil) }) + Convey("Then the error message should be", func() { + So(err.Error(), ShouldEqual, "status property value '[status]' does not have a supported type [string/map]") + }) }) }) + + Convey("Given a swagger schema definition that has an status property that IS an object", t, func() { + expectedStatusProperty := "some-other-property-holding-status" + extensions := spec.Extensions{} + extensions.Add(extTfFieldStatus, true) + r := resourceInfo{ + schemaDefinition: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "id": { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + statusDefaultPropertyName: { + VendorExtensible: spec.VendorExtensible{}, + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + expectedStatusProperty: { + VendorExtensible: spec.VendorExtensible{Extensions: extensions}, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + }, + }, + SwaggerSchemaProps: spec.SwaggerSchemaProps{ + ReadOnly: true, + }, + }, + }, + }, + }, + } + Convey("When getStatusValueFromPayload method is called with a payload that has an status object property inside which there's an status property", func() { + expectedStatusValue := "someStatusValue" + payload := map[string]interface{}{ + statusDefaultPropertyName: map[string]interface{}{ + expectedStatusProperty: expectedStatusValue, + }, + } + statusField, err := r.getStatusValueFromPayload(payload) + Convey("Then the error returned should be nil", func() { + So(err, ShouldBeNil) + }) + Convey("Then the value returned should contain the name of the property 'status'", func() { + So(statusField, ShouldEqual, expectedStatusValue) + }) + }) + }) + } func TestIsIDProperty(t *testing.T) { From 101be69078bf93701d1312d78289f1d6f60579f2 Mon Sep 17 00:00:00 2001 From: dikhan Date: Tue, 25 Sep 2018 15:58:34 -0700 Subject: [PATCH 60/60] add documentation --- docs/how_to.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/docs/how_to.md b/docs/how_to.md index feec45141..10ef5f1ef 100644 --- a/docs/how_to.md +++ b/docs/how_to.md @@ -430,6 +430,34 @@ definitions: - deleted ```` +Alternatively, the status field can also be of 'object' type in which case the nested properties can be defined in place or +the $ref attribute can be used to link to the corresponding status schema definition. The nested properties are considered +computed automatically even if they are not marked as readOnly. + +```` +definitions: + LBV1: + type: "object" + ... + properties: + newStatus: + $ref: "#/definitions/Status" + x-terraform-field-status: true # identifies the field that should be used as status for async operations. This is handy when the field name is not status but some other name the service provider might have chosen and enables the provider to identify the field as the status field that will be used to track progress for the async operations + readOnly: true + timeToProcess: # time that the resource will take to be processed in seconds + type: integer + default: 60 # it will take two minute to process the resource operation (POST/PUT/READ/DELETE) + simulate_failure: # allows user to set it to true and force an error on the API when the given operation (POST/PUT/READ/DELETE) is being performed + type: boolean + Status: + type: object + properties: + message: + type: string + status: + type: string +```` + *Note: This extension is only supported at the operation's response level.* @@ -590,6 +618,51 @@ string | Type: schema.TypeString | string value integer | schema.TypeInt | int value number | schema.TypeFloat | float value boolean | schema.TypeBool | boolean value +object | schema.TypeMap | map value + +Object types can be defined in two fashions: + +###### Nested properties + +Properties can have their schema definition in place or nested; and they must be of type 'object'. + +```` +definitions: + ContentDeliveryNetworkV1: + type: "object" + ... + properties: + ... + object_nested_scheme_property: + type: object # nested properties required type equal object to be considered as object + properties: + name: + type: string +```` + +###### Ref schema definition + +A property that has a $ref attribute is considered automatically and object so defining the type is optional (although +it's recommended). + +```` +definitions: + ContentDeliveryNetworkV1: + type: "object" + ... + properties: + ... + object_property: + #type: object - type is optional for properties of object type that use $ref + $ref: "#/definitions/ObjectProperty" + ObjectProperty: + type: object + required: + - message + properties: + message: + type: string +```` Additionally, properties can be flagged as required as follows: