internal/pkg/apivalidator/request.go (119 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package apivalidator import ( "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "sort" "strings" "github.com/go-openapi/spec" "github.com/elastic/cloud-sdk-go/pkg/multierror" ) type prismResponseBody struct { Type string `json:"type,omitempty"` Title string `json:"title,omitempty"` Status int32 `json:"status,omitempty"` Detail string `json:"detail,omitempty"` } type apiRequests struct { reqs []*http.Request } // NewHTTPRequests creates multiple valid requests from a defined // api specification and host. func NewHTTPRequests(host string, client *http.Client, cloudSpec *spec.Swagger) error { if client == nil { return errors.New("requests to api: an http client must be specified") } var reqs []*http.Request requests := apiRequests{reqs} // nolint for path, pathItem := range cloudSpec.Paths.Paths { var request []*http.Request if pathItem.Head != nil { request = append(request, buildRequest(host, path, "HEAD")) } if pathItem.Get != nil { request = append(request, buildRequest(host, path, "GET")) } if pathItem.Post != nil { request = append(request, buildRequest(host, path, "POST")) } if pathItem.Put != nil { request = append(request, buildRequest(host, path, "PUT")) } if pathItem.Patch != nil { request = append(request, buildRequest(host, path, "PATCH")) } if pathItem.Delete != nil { request = append(request, buildRequest(host, path, "DELETE")) } requests.reqs = append(requests.reqs, request...) } sort.SliceStable(requests.reqs, func(i, j int) bool { return requests.reqs[i].Method < requests.reqs[j].Method }) var merr = multierror.NewPrefixed("api spec validation") for i := range requests.reqs { req := requests.reqs[i] fmt.Printf("%v %v%v\n", req.Method, req.URL.Host, req.URL.Path) if err := validateRequest(req, client); err != nil { merr = merr.Append(err) } } return merr.ErrorOrNil() } func buildRequest(host, path, method string) *http.Request { // We are setting some defaults and populating empty bodies // in order to avoid validation errors from API path = strings.ReplaceAll(path, "{resource_kind}", "kibana") path = strings.ReplaceAll(path, "{stateless_resource_kind}", "apm") isPost := method == "POST" isPut := method == "PUT" isPatch := method == "PATCH" isUsersAuthKeys := strings.Contains(path, "/users/auth/keys") isDeploymentTemplates := strings.Contains(path, "deployments/templates") if isDeploymentTemplates { path += "?region=ece-region" } if isPost || isPut || isPatch || isUsersAuthKeys { r := strings.NewReader(`{}`) return &http.Request{ Method: method, Body: io.NopCloser(r), URL: &url.URL{ Host: host, Path: path, }, } } return &http.Request{ Method: method, URL: &url.URL{ Host: host, Path: path, }, } } func validateRequest(request *http.Request, client *http.Client) error { endpointURL := request.URL.Host + request.URL.Path req, err := http.NewRequest(request.Method, endpointURL, request.Body) if err != nil { return err } req.Header.Set("Authorization", os.ExpandEnv("ApiKey $EC_API_KEY")) resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() var prismResponses *prismResponseBody if err := json.NewDecoder(resp.Body).Decode(&prismResponses); err != nil { return err } // When a response from the prism validation proxy instead of an API response // is returned, it means we have discrepancies between our API spec and the live API. // These discrepancies are what we are looking for. if prismResponses.Type != "" { return fmt.Errorf("prism error: Type: %v, Title: %v, Status: %v, Detail: %v", prismResponses.Type, prismResponses.Title, prismResponses.Status, prismResponses.Detail) } return nil }