pkg/api/client_runtime.go (92 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 api import ( "context" "errors" "fmt" "net/url" "strings" "github.com/go-openapi/runtime" runtimeclient "github.com/go-openapi/runtime/client" "github.com/elastic/cloud-sdk-go/pkg/client" ) const ( // RegionBasePath is for /platform operations which require a Region to be // passed as of an API call context.Context. Previously used to create a // global client per API instance, now used on a per-operation basis. RegionBasePath = "/api/v1/regions/%s" rawMetadataDeploymentResourceTextProducer = "set-deployment-resource-raw-metadata" rawMetadataTextProducer = "set-es-cluster-metadata-raw" updateUserTextProducer = "update-user" updateCurrentUserTextProducer = "update-current-user" ) // DefaultBasePath is used as the base prefix for the API. var DefaultBasePath = client.DefaultBasePath // globalPath contains a mapping of the path prefixes which need/ don't need a // region path interpolation, to work properly. If set to false, then the API // for that path will require a custom context.Context containing the region // value != "". Any of the prefixes set to `true` are global paths due to the // region value already being integrated into the auto-generated parameters. // Strictly, there's no need for a `"key":false` to be present in the map, but // it does make it explicit and nicer to maintain. var globalPath = map[string]bool{ "clusters": false, "comments": false, "deployments": true, "phone-home": true, "platform": false, "stack": false, "user": true, "users": true, "billing": true, "organizations": true, "saas": true, } type newRuntimeFunc func(region string) *runtimeclient.Runtime // NewCloudClientRuntime creates a CloudClientRuntime from the config. Using // the configured region (if any) to instantiate two different client.Runtime. // If there's no region specified in the config then both are regionless. func NewCloudClientRuntime(c Config) (*CloudClientRuntime, error) { u, err := url.Parse(c.Host) if err != nil { return nil, err } scheme := []string{u.Scheme} return &CloudClientRuntime{ newRegionRuntime: func(r string) *runtimeclient.Runtime { return AddTypeConsumers(runtimeclient.NewWithClient( u.Host, fmt.Sprintf(RegionBasePath, r), scheme, c.Client, )) }, runtime: AddTypeConsumers(runtimeclient.NewWithClient( u.Host, DefaultBasePath, scheme, c.Client, )), }, nil } // CloudClientRuntime wraps runtimeclient.Runtime to allow operations to use a // transport depending on the operation which is being performed. type CloudClientRuntime struct { newRegionRuntime newRuntimeFunc runtime *runtimeclient.Runtime } // Submit calls either the regionRuntime or the regionless runtime depending on // which operation is being performed. Any API call to /deployments will use a // regionless runtime while all others will use a region (if specified). func (r *CloudClientRuntime) Submit(op *runtime.ClientOperation) (interface{}, error) { rTime, err := r.getRuntime(op) if err != nil { return nil, err } defer overrideJSONProducer(rTime, op.ID)() return rTime.Submit(op) } func (r *CloudClientRuntime) getRuntime(op *runtime.ClientOperation) (*runtimeclient.Runtime, error) { var notDeploymentNotes = !strings.Contains(op.PathPattern, "/note") regionless := globalPath[strings.Split(op.PathPattern, "/")[1]] if regionless && notDeploymentNotes { return r.runtime, nil } region, err := getRegion(op.Context) if err != nil { return nil, err } return r.newRegionRuntime(region), nil } // overrideJSONProducer will override the default JSON producer function for // a Text producer which won't to serialize the data to JSON, and just send // the body as is over the wire. This is useful in cases where a JSON body is // being sent as a Go string value, not doing this will cause the payload json // quotes to be escaped. See unit tests for examples. // It returns a function which can be used as a callback to reset the producer // to its original value. func overrideJSONProducer(r *runtimeclient.Runtime, opID string) func() { if !(opID == updateUserTextProducer || opID == rawMetadataTextProducer || opID == updateCurrentUserTextProducer || opID == rawMetadataDeploymentResourceTextProducer) { return func() {} } r.Producers[runtime.JSONMime] = runtime.TextProducer() return func() { r.Producers[runtime.JSONMime] = runtime.JSONProducer() } } func getRegion(ctx context.Context) (string, error) { if region, ok := GetContextRegion(ctx); ok { return region, nil } return "", errors.New( "the requested operation requires a region but none has been set", ) }