mirror of
https://github.com/hanzozt/zt.git
synced 2026-11-03 05:01:58 +00:00
- ziti edge login now properly probes endpoints and their return content type along with the existing status code check - probed endpoint responses are no longer blindly parsed - the management endpoint is the default initial probe point, falling back to the client version endpoint then the legacy root version endpoint - the SPA/ZAC bindings for web apis has been moved to the webapis folder - improved erroring and messaging for the SPA/ZAC handling
606 lines
17 KiB
Go
606 lines
17 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://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 util
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/Jeffail/gabs"
|
|
openApiRuntime "github.com/go-openapi/runtime"
|
|
"github.com/go-openapi/strfmt"
|
|
"github.com/openziti/edge-api/rest_management_api_client"
|
|
"github.com/openziti/edge-api/rest_model"
|
|
fabric_rest_client "github.com/openziti/ziti/controller/rest_client"
|
|
"gopkg.in/resty.v1"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Use a 2-second timeout with a retry count of 5
|
|
func NewClient() *resty.Client {
|
|
return resty.
|
|
New().
|
|
SetTimeout(2 * time.Second).
|
|
SetRetryCount(5).
|
|
SetRedirectPolicy(resty.FlexibleRedirectPolicy(15))
|
|
}
|
|
|
|
func PrettyPrintResponse(resp *resty.Response) string {
|
|
out := resp.String()
|
|
var prettyJSON bytes.Buffer
|
|
if err := json.Indent(&prettyJSON, []byte(out), "", " "); err == nil {
|
|
return prettyJSON.String()
|
|
}
|
|
return out
|
|
}
|
|
|
|
func OutputJson(out io.Writer, data []byte) {
|
|
var prettyJSON bytes.Buffer
|
|
if err := json.Indent(&prettyJSON, data, "", " "); err == nil {
|
|
if _, err := fmt.Fprint(out, prettyJSON.String()); err != nil {
|
|
panic(err)
|
|
}
|
|
} else {
|
|
if _, err := fmt.Fprint(out, data); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ControllerDetailEntity(api API, entityType, entityId string, logJSON bool, out io.Writer, timeout int, verbose bool) (*gabs.Container, error) {
|
|
restClientIdentity, err := LoadSelectedIdentity()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
baseUrl, err := restClientIdentity.GetBaseUrlForApi(api)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := NewRequest(restClientIdentity, timeout, verbose)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
queryUrl := baseUrl + "/" + path.Join(entityType, entityId)
|
|
|
|
resp, err := req.Get(queryUrl)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to list entities at %v in Ziti Edge Controller at %v. Error: %v", queryUrl, baseUrl, err)
|
|
}
|
|
|
|
if resp.StatusCode() != http.StatusOK {
|
|
return nil, fmt.Errorf("error listing %v in Ziti Edge Controller. Status code: %v, Server returned: %v",
|
|
queryUrl, resp.Status(), PrettyPrintResponse(resp))
|
|
}
|
|
|
|
if logJSON {
|
|
OutputJson(out, resp.Body())
|
|
}
|
|
|
|
jsonParsed, err := gabs.ParseJSON(resp.Body())
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse response from %v. Server returned: %v", queryUrl, resp.String())
|
|
}
|
|
|
|
return jsonParsed, nil
|
|
}
|
|
|
|
// EdgeControllerListSubEntities will list entities of the given type in the given Edge Controller
|
|
func EdgeControllerListSubEntities(entityType, subType, entityId string, filter string, logJSON bool, out io.Writer, timeout int, verbose bool) (*gabs.Container, error) {
|
|
params := url.Values{}
|
|
if filter != "" {
|
|
params.Add("filter", filter)
|
|
}
|
|
return EdgeControllerList(entityType+"/"+entityId+"/"+subType, params, logJSON, out, timeout, verbose)
|
|
}
|
|
|
|
// EdgeControllerList will list entities of the given type in the given Edge Controller
|
|
func EdgeControllerList(path string, params url.Values, logJSON bool, out io.Writer, timeout int, verbose bool) (*gabs.Container, error) {
|
|
return ControllerList("edge", path, params, logJSON, out, timeout, verbose)
|
|
}
|
|
|
|
// ControllerList will list entities of the given type in the given Edge Controller
|
|
func ControllerList(api API, path string, params url.Values, logJSON bool, out io.Writer, timeout int, verbose bool) (*gabs.Container, error) {
|
|
restClientIdentity, err := LoadSelectedIdentity()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
baseUrl, err := restClientIdentity.GetBaseUrlForApi(api)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := NewRequest(restClientIdentity, timeout, verbose)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
queryUrl := baseUrl + "/" + path
|
|
|
|
if len(params) > 0 {
|
|
queryUrl += "?" + params.Encode()
|
|
}
|
|
|
|
resp, err := req.Get(queryUrl)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to list entities at %v in Ziti Controller at %v. Error: %v", queryUrl, baseUrl, err)
|
|
}
|
|
|
|
if resp.StatusCode() != http.StatusOK {
|
|
return nil, fmt.Errorf("error listing %v in Ziti Edge Controller. Status code: %v, Server returned: %v",
|
|
queryUrl, resp.Status(), PrettyPrintResponse(resp))
|
|
}
|
|
|
|
if logJSON {
|
|
OutputJson(out, resp.Body())
|
|
}
|
|
|
|
jsonParsed, err := gabs.ParseJSON(resp.Body())
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse response from %v. Server returned: %v", queryUrl, resp.String())
|
|
}
|
|
|
|
return jsonParsed, nil
|
|
}
|
|
|
|
var _ http.RoundTripper = &edgeTransport{}
|
|
|
|
type edgeTransport struct {
|
|
*http.Transport
|
|
RequestFunc func(*http.Request) error
|
|
ResponseFunc func(*http.Response, error)
|
|
}
|
|
|
|
func (edgeTransport *edgeTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
if edgeTransport.RequestFunc != nil {
|
|
if err := edgeTransport.RequestFunc(r); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
resp, err := edgeTransport.Transport.RoundTrip(r)
|
|
|
|
if edgeTransport.ResponseFunc != nil {
|
|
edgeTransport.ResponseFunc(resp, err)
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
|
|
type ApiErrorPayload interface {
|
|
Error() string
|
|
GetPayload() *rest_model.APIErrorEnvelope
|
|
}
|
|
|
|
type RestApiError struct {
|
|
ApiErrorPayload
|
|
}
|
|
|
|
func formatApiError(error *rest_model.APIError) string {
|
|
cause := ""
|
|
if error.Cause != nil {
|
|
if error.Cause.APIError.Code != "" {
|
|
cause = formatApiError(&error.Cause.APIError)
|
|
} else if error.Cause.APIFieldError.Field != "" {
|
|
cause = fmt.Sprintf("INVALID_FIELD - %s [%s] %s", error.Cause.APIFieldError.Field, error.Cause.APIFieldError.Value, error.Cause.APIFieldError.Reason)
|
|
}
|
|
}
|
|
|
|
if cause != "" {
|
|
cause = ": " + cause
|
|
}
|
|
return fmt.Sprintf("%s - %s%s", error.Code, error.Message, cause)
|
|
}
|
|
|
|
func (a RestApiError) Error() string {
|
|
if payload := a.ApiErrorPayload.GetPayload(); payload != nil {
|
|
|
|
if payload.Error == nil {
|
|
return fmt.Sprintf("could not read API error, payload.error was nil: %v", a.ApiErrorPayload.Error())
|
|
}
|
|
return formatApiError(payload.Error)
|
|
}
|
|
|
|
return fmt.Sprintf("could not read API error, payload was nil: %v", a.ApiErrorPayload.Error())
|
|
}
|
|
|
|
func WrapIfApiError(err error) error {
|
|
if apiErrorPayload, ok := err.(ApiErrorPayload); ok {
|
|
return &RestApiError{apiErrorPayload}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
type ClientOpts interface {
|
|
OutputRequestJson() bool
|
|
OutputResponseJson() bool
|
|
OutputWriter() io.Writer
|
|
ErrOutputWriter() io.Writer
|
|
}
|
|
|
|
func NewEdgeManagementClient(clientOpts ClientOpts) (*rest_management_api_client.ZitiEdgeManagement, error) {
|
|
restClientIdentity, err := LoadSelectedIdentity()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return restClientIdentity.NewEdgeManagementClient(clientOpts)
|
|
}
|
|
|
|
func NewFabricManagementClient(clientOpts ClientOpts) (*fabric_rest_client.ZitiFabric, error) {
|
|
restClientIdentity, err := LoadSelectedIdentity()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return restClientIdentity.NewFabricManagementClient(clientOpts)
|
|
}
|
|
|
|
type EdgeManagementAuth struct {
|
|
Token string
|
|
}
|
|
|
|
func (e EdgeManagementAuth) AuthenticateRequest(request openApiRuntime.ClientRequest, registry strfmt.Registry) error {
|
|
return request.SetHeaderParam("zt-session", e.Token)
|
|
}
|
|
|
|
// ControllerCreate will create entities of the given type in the given Edge Controller
|
|
func ControllerCreate(api API, entityType string, body string, out io.Writer, logRequestJson, logResponseJson bool, timeout int, verbose bool) (*gabs.Container, error) {
|
|
restClientIdentity, err := LoadSelectedRWIdentity()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
baseUrl, err := restClientIdentity.GetBaseUrlForApi(api)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := NewRequest(restClientIdentity, timeout, verbose)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
url := baseUrl + "/" + entityType
|
|
if logRequestJson {
|
|
fmt.Printf("%v to %v\n", "POST", url)
|
|
OutputJson(out, []byte(body))
|
|
fmt.Println()
|
|
}
|
|
|
|
resp, err := req.SetBody(body).Post(url)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to create %v instance in Ziti Edge Controller at %v. Error: %v", entityType, baseUrl, err)
|
|
}
|
|
|
|
if resp.StatusCode() != http.StatusCreated {
|
|
return nil, fmt.Errorf("error creating %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v",
|
|
entityType, baseUrl, resp.Status(), PrettyPrintResponse(resp))
|
|
}
|
|
|
|
if logResponseJson {
|
|
OutputJson(out, resp.Body())
|
|
}
|
|
|
|
jsonParsed, err := gabs.ParseJSON(resp.Body())
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse response from %v. Server returned: %v", baseUrl, resp.String())
|
|
}
|
|
|
|
return jsonParsed, nil
|
|
}
|
|
|
|
// ControllerDelete will delete entities of the given type in the given Controller
|
|
func ControllerDelete(api API, entityType string, id string, body string, out io.Writer, logRequestJson bool, logResponseJson bool, timeout int, verbose bool) (error, *int) {
|
|
restClientIdentity, err := LoadSelectedRWIdentity()
|
|
if err != nil {
|
|
return err, nil
|
|
}
|
|
|
|
baseUrl, err := restClientIdentity.GetBaseUrlForApi(api)
|
|
if err != nil {
|
|
return err, nil
|
|
}
|
|
|
|
req, err := NewRequest(restClientIdentity, timeout, verbose)
|
|
if err != nil {
|
|
return err, nil
|
|
}
|
|
|
|
entityPath := entityType + "/" + id
|
|
fullUrl := baseUrl + "/" + entityPath
|
|
|
|
if logRequestJson {
|
|
fmt.Printf("%v to %v\n", "POST", fullUrl)
|
|
OutputJson(out, []byte(body))
|
|
fmt.Println()
|
|
}
|
|
|
|
if body != "" {
|
|
req = req.SetBody(body)
|
|
}
|
|
|
|
resp, err := req.Delete(fullUrl)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("unable to delete %v instance in Ziti Edge Controller at %v. Error: %v", entityPath, baseUrl, err), nil
|
|
}
|
|
|
|
if resp.StatusCode() != http.StatusOK {
|
|
statusCode := resp.StatusCode()
|
|
return fmt.Errorf("error deleting %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v",
|
|
entityPath, baseUrl, resp.Status(), PrettyPrintResponse(resp)), &statusCode
|
|
}
|
|
|
|
if logResponseJson {
|
|
OutputJson(out, resp.Body())
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// ControllerUpdate will update entities of the given type in the given Edge Controller
|
|
func ControllerUpdate(api API, entityType string, body string, out io.Writer, method string, logRequestJson, logResponseJSON bool, timeout int, verbose bool) (*gabs.Container, error) {
|
|
restClientIdentity, err := LoadSelectedRWIdentity()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
baseUrl, err := restClientIdentity.GetBaseUrlForApi(api)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := NewRequest(restClientIdentity, timeout, verbose)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
url := baseUrl + "/" + entityType
|
|
|
|
if logRequestJson {
|
|
fmt.Printf("%v to %v\n", method, url)
|
|
OutputJson(out, []byte(body))
|
|
fmt.Println()
|
|
}
|
|
|
|
resp, err := req.SetBody(body).Execute(method, url)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to update %v instance in Ziti Edge Controller at %v. Error: %v", entityType, baseUrl, err)
|
|
}
|
|
|
|
if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusAccepted {
|
|
return nil, fmt.Errorf("error updating %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v",
|
|
entityType, baseUrl, resp.Status(), PrettyPrintResponse(resp))
|
|
}
|
|
|
|
if logResponseJSON {
|
|
OutputJson(out, resp.Body())
|
|
}
|
|
|
|
if len(resp.Body()) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
jsonParsed, err := gabs.ParseJSON(resp.Body())
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse response from %v. Server returned: %v", baseUrl, resp.String())
|
|
}
|
|
|
|
return jsonParsed, nil
|
|
}
|
|
|
|
// EdgeControllerVerify will verify entities of the given type in the given Edge Controller
|
|
func EdgeControllerVerify(entityType, id, body string, out io.Writer, logJSON bool, timeout int, verbose bool) error {
|
|
restClientIdentity, err := LoadSelectedRWIdentity()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
baseUrl, err := restClientIdentity.GetBaseUrlForApi("edge")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client, err := restClientIdentity.NewClient(time.Duration(timeout)*time.Second, verbose)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req := restClientIdentity.NewRequest(client)
|
|
|
|
resp, err := req.
|
|
SetHeader("Content-Type", "text/plain").
|
|
SetBody(body).
|
|
Post(baseUrl + "/" + entityType + "/" + id + "/verify")
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("unable to verify %v instance [%s] in Ziti Edge Controller at %v. Error: %v", entityType, id, baseUrl, err)
|
|
}
|
|
|
|
if resp.StatusCode() != http.StatusOK {
|
|
return fmt.Errorf("error verifying %v instance (%v) in Ziti Edge Controller at %v. Status code: %v, Server returned: %v",
|
|
entityType, id, baseUrl, resp.Status(), PrettyPrintResponse(resp))
|
|
}
|
|
|
|
if logJSON {
|
|
OutputJson(out, resp.Body())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func EdgeControllerRequest(entityType string, out io.Writer, logJSON bool, timeout int, verbose bool, doRequest func(*resty.Request, string) (*resty.Response, error)) (*gabs.Container, error) {
|
|
restClientIdentity, err := LoadSelectedRWIdentity()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
baseUrl, err := restClientIdentity.GetBaseUrlForApi("edge")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
request, err := NewRequest(restClientIdentity, timeout, verbose)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := doRequest(request, baseUrl+"/"+entityType)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to [%s] %v instance in Ziti Edge Controller at %v. Error: %v", request.Method, entityType, baseUrl, err)
|
|
}
|
|
|
|
if resp.StatusCode() != http.StatusOK {
|
|
return nil, fmt.Errorf("error performing request [%s] %v instance in Ziti Edge Controller at %v. Status code: %v, Server returned: %v",
|
|
request.Method, entityType, baseUrl, resp.Status(), PrettyPrintResponse(resp))
|
|
}
|
|
|
|
if logJSON {
|
|
OutputJson(out, resp.Body())
|
|
}
|
|
|
|
if resp.Body() == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
jsonParsed, err := gabs.ParseJSON(resp.Body())
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse response from %v. Server returned: %v", baseUrl, resp.String())
|
|
}
|
|
|
|
return jsonParsed, nil
|
|
}
|
|
|
|
func EdgeControllerGetManagementApiBasePathWithPool(host string, caPool *x509.CertPool) string {
|
|
client := NewClient()
|
|
|
|
client.SetHostURL(host)
|
|
|
|
if caPool != nil {
|
|
if client.GetClient().Transport.(*http.Transport).TLSClientConfig == nil {
|
|
client.GetClient().Transport.(*http.Transport).TLSClientConfig = &tls.Config{
|
|
RootCAs: caPool,
|
|
}
|
|
}
|
|
}
|
|
|
|
return getManagementApiBasePath(host, client)
|
|
}
|
|
|
|
func hasJsonContentType(resp *http.Response) bool {
|
|
contentType := resp.Header.Get("Content-Type")
|
|
return strings.HasPrefix(contentType, "application/json")
|
|
}
|
|
|
|
// parseManagementPath will treat body as JSON from the version endpoint and attempt to
|
|
// return the management path provided in that data. If the data cannot be parsed, empty string
|
|
// is returned.
|
|
func parseManagementPath(body []byte) string {
|
|
data, err := gabs.ParseJSON(body)
|
|
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
// controller w/ APIs split
|
|
if data.ExistsP("data.apiVersions.edge-management") {
|
|
if respPath, ok := data.Path("data.apiVersions.edge-management.v1.path").Data().(string); !ok {
|
|
return ""
|
|
} else {
|
|
return respPath
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// getManagementApiBasePath attempts to determine which path prefix to use for the management API. This covers cases
|
|
// where only a host is provided w/ no path prefix or where host provided only hosts the client API and the management
|
|
// API is hosted somewhere else. API locations are determined by inspecting the versions endpoint.
|
|
func getManagementApiBasePath(host string, client *resty.Client) string {
|
|
resp, err := client.R().Get("/edge/management/v1/version")
|
|
|
|
if err == nil && resp.StatusCode() == http.StatusOK && hasJsonContentType(resp.RawResponse) {
|
|
respPath := parseManagementPath(resp.Body())
|
|
|
|
if respPath != "" {
|
|
return host + respPath
|
|
}
|
|
}
|
|
|
|
//guessing management first didn't work, maybe we have access to the client API to find the management API
|
|
resp, err = client.R().Get("/edge/client/v1/version")
|
|
|
|
if err == nil && resp.StatusCode() == http.StatusOK && hasJsonContentType(resp.RawResponse) {
|
|
respPath := parseManagementPath(resp.Body())
|
|
|
|
if respPath != "" {
|
|
return host + respPath
|
|
}
|
|
}
|
|
|
|
//neither the specific management/client paths worked, maybe the legacy /version path works
|
|
resp, err = client.R().Get("/version")
|
|
|
|
if err == nil && resp.StatusCode() == http.StatusOK && hasJsonContentType(resp.RawResponse) {
|
|
respPath := parseManagementPath(resp.Body())
|
|
|
|
if respPath != "" {
|
|
return host + respPath
|
|
}
|
|
}
|
|
|
|
//give up
|
|
return host
|
|
}
|
|
|
|
// EdgeControllerGetManagementApiBasePath accepts host as `http://domain:port` and attempts to
|
|
// determine the proper path that should be used to access the Edge Management API. Depending
|
|
// on the version of the Edge Controller the API may be monolith on `/edge/<version>` and `/` or split into
|
|
// `/edge/management/<version>` and `/edge/client/<version>`.
|
|
func EdgeControllerGetManagementApiBasePath(host string, cert string) string {
|
|
client := NewClient()
|
|
|
|
client.SetHostURL(host)
|
|
|
|
if cert != "" {
|
|
client.SetRootCertificate(cert)
|
|
}
|
|
|
|
return getManagementApiBasePath(host, client)
|
|
}
|