Implement API client functionality in the garm-cli

Signed-off-by: Mihaela Balutoiu <mbalutoiu@cloudbasesolutions.com>
This commit is contained in:
Mihaela Balutoiu 2023-07-19 20:11:11 +03:00
parent 851a9bd0ae
commit ff5abf1294
35 changed files with 220 additions and 5661 deletions

View file

@ -1,301 +0,0 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// 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
//
// 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 client
import (
"encoding/json"
"fmt"
apiParams "github.com/cloudbase/garm/apiserver/params"
"github.com/cloudbase/garm/cmd/garm-cli/config"
"github.com/cloudbase/garm/params"
"github.com/go-resty/resty/v2"
"github.com/pkg/errors"
)
func NewClient(name string, cfg config.Manager, debug bool) *Client {
cli := resty.New()
if cfg.Token != "" {
cli = cli.SetAuthToken(cfg.Token)
}
cli = cli.
SetHeader("Accept", "application/json").
SetDebug(debug)
return &Client{
ManagerName: name,
Config: cfg,
client: cli,
}
}
type Client struct {
ManagerName string
Config config.Manager
client *resty.Client
}
func (c *Client) handleError(err error, resp *resty.Response) error {
var ret error
if err != nil {
ret = fmt.Errorf("request returned error: %s", err)
}
if resp != nil && resp.IsError() {
body := resp.Body()
if len(body) > 0 {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr == nil {
ret = fmt.Errorf("API returned error: %s", apiErr.Details)
}
}
}
return ret
}
func (c *Client) decodeAPIError(body []byte) (apiParams.APIErrorResponse, error) {
var errDetails apiParams.APIErrorResponse
if err := json.Unmarshal(body, &errDetails); err != nil {
return apiParams.APIErrorResponse{}, fmt.Errorf("invalid response from server, use --debug for more info")
}
return errDetails, fmt.Errorf("error in API call: %s", errDetails.Details)
}
func (c *Client) InitManager(url string, param params.NewUserParams) (params.User, error) {
body, err := json.Marshal(param)
if err != nil {
return params.User{}, errors.Wrap(err, "marshaling body")
}
url = fmt.Sprintf("%s/api/v1/first-run/", url)
var response params.User
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Post(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return params.User{}, errors.Wrap(decErr, "sending request")
}
return params.User{}, fmt.Errorf("error running init: %s", apiErr.Details)
}
return response, nil
}
func (c *Client) Login(url string, param params.PasswordLoginParams) (string, error) {
body, err := json.Marshal(param)
if err != nil {
return "", errors.Wrap(err, "marshaling body")
}
url = fmt.Sprintf("%s/api/v1/auth/login", url)
var response params.JWTResponse
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Post(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return "", errors.Wrap(decErr, "sending request")
}
return "", fmt.Errorf("error performing login: %s", apiErr.Details)
}
return response.Token, nil
}
func (c *Client) ListCredentials() ([]params.GithubCredentials, error) {
var ghCreds []params.GithubCredentials
url := fmt.Sprintf("%s/api/v1/credentials", c.Config.BaseURL)
resp, err := c.client.R().
SetResult(&ghCreds).
Get(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return nil, errors.Wrap(decErr, "sending request")
}
return nil, fmt.Errorf("error fetching credentials: %s", apiErr.Details)
}
return ghCreds, nil
}
func (c *Client) ListProviders() ([]params.Provider, error) {
var providers []params.Provider
url := fmt.Sprintf("%s/api/v1/providers", c.Config.BaseURL)
resp, err := c.client.R().
SetResult(&providers).
Get(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return nil, errors.Wrap(decErr, "sending request")
}
return nil, fmt.Errorf("error fetching providers: %s", apiErr.Details)
}
return providers, nil
}
func (c *Client) GetInstanceByName(instanceName string) (params.Instance, error) {
url := fmt.Sprintf("%s/api/v1/instances/%s", c.Config.BaseURL, instanceName)
var response params.Instance
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return response, errors.Wrap(decErr, "sending request")
}
return response, fmt.Errorf("error performing login: %s", apiErr.Details)
}
return response, nil
}
func (c *Client) DeleteRunner(instanceName string) error {
url := fmt.Sprintf("%s/api/v1/instances/%s", c.Config.BaseURL, instanceName)
resp, err := c.client.R().
Delete(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return errors.Wrap(decErr, "sending request")
}
return fmt.Errorf("error deleting runner: %s", apiErr.Details)
}
return nil
}
func (c *Client) ListAllJobs() ([]params.Job, error) {
url := fmt.Sprintf("%s/api/v1/jobs", c.Config.BaseURL)
var response []params.Job
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err != nil || resp.IsError() {
return response, c.handleError(err, resp)
}
return response, nil
}
func (c *Client) ListPoolInstances(poolID string) ([]params.Instance, error) {
url := fmt.Sprintf("%s/api/v1/pools/%s/instances", c.Config.BaseURL, poolID)
var response []params.Instance
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return response, errors.Wrap(decErr, "sending request")
}
return response, fmt.Errorf("error performing login: %s", apiErr.Details)
}
return response, nil
}
func (c *Client) ListAllInstances() ([]params.Instance, error) {
url := fmt.Sprintf("%s/api/v1/instances", c.Config.BaseURL)
var response []params.Instance
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return response, errors.Wrap(decErr, "sending request")
}
return response, fmt.Errorf("error performing login: %s", apiErr.Details)
}
return response, nil
}
func (c *Client) GetPoolByID(poolID string) (params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/pools/%s", c.Config.BaseURL, poolID)
var response params.Pool
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return response, errors.Wrap(decErr, "sending request")
}
return response, fmt.Errorf("error performing login: %s", apiErr.Details)
}
return response, nil
}
func (c *Client) ListAllPools() ([]params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/pools", c.Config.BaseURL)
var response []params.Pool
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return response, errors.Wrap(decErr, "sending request")
}
return response, fmt.Errorf("error performing login: %s", apiErr.Details)
}
return response, nil
}
func (c *Client) DeletePoolByID(poolID string) error {
url := fmt.Sprintf("%s/api/v1/pools/%s", c.Config.BaseURL, poolID)
resp, err := c.client.R().
Delete(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return errors.Wrap(decErr, "sending request")
}
return fmt.Errorf("error deleting pool by ID: %s", apiErr.Details)
}
return nil
}
func (c *Client) UpdatePoolByID(poolID string, param params.UpdatePoolParams) (params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/pools/%s", c.Config.BaseURL, poolID)
var response params.Pool
body, err := json.Marshal(param)
if err != nil {
return response, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Put(url)
if err != nil || resp.IsError() {
apiErr, decErr := c.decodeAPIError(resp.Body())
if decErr != nil {
return response, errors.Wrap(decErr, "sending request")
}
return response, fmt.Errorf("error performing login: %s", apiErr.Details)
}
return response, nil
}

View file

@ -1,179 +0,0 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// 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
//
// 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 client
import (
"encoding/json"
"fmt"
"github.com/cloudbase/garm/params"
)
func (c *Client) ListEnterprises() ([]params.Enterprise, error) {
var enterprises []params.Enterprise
url := fmt.Sprintf("%s/api/v1/enterprises", c.Config.BaseURL)
resp, err := c.client.R().
SetResult(&enterprises).
Get(url)
if err := c.handleError(err, resp); err != nil {
return nil, err
}
return enterprises, nil
}
func (c *Client) CreateEnterprise(param params.CreateEnterpriseParams) (params.Enterprise, error) {
var response params.Enterprise
url := fmt.Sprintf("%s/api/v1/enterprises", c.Config.BaseURL)
body, err := json.Marshal(param)
if err != nil {
return params.Enterprise{}, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Post(url)
if err := c.handleError(err, resp); err != nil {
return params.Enterprise{}, err
}
return response, nil
}
func (c *Client) UpdateEnterprise(enterpriseID string, param params.UpdateEntityParams) (params.Enterprise, error) {
url := fmt.Sprintf("%s/api/v1/enterprises/%s", c.Config.BaseURL, enterpriseID)
var response params.Enterprise
body, err := json.Marshal(param)
if err != nil {
return response, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Put(url)
if err := c.handleError(err, resp); err != nil {
return params.Enterprise{}, err
}
return response, nil
}
func (c *Client) GetEnterprise(enterpriseID string) (params.Enterprise, error) {
var response params.Enterprise
url := fmt.Sprintf("%s/api/v1/enterprises/%s", c.Config.BaseURL, enterpriseID)
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return params.Enterprise{}, err
}
return response, nil
}
func (c *Client) DeleteEnterprise(enterpriseID string) error {
url := fmt.Sprintf("%s/api/v1/enterprises/%s", c.Config.BaseURL, enterpriseID)
resp, err := c.client.R().
Delete(url)
if err := c.handleError(err, resp); err != nil {
return err
}
return nil
}
func (c *Client) CreateEnterprisePool(enterpriseID string, param params.CreatePoolParams) (params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/enterprises/%s/pools", c.Config.BaseURL, enterpriseID)
var response params.Pool
body, err := json.Marshal(param)
if err != nil {
return response, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Post(url)
if err := c.handleError(err, resp); err != nil {
return params.Pool{}, err
}
return response, nil
}
func (c *Client) ListEnterprisePools(enterpriseID string) ([]params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/enterprises/%s/pools", c.Config.BaseURL, enterpriseID)
var response []params.Pool
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return nil, err
}
return response, nil
}
func (c *Client) GetEnterprisePool(enterpriseID, poolID string) (params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/enterprises/%s/pools/%s", c.Config.BaseURL, enterpriseID, poolID)
var response params.Pool
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return params.Pool{}, err
}
return response, nil
}
func (c *Client) DeleteEnterprisePool(enterpriseID, poolID string) error {
url := fmt.Sprintf("%s/api/v1/enterprises/%s/pools/%s", c.Config.BaseURL, enterpriseID, poolID)
resp, err := c.client.R().
Delete(url)
if err := c.handleError(err, resp); err != nil {
return err
}
return nil
}
func (c *Client) UpdateEnterprisePool(enterpriseID, poolID string, param params.UpdatePoolParams) (params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/enterprises/%s/pools/%s", c.Config.BaseURL, enterpriseID, poolID)
var response params.Pool
body, err := json.Marshal(param)
if err != nil {
return response, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Put(url)
if err := c.handleError(err, resp); err != nil {
return params.Pool{}, err
}
return response, nil
}
func (c *Client) ListEnterpriseInstances(enterpriseID string) ([]params.Instance, error) {
url := fmt.Sprintf("%s/api/v1/enterprises/%s/instances", c.Config.BaseURL, enterpriseID)
var response []params.Instance
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return nil, err
}
return response, nil
}

View file

@ -1,196 +0,0 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// 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
//
// 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 client
import (
"encoding/json"
"fmt"
"github.com/cloudbase/garm/params"
)
func (c *Client) ListOrganizations() ([]params.Organization, error) {
var orgs []params.Organization
url := fmt.Sprintf("%s/api/v1/organizations", c.Config.BaseURL)
resp, err := c.client.R().
SetResult(&orgs).
Get(url)
if err := c.handleError(err, resp); err != nil {
return nil, err
}
return orgs, nil
}
func (c *Client) CreateOrganization(param params.CreateOrgParams) (params.Organization, error) {
var response params.Organization
url := fmt.Sprintf("%s/api/v1/organizations", c.Config.BaseURL)
body, err := json.Marshal(param)
if err != nil {
return params.Organization{}, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Post(url)
if err := c.handleError(err, resp); err != nil {
return params.Organization{}, err
}
return response, nil
}
func (c *Client) UpdateOrganization(orgID string, param params.UpdateEntityParams) (params.Organization, error) {
url := fmt.Sprintf("%s/api/v1/organizations/%s", c.Config.BaseURL, orgID)
var response params.Organization
body, err := json.Marshal(param)
if err != nil {
return response, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Put(url)
if err := c.handleError(err, resp); err != nil {
return params.Organization{}, err
}
return response, nil
}
func (c *Client) GetOrganization(orgID string) (params.Organization, error) {
var response params.Organization
url := fmt.Sprintf("%s/api/v1/organizations/%s", c.Config.BaseURL, orgID)
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return params.Organization{}, err
}
return response, nil
}
func (c *Client) DeleteOrganization(orgID string) error {
url := fmt.Sprintf("%s/api/v1/organizations/%s", c.Config.BaseURL, orgID)
resp, err := c.client.R().
Delete(url)
if err := c.handleError(err, resp); err != nil {
return err
}
return nil
}
func (c *Client) CreateOrgPool(orgID string, param params.CreatePoolParams) (params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/organizations/%s/pools", c.Config.BaseURL, orgID)
var response params.Pool
body, err := json.Marshal(param)
if err != nil {
return response, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Post(url)
if err := c.handleError(err, resp); err != nil {
return params.Pool{}, err
}
return response, nil
}
func (c *Client) ListOrgPools(orgID string) ([]params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/organizations/%s/pools", c.Config.BaseURL, orgID)
var response []params.Pool
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return nil, err
}
return response, nil
}
func (c *Client) GetOrgPool(orgID, poolID string) (params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/organizations/%s/pools/%s", c.Config.BaseURL, orgID, poolID)
var response params.Pool
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return params.Pool{}, err
}
return response, nil
}
func (c *Client) DeleteOrgPool(orgID, poolID string) error {
url := fmt.Sprintf("%s/api/v1/organizations/%s/pools/%s", c.Config.BaseURL, orgID, poolID)
resp, err := c.client.R().
Delete(url)
if err := c.handleError(err, resp); err != nil {
return err
}
return nil
}
func (c *Client) UpdateOrgPool(orgID, poolID string, param params.UpdatePoolParams) (params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/organizations/%s/pools/%s", c.Config.BaseURL, orgID, poolID)
var response params.Pool
body, err := json.Marshal(param)
if err != nil {
return response, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Put(url)
if err := c.handleError(err, resp); err != nil {
return params.Pool{}, err
}
return response, nil
}
func (c *Client) ListOrgInstances(orgID string) ([]params.Instance, error) {
url := fmt.Sprintf("%s/api/v1/organizations/%s/instances", c.Config.BaseURL, orgID)
var response []params.Instance
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return nil, err
}
return response, nil
}
func (c *Client) CreateMetricsToken() (string, error) {
url := fmt.Sprintf("%s/api/v1/metrics-token", c.Config.BaseURL)
type response struct {
Token string `json:"token"`
}
var t response
resp, err := c.client.R().
SetResult(&t).
Get(url)
if err := c.handleError(err, resp); err != nil {
return "", err
}
return t.Token, nil
}

View file

@ -1,179 +0,0 @@
// Copyright 2022 Cloudbase Solutions SRL
//
// 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
//
// 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 client
import (
"encoding/json"
"fmt"
"github.com/cloudbase/garm/params"
)
func (c *Client) ListRepositories() ([]params.Repository, error) {
var repos []params.Repository
url := fmt.Sprintf("%s/api/v1/repositories", c.Config.BaseURL)
resp, err := c.client.R().
SetResult(&repos).
Get(url)
if err := c.handleError(err, resp); err != nil {
return nil, err
}
return repos, nil
}
func (c *Client) CreateRepository(param params.CreateRepoParams) (params.Repository, error) {
var response params.Repository
url := fmt.Sprintf("%s/api/v1/repositories", c.Config.BaseURL)
body, err := json.Marshal(param)
if err != nil {
return params.Repository{}, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Post(url)
if err := c.handleError(err, resp); err != nil {
return params.Repository{}, err
}
return response, nil
}
func (c *Client) GetRepository(repoID string) (params.Repository, error) {
var response params.Repository
url := fmt.Sprintf("%s/api/v1/repositories/%s", c.Config.BaseURL, repoID)
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return params.Repository{}, err
}
return response, nil
}
func (c *Client) DeleteRepository(repoID string) error {
url := fmt.Sprintf("%s/api/v1/repositories/%s", c.Config.BaseURL, repoID)
resp, err := c.client.R().
Delete(url)
if err := c.handleError(err, resp); err != nil {
return err
}
return nil
}
func (c *Client) CreateRepoPool(repoID string, param params.CreatePoolParams) (params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/repositories/%s/pools", c.Config.BaseURL, repoID)
var response params.Pool
body, err := json.Marshal(param)
if err != nil {
return response, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Post(url)
if err := c.handleError(err, resp); err != nil {
return params.Pool{}, err
}
return response, nil
}
func (c *Client) ListRepoPools(repoID string) ([]params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/repositories/%s/pools", c.Config.BaseURL, repoID)
var response []params.Pool
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return nil, err
}
return response, nil
}
func (c *Client) GetRepoPool(repoID, poolID string) (params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/repositories/%s/pools/%s", c.Config.BaseURL, repoID, poolID)
var response params.Pool
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return params.Pool{}, err
}
return response, nil
}
func (c *Client) DeleteRepoPool(repoID, poolID string) error {
url := fmt.Sprintf("%s/api/v1/repositories/%s/pools/%s", c.Config.BaseURL, repoID, poolID)
resp, err := c.client.R().
Delete(url)
if err := c.handleError(err, resp); err != nil {
return err
}
return nil
}
func (c *Client) UpdateRepoPool(repoID, poolID string, param params.UpdatePoolParams) (params.Pool, error) {
url := fmt.Sprintf("%s/api/v1/repositories/%s/pools/%s", c.Config.BaseURL, repoID, poolID)
var response params.Pool
body, err := json.Marshal(param)
if err != nil {
return response, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Put(url)
if err := c.handleError(err, resp); err != nil {
return params.Pool{}, err
}
return response, nil
}
func (c *Client) UpdateRepo(repoID string, param params.UpdateEntityParams) (params.Repository, error) {
url := fmt.Sprintf("%s/api/v1/repositories/%s", c.Config.BaseURL, repoID)
var response params.Repository
body, err := json.Marshal(param)
if err != nil {
return response, err
}
resp, err := c.client.R().
SetBody(body).
SetResult(&response).
Put(url)
if err := c.handleError(err, resp); err != nil {
return params.Repository{}, err
}
return response, nil
}
func (c *Client) ListRepoInstances(repoID string) ([]params.Instance, error) {
url := fmt.Sprintf("%s/api/v1/repositories/%s/instances", c.Config.BaseURL, repoID)
var response []params.Instance
resp, err := c.client.R().
SetResult(&response).
Get(url)
if err := c.handleError(err, resp); err != nil {
return nil, err
}
return response, nil
}

View file

@ -17,6 +17,7 @@ package cmd
import (
"fmt"
apiClientCreds "github.com/cloudbase/garm/client/credentials"
"github.com/cloudbase/garm/params"
"github.com/jedib0t/go-pretty/v6/table"
@ -50,11 +51,12 @@ func init() {
return errNeedsInitError
}
creds, err := cli.ListCredentials()
listCredsReq := apiClientCreds.NewListCredentialsParams()
response, err := apiCli.Credentials.ListCredentials(listCredsReq, authToken)
if err != nil {
return err
}
formatGithubCredentials(creds)
formatGithubCredentials(response.Payload)
return nil
},
})

View file

@ -17,6 +17,7 @@ package cmd
import (
"fmt"
apiClientEnterprises "github.com/cloudbase/garm/client/enterprises"
"github.com/cloudbase/garm/params"
"github.com/jedib0t/go-pretty/v6/table"
@ -54,16 +55,17 @@ var enterpriseAddCmd = &cobra.Command{
return errNeedsInitError
}
newEnterpriseReq := params.CreateEnterpriseParams{
newEnterpriseReq := apiClientEnterprises.NewCreateEnterpriseParams()
newEnterpriseReq.Body = params.CreateEnterpriseParams{
Name: enterpriseName,
WebhookSecret: enterpriseWebhookSecret,
CredentialsName: enterpriseCreds,
}
enterprise, err := cli.CreateEnterprise(newEnterpriseReq)
response, err := apiCli.Enterprises.CreateEnterprise(newEnterpriseReq, authToken)
if err != nil {
return err
}
formatOneEnterprise(enterprise)
formatOneEnterprise(response.Payload)
return nil
},
}
@ -79,11 +81,12 @@ var enterpriseListCmd = &cobra.Command{
return errNeedsInitError
}
enterprises, err := cli.ListEnterprises()
listEnterprisesReq := apiClientEnterprises.NewListEnterprisesParams()
response, err := apiCli.Enterprises.ListEnterprises(listEnterprisesReq, authToken)
if err != nil {
return err
}
formatEnterprises(enterprises)
formatEnterprises(response.Payload)
return nil
},
}
@ -103,11 +106,13 @@ var enterpriseShowCmd = &cobra.Command{
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
enterprise, err := cli.GetEnterprise(args[0])
showEnterpriseReq := apiClientEnterprises.NewGetEnterpriseParams()
showEnterpriseReq.EnterpriseID = args[0]
response, err := apiCli.Enterprises.GetEnterprise(showEnterpriseReq, authToken)
if err != nil {
return err
}
formatOneEnterprise(enterprise)
formatOneEnterprise(response.Payload)
return nil
},
}
@ -128,7 +133,9 @@ var enterpriseDeleteCmd = &cobra.Command{
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
if err := cli.DeleteEnterprise(args[0]); err != nil {
deleteEnterpriseReq := apiClientEnterprises.NewDeleteEnterpriseParams()
deleteEnterpriseReq.EnterpriseID = args[0]
if err := apiCli.Enterprises.DeleteEnterprise(deleteEnterpriseReq, authToken); err != nil {
return err
}
return nil
@ -152,16 +159,17 @@ var enterpriseUpdateCmd = &cobra.Command{
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
enterpriseUpdateReq := params.UpdateEntityParams{
updateEnterpriseReq := apiClientEnterprises.NewUpdateEnterpriseParams()
updateEnterpriseReq.Body = params.UpdateEntityParams{
WebhookSecret: repoWebhookSecret,
CredentialsName: repoCreds,
}
enterprise, err := cli.UpdateEnterprise(args[0], enterpriseUpdateReq)
updateEnterpriseReq.EnterpriseID = args[0]
response, err := apiCli.Enterprises.UpdateEnterprise(updateEnterpriseReq, authToken)
if err != nil {
return err
}
formatOneEnterprise(enterprise)
formatOneEnterprise(response.Payload)
return nil
},
}

View file

@ -22,6 +22,8 @@ import (
"github.com/cloudbase/garm/cmd/garm-cli/config"
"github.com/cloudbase/garm/params"
apiClientFirstRun "github.com/cloudbase/garm/client/first_run"
apiClientLogin "github.com/cloudbase/garm/client/login"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -54,7 +56,8 @@ garm-cli init --name=dev --url=https://runner.example.com --username=admin --pas
return err
}
newUser := params.NewUserParams{
newUserReq := apiClientFirstRun.NewFirstRunParams()
newUserReq.Body = params.NewUserParams{
Username: loginUserName,
Password: loginPassword,
FullName: loginFullName,
@ -62,17 +65,21 @@ garm-cli init --name=dev --url=https://runner.example.com --username=admin --pas
}
url := strings.TrimSuffix(loginURL, "/")
response, err := cli.InitManager(url, newUser)
initApiClient(url, "")
response, err := apiCli.FirstRun.FirstRun(newUserReq, authToken)
if err != nil {
return errors.Wrap(err, "initializing manager")
}
loginParams := params.PasswordLoginParams{
newLoginParamsReq := apiClientLogin.NewLoginParams()
newLoginParamsReq.Body = params.PasswordLoginParams{
Username: loginUserName,
Password: loginPassword,
}
token, err := cli.Login(url, loginParams)
token, err := apiCli.Login.Login(newLoginParamsReq, authToken)
if err != nil {
return errors.Wrap(err, "authenticating")
}
@ -80,7 +87,7 @@ garm-cli init --name=dev --url=https://runner.example.com --username=admin --pas
cfg.Managers = append(cfg.Managers, config.Manager{
Name: loginProfileName,
BaseURL: url,
Token: token,
Token: token.Payload.Token,
})
cfg.ActiveManager = loginProfileName
@ -89,7 +96,7 @@ garm-cli init --name=dev --url=https://runner.example.com --username=admin --pas
return errors.Wrap(err, "saving config")
}
renderUserTable(response)
renderUserTable(response.Payload)
return nil
},
}

View file

@ -18,6 +18,7 @@ import (
"fmt"
"strings"
apiClientJobs "github.com/cloudbase/garm/client/jobs"
"github.com/cloudbase/garm/params"
"github.com/google/uuid"
"github.com/jedib0t/go-pretty/v6/table"
@ -44,11 +45,12 @@ var jobsListCmd = &cobra.Command{
return errNeedsInitError
}
jobs, err := cli.ListAllJobs()
listJobsReq := apiClientJobs.NewListJobsParams()
response, err := apiCli.Jobs.ListJobs(listJobsReq, authToken)
if err != nil {
return err
}
formatJobs(jobs)
formatJobs(response.Payload)
return nil
},
}

View file

@ -17,6 +17,7 @@ package cmd
import (
"fmt"
apiClientMetricToken "github.com/cloudbase/garm/client/metrics_token"
"github.com/spf13/cobra"
)
@ -39,11 +40,12 @@ var metricsTokenCreateCmd = &cobra.Command{
return errNeedsInitError
}
token, err := cli.CreateMetricsToken()
showMetricsTokenReq := apiClientMetricToken.NewGetMetricsTokenParams()
response, err := apiCli.MetricsToken.GetMetricsToken(showMetricsTokenReq, authToken)
if err != nil {
return err
}
fmt.Println(token)
fmt.Println(response.Payload.Token)
return nil
},

View file

@ -17,6 +17,7 @@ package cmd
import (
"fmt"
apiClientOrgs "github.com/cloudbase/garm/client/organizations"
"github.com/cloudbase/garm/params"
"github.com/jedib0t/go-pretty/v6/table"
@ -54,16 +55,17 @@ var orgAddCmd = &cobra.Command{
return errNeedsInitError
}
newOrgReq := params.CreateOrgParams{
newOrgReq := apiClientOrgs.NewCreateOrgParams()
newOrgReq.Body = params.CreateOrgParams{
Name: orgName,
WebhookSecret: orgWebhookSecret,
CredentialsName: orgCreds,
}
org, err := cli.CreateOrganization(newOrgReq)
response, err := apiCli.Organizations.CreateOrg(newOrgReq, authToken)
if err != nil {
return err
}
formatOneOrganization(org)
formatOneOrganization(response.Payload)
return nil
},
}
@ -85,16 +87,17 @@ var orgUpdateCmd = &cobra.Command{
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
orgUpdateReq := params.UpdateEntityParams{
updateOrgReq := apiClientOrgs.NewUpdateOrgParams()
updateOrgReq.Body = params.UpdateEntityParams{
WebhookSecret: repoWebhookSecret,
CredentialsName: orgCreds,
}
org, err := cli.UpdateOrganization(args[0], orgUpdateReq)
updateOrgReq.OrgID = args[0]
response, err := apiCli.Organizations.UpdateOrg(updateOrgReq, authToken)
if err != nil {
return err
}
formatOneOrganization(org)
formatOneOrganization(response.Payload)
return nil
},
}
@ -110,11 +113,12 @@ var orgListCmd = &cobra.Command{
return errNeedsInitError
}
orgs, err := cli.ListOrganizations()
listOrgsReq := apiClientOrgs.NewListOrgsParams()
response, err := apiCli.Organizations.ListOrgs(listOrgsReq, authToken)
if err != nil {
return err
}
formatOrganizations(orgs)
formatOrganizations(response.Payload)
return nil
},
}
@ -134,11 +138,13 @@ var orgShowCmd = &cobra.Command{
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
org, err := cli.GetOrganization(args[0])
showOrgReq := apiClientOrgs.NewGetOrgParams()
showOrgReq.OrgID = args[0]
response, err := apiCli.Organizations.GetOrg(showOrgReq, authToken)
if err != nil {
return err
}
formatOneOrganization(org)
formatOneOrganization(response.Payload)
return nil
},
}
@ -159,7 +165,9 @@ var orgDeleteCmd = &cobra.Command{
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
if err := cli.DeleteOrganization(args[0]); err != nil {
deleteOrgReq := apiClientOrgs.NewDeleteOrgParams()
deleteOrgReq.OrgID = args[0]
if err := apiCli.Organizations.DeleteOrg(deleteOrgReq, authToken); err != nil {
return err
}
return nil

View file

@ -20,6 +20,11 @@ import (
"os"
"strings"
apiClientEnterprises "github.com/cloudbase/garm/client/enterprises"
apiClientOrgs "github.com/cloudbase/garm/client/organizations"
apiClientPools "github.com/cloudbase/garm/client/pools"
apiClientRepos "github.com/cloudbase/garm/client/repositories"
"github.com/cloudbase/garm/params"
"github.com/jedib0t/go-pretty/v6/table"
@ -96,13 +101,28 @@ Example:
switch len(args) {
case 0:
if cmd.Flags().Changed("repo") {
pools, err = cli.ListRepoPools(poolRepository)
var response *apiClientRepos.ListRepoPoolsOK
listRepoPoolsReq := apiClientRepos.NewListRepoPoolsParams()
listRepoPoolsReq.RepoID = poolRepository
response, err = apiCli.Repositories.ListRepoPools(listRepoPoolsReq, authToken)
pools = response.Payload
} else if cmd.Flags().Changed("org") {
pools, err = cli.ListOrgPools(poolOrganization)
var response *apiClientOrgs.ListOrgPoolsOK
listOrgPoolsReq := apiClientOrgs.NewListOrgPoolsParams()
listOrgPoolsReq.OrgID = poolOrganization
response, err = apiCli.Organizations.ListOrgPools(listOrgPoolsReq, authToken)
pools = response.Payload
} else if cmd.Flags().Changed("enterprise") {
pools, err = cli.ListEnterprisePools(poolEnterprise)
var response *apiClientEnterprises.ListEnterprisePoolsOK
listEnterprisePoolsReq := apiClientEnterprises.NewListEnterprisePoolsParams()
listEnterprisePoolsReq.EnterpriseID = poolEnterprise
response, err = apiCli.Enterprises.ListEnterprisePools(listEnterprisePoolsReq, authToken)
pools = response.Payload
} else if cmd.Flags().Changed("all") {
pools, err = cli.ListAllPools()
var response *apiClientPools.ListPoolsOK
listPoolsReq := apiClientPools.NewListPoolsParams()
response, err = apiCli.Pools.ListPools(listPoolsReq, authToken)
pools = response.Payload
} else {
cmd.Help() //nolint
os.Exit(0)
@ -138,11 +158,13 @@ var poolShowCmd = &cobra.Command{
return fmt.Errorf("too many arguments")
}
pool, err := cli.GetPoolByID(args[0])
getPoolReq := apiClientPools.NewGetPoolParams()
getPoolReq.PoolID = args[0]
response, err := apiCli.Pools.GetPool(getPoolReq, authToken)
if err != nil {
return err
}
formatOnePool(pool)
formatOnePool(response.Payload)
return nil
},
}
@ -166,7 +188,9 @@ var poolDeleteCmd = &cobra.Command{
return fmt.Errorf("too many arguments")
}
if err := cli.DeletePoolByID(args[0]); err != nil {
deletePoolReq := apiClientPools.NewDeletePoolParams()
deletePoolReq.PoolID = args[0]
if err := apiCli.Pools.DeletePool(deletePoolReq, authToken); err != nil {
return err
}
return nil
@ -226,11 +250,26 @@ var poolAddCmd = &cobra.Command{
var err error
if cmd.Flags().Changed("repo") {
pool, err = cli.CreateRepoPool(poolRepository, newPoolParams)
var response *apiClientRepos.CreateRepoPoolOK
newRepoPoolReq := apiClientRepos.NewCreateRepoPoolParams()
newRepoPoolReq.RepoID = poolRepository
newRepoPoolReq.Body = newPoolParams
response, err = apiCli.Repositories.CreateRepoPool(newRepoPoolReq, authToken)
pool = response.Payload
} else if cmd.Flags().Changed("org") {
pool, err = cli.CreateOrgPool(poolOrganization, newPoolParams)
var response *apiClientOrgs.CreateOrgPoolOK
newOrgPoolReq := apiClientOrgs.NewCreateOrgPoolParams()
newOrgPoolReq.OrgID = poolOrganization
newOrgPoolReq.Body = newPoolParams
response, err = apiCli.Organizations.CreateOrgPool(newOrgPoolReq, authToken)
pool = response.Payload
} else if cmd.Flags().Changed("enterprise") {
pool, err = cli.CreateEnterprisePool(poolEnterprise, newPoolParams)
var response *apiClientEnterprises.CreateEnterprisePoolOK
newEnterprisePoolReq := apiClientEnterprises.NewCreateEnterprisePoolParams()
newEnterprisePoolReq.EnterpriseID = poolEnterprise
newEnterprisePoolReq.Body = newPoolParams
response, err = apiCli.Enterprises.CreateEnterprisePool(newEnterprisePoolReq, authToken)
pool = response.Payload
} else {
cmd.Help() //nolint
os.Exit(0)
@ -267,6 +306,7 @@ explicitly remove them using the runner delete command.
return fmt.Errorf("too many arguments")
}
updatePoolReq := apiClientPools.NewUpdatePoolParams()
poolUpdateParams := params.UpdatePoolParams{}
if cmd.Flags().Changed("image") {
@ -331,12 +371,14 @@ explicitly remove them using the runner delete command.
poolUpdateParams.ExtraSpecs = data
}
pool, err := cli.UpdatePoolByID(args[0], poolUpdateParams)
updatePoolReq.PoolID = args[0]
updatePoolReq.Body = poolUpdateParams
response, err := apiCli.Pools.UpdatePool(updatePoolReq, authToken)
if err != nil {
return err
}
formatOnePool(pool)
formatOnePool(response.Payload)
return nil
},
}

View file

@ -18,6 +18,7 @@ import (
"fmt"
"strings"
apiClientLogin "github.com/cloudbase/garm/client/login"
"github.com/cloudbase/garm/cmd/garm-cli/common"
"github.com/cloudbase/garm/cmd/garm-cli/config"
"github.com/cloudbase/garm/params"
@ -143,12 +144,15 @@ var profileAddCmd = &cobra.Command{
}
url := strings.TrimSuffix(loginURL, "/")
loginParams := params.PasswordLoginParams{
initApiClient(url, "")
newLoginParamsReq := apiClientLogin.NewLoginParams()
newLoginParamsReq.Body = params.PasswordLoginParams{
Username: loginUserName,
Password: loginPassword,
}
resp, err := cli.Login(url, loginParams)
resp, err := apiCli.Login.Login(newLoginParamsReq, authToken)
if err != nil {
return err
}
@ -156,7 +160,7 @@ var profileAddCmd = &cobra.Command{
cfg.Managers = append(cfg.Managers, config.Manager{
Name: loginProfileName,
BaseURL: url,
Token: resp,
Token: resp.Payload.Token,
})
cfg.ActiveManager = loginProfileName
@ -190,16 +194,17 @@ installation, by performing a login.
return err
}
loginParams := params.PasswordLoginParams{
newLoginParamsReq := apiClientLogin.NewLoginParams()
newLoginParamsReq.Body = params.PasswordLoginParams{
Username: loginUserName,
Password: loginPassword,
}
resp, err := cli.Login(mgr.BaseURL, loginParams)
resp, err := apiCli.Login.Login(newLoginParamsReq, authToken)
if err != nil {
return err
}
if err := cfg.SetManagerToken(mgr.Name, resp); err != nil {
if err := cfg.SetManagerToken(mgr.Name, resp.Payload.Token); err != nil {
return fmt.Errorf("error saving new token: %s", err)
}

View file

@ -17,6 +17,7 @@ package cmd
import (
"fmt"
apiClientProviders "github.com/cloudbase/garm/client/providers"
"github.com/cloudbase/garm/params"
"github.com/jedib0t/go-pretty/v6/table"
@ -49,11 +50,12 @@ func init() {
return errNeedsInitError
}
providers, err := cli.ListProviders()
listProvidersReq := apiClientProviders.NewListProvidersParams()
response, err := apiCli.Providers.ListProviders(listProvidersReq, authToken)
if err != nil {
return err
}
formatProviders(providers)
formatProviders(response.Payload)
return nil
},
})

View file

@ -17,6 +17,7 @@ package cmd
import (
"fmt"
apiClientRepos "github.com/cloudbase/garm/client/repositories"
"github.com/cloudbase/garm/params"
"github.com/jedib0t/go-pretty/v6/table"
@ -55,17 +56,18 @@ var repoAddCmd = &cobra.Command{
return errNeedsInitError
}
newRepoReq := params.CreateRepoParams{
newRepoReq := apiClientRepos.NewCreateRepoParams()
newRepoReq.Body = params.CreateRepoParams{
Owner: repoOwner,
Name: repoName,
WebhookSecret: repoWebhookSecret,
CredentialsName: repoCreds,
}
repo, err := cli.CreateRepository(newRepoReq)
response, err := apiCli.Repositories.CreateRepo(newRepoReq, authToken)
if err != nil {
return err
}
formatOneRepository(repo)
formatOneRepository(response.Payload)
return nil
},
}
@ -81,11 +83,12 @@ var repoListCmd = &cobra.Command{
return errNeedsInitError
}
repos, err := cli.ListRepositories()
listReposReq := apiClientRepos.NewListReposParams()
response, err := apiCli.Repositories.ListRepos(listReposReq, authToken)
if err != nil {
return err
}
formatRepositories(repos)
formatRepositories(response.Payload)
return nil
},
}
@ -107,16 +110,18 @@ var repoUpdateCmd = &cobra.Command{
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
repoUpdateReq := params.UpdateEntityParams{
updateReposReq := apiClientRepos.NewUpdateRepoParams()
updateReposReq.Body = params.UpdateEntityParams{
WebhookSecret: repoWebhookSecret,
CredentialsName: repoCreds,
}
repo, err := cli.UpdateRepo(args[0], repoUpdateReq)
updateReposReq.RepoID = args[0]
response, err := apiCli.Repositories.UpdateRepo(updateReposReq, authToken)
if err != nil {
return err
}
formatOneRepository(repo)
formatOneRepository(response.Payload)
return nil
},
}
@ -136,11 +141,13 @@ var repoShowCmd = &cobra.Command{
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
repo, err := cli.GetRepository(args[0])
showRepoReq := apiClientRepos.NewGetRepoParams()
showRepoReq.RepoID = args[0]
response, err := apiCli.Repositories.GetRepo(showRepoReq, authToken)
if err != nil {
return err
}
formatOneRepository(repo)
formatOneRepository(response.Payload)
return nil
},
}
@ -161,7 +168,9 @@ var repoDeleteCmd = &cobra.Command{
if len(args) > 1 {
return fmt.Errorf("too many arguments")
}
if err := cli.DeleteRepository(args[0]); err != nil {
deleteRepoReq := apiClientRepos.NewDeleteRepoParams()
deleteRepoReq.RepoID = args[0]
if err := apiCli.Repositories.DeleteRepo(deleteRepoReq, authToken); err != nil {
return err
}
return nil

View file

@ -16,11 +16,14 @@ package cmd
import (
"fmt"
"net/url"
"os"
"github.com/cloudbase/garm/cmd/garm-cli/client"
apiClient "github.com/cloudbase/garm/client"
"github.com/cloudbase/garm/cmd/garm-cli/config"
"github.com/go-openapi/runtime"
openapiRuntimeClient "github.com/go-openapi/runtime/client"
"github.com/spf13/cobra"
)
@ -29,8 +32,8 @@ var Version string
var (
cfg *config.Config
mgr config.Manager
cli *client.Client
active string
apiCli *apiClient.GarmAPI
authToken runtime.ClientAuthInfoWriter
needsInit bool
debug bool
errNeedsInitError = fmt.Errorf("please log into a garm installation first")
@ -55,6 +58,28 @@ func Execute() {
}
}
func initApiClient(baseUrl, token string) {
baseUrlParsed, err := url.Parse(baseUrl)
if err != nil {
fmt.Printf("Failed to parse base url %s: %s", baseUrl, err)
os.Exit(1)
}
apiPath, err := url.JoinPath(baseUrlParsed.Path, apiClient.DefaultBasePath)
if err != nil {
fmt.Printf("Failed to join base url path %s with %s: %s", baseUrlParsed.Path, apiClient.DefaultBasePath, err)
os.Exit(1)
}
if debug {
os.Setenv("SWAGGER_DEBUG", "true")
}
transportCfg := apiClient.DefaultTransportConfig().
WithHost(baseUrlParsed.Host).
WithBasePath(apiPath).
WithSchemes([]string{baseUrlParsed.Scheme})
apiCli = apiClient.NewHTTPClientWithConfig(nil, transportCfg)
authToken = openapiRuntimeClient.BearerToken(token)
}
func initConfig() {
var err error
cfg, err = config.LoadConfig()
@ -70,7 +95,6 @@ func initConfig() {
if err != nil {
mgr = cfg.Managers[0]
}
active = mgr.Name
}
cli = client.NewClient(active, mgr, debug)
initApiClient(mgr.BaseURL, mgr.Token)
}

View file

@ -20,6 +20,10 @@ import (
"github.com/cloudbase/garm/params"
apiClientEnterprises "github.com/cloudbase/garm/client/enterprises"
apiClientInstances "github.com/cloudbase/garm/client/instances"
apiClientOrgs "github.com/cloudbase/garm/client/organizations"
apiClientRepos "github.com/cloudbase/garm/client/repositories"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)
@ -88,16 +92,35 @@ Example:
return fmt.Errorf("specifying a pool ID and any of [all org repo enterprise] are mutually exclusive")
}
instances, err = cli.ListPoolInstances(args[0])
var response *apiClientInstances.ListPoolInstancesOK
listPoolInstancesReq := apiClientInstances.NewListPoolInstancesParams()
listPoolInstancesReq.PoolID = args[0]
response, err = apiCli.Instances.ListPoolInstances(listPoolInstancesReq, authToken)
instances = response.Payload
case 0:
if cmd.Flags().Changed("repo") {
instances, err = cli.ListRepoInstances(runnerRepository)
var response *apiClientRepos.ListRepoInstancesOK
listRepoInstancesReq := apiClientRepos.NewListRepoInstancesParams()
listRepoInstancesReq.RepoID = runnerRepository
response, err = apiCli.Repositories.ListRepoInstances(listRepoInstancesReq, authToken)
instances = response.Payload
} else if cmd.Flags().Changed("org") {
instances, err = cli.ListOrgInstances(runnerOrganization)
var response *apiClientOrgs.ListOrgInstancesOK
listOrgInstancesReq := apiClientOrgs.NewListOrgInstancesParams()
listOrgInstancesReq.OrgID = runnerOrganization
response, err = apiCli.Organizations.ListOrgInstances(listOrgInstancesReq, authToken)
instances = response.Payload
} else if cmd.Flags().Changed("enterprise") {
instances, err = cli.ListEnterpriseInstances(runnerEnterprise)
var response *apiClientEnterprises.ListEnterpriseInstancesOK
listEnterpriseInstancesReq := apiClientEnterprises.NewListEnterpriseInstancesParams()
listEnterpriseInstancesReq.EnterpriseID = runnerEnterprise
response, err = apiCli.Enterprises.ListEnterpriseInstances(listEnterpriseInstancesReq, authToken)
instances = response.Payload
} else if cmd.Flags().Changed("all") {
instances, err = cli.ListAllInstances()
var response *apiClientInstances.ListInstancesOK
listInstancesReq := apiClientInstances.NewListInstancesParams()
response, err = apiCli.Instances.ListInstances(listInstancesReq, authToken)
instances = response.Payload
} else {
cmd.Help() //nolint
os.Exit(0)
@ -133,11 +156,13 @@ var runnerShowCmd = &cobra.Command{
return fmt.Errorf("too many arguments")
}
instance, err := cli.GetInstanceByName(args[0])
showInstanceReq := apiClientInstances.NewGetInstanceParams()
showInstanceReq.InstanceName = args[0]
response, err := apiCli.Instances.GetInstance(showInstanceReq, authToken)
if err != nil {
return err
}
formatSingleInstance(instance)
formatSingleInstance(response.Payload)
return nil
},
}
@ -170,7 +195,9 @@ to either cancel the workflow or wait for it to finish.
return fmt.Errorf("use --force-remove-runner=true to remove a runner")
}
if err := cli.DeleteRunner(args[0]); err != nil {
deleteInstanceReq := apiClientInstances.NewDeleteInstanceParams()
deleteInstanceReq.InstanceName = args[0]
if err := apiCli.Instances.DeleteInstance(deleteInstanceReq, authToken); err != nil {
return err
}
return nil

1
go.mod
View file

@ -8,7 +8,6 @@ require (
github.com/go-openapi/errors v0.20.4
github.com/go-openapi/runtime v0.26.0
github.com/go-openapi/strfmt v0.21.7
github.com/go-resty/resty/v2 v2.7.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-github/v53 v53.2.0
github.com/google/uuid v1.3.0

3
go.sum
View file

@ -92,8 +92,6 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU=
github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -369,7 +367,6 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=

View file

@ -1,30 +0,0 @@
# 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
*.test
*.prof
coverage.out
coverage.txt
# Exclude intellij IDE folders
.idea/*

View file

@ -1,48 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
load("@bazel_gazelle//:def.bzl", "gazelle")
# gazelle:prefix github.com/go-resty/resty/v2
# gazelle:go_naming_convention import_alias
gazelle(name = "gazelle")
go_library(
name = "resty",
srcs = [
"client.go",
"middleware.go",
"redirect.go",
"request.go",
"response.go",
"resty.go",
"retry.go",
"trace.go",
"transport.go",
"transport112.go",
"util.go",
],
importpath = "github.com/go-resty/resty/v2",
visibility = ["//visibility:public"],
deps = ["@org_golang_x_net//publicsuffix:go_default_library"],
)
go_test(
name = "resty_test",
srcs = [
"client_test.go",
"context_test.go",
"example_test.go",
"request_test.go",
"resty_test.go",
"retry_test.go",
"util_test.go",
],
data = glob([".testdata/*"]),
embed = [":resty"],
deps = ["@org_golang_x_net//proxy:go_default_library"],
)
alias(
name = "go_default_library",
actual = ":resty",
visibility = ["//visibility:public"],
)

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015-2021 Jeevanandam M., https://myjeeva.com <jeeva@myjeeva.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,906 +0,0 @@
<p align="center">
<h1 align="center">Resty</h1>
<p align="center">Simple HTTP and REST client library for Go (inspired by Ruby rest-client)</p>
<p align="center"><a href="#features">Features</a> section describes in detail about Resty capabilities</p>
</p>
<p align="center">
<p align="center"><a href="https://github.com/go-resty/resty/actions/workflows/ci.yml?query=branch%3Amaster"><img src="https://github.com/go-resty/resty/actions/workflows/ci.yml/badge.svg" alt="Build Status"></a> <a href="https://codecov.io/gh/go-resty/resty/branch/master"><img src="https://codecov.io/gh/go-resty/resty/branch/master/graph/badge.svg" alt="Code Coverage"></a> <a href="https://goreportcard.com/report/go-resty/resty"><img src="https://goreportcard.com/badge/go-resty/resty" alt="Go Report Card"></a> <a href="https://github.com/go-resty/resty/releases/latest"><img src="https://img.shields.io/badge/version-2.7.0-blue.svg" alt="Release Version"></a> <a href="https://pkg.go.dev/github.com/go-resty/resty/v2"><img src="https://pkg.go.dev/badge/github.com/go-resty/resty" alt="GoDoc"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/go-resty/resty.svg" alt="License"></a> <a href="https://github.com/avelino/awesome-go"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Go"></a></p>
</p>
<p align="center">
<h4 align="center">Resty Communication Channels</h4>
<p align="center"><a href="https://gitter.im/go_resty/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/go_resty/community.svg" alt="Chat on Gitter - Resty Community"></a> <a href="https://twitter.com/go_resty"><img src="https://img.shields.io/badge/twitter-@go__resty-55acee.svg" alt="Twitter @go_resty"></a></p>
</p>
## News
* v2.7.0 [released](https://github.com/go-resty/resty/releases/tag/v2.7.0) and tagged on Nov 03, 2021.
* v2.0.0 [released](https://github.com/go-resty/resty/releases/tag/v2.0.0) and tagged on Jul 16, 2019.
* v1.12.0 [released](https://github.com/go-resty/resty/releases/tag/v1.12.0) and tagged on Feb 27, 2019.
* v1.0 released and tagged on Sep 25, 2017. - Resty's first version was released on Sep 15, 2015 then it grew gradually as a very handy and helpful library. Its been a two years since first release. I'm very thankful to Resty users and its [contributors](https://github.com/go-resty/resty/graphs/contributors).
## Features
* GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS, etc.
* Simple and chainable methods for settings and request
* [Request](https://pkg.go.dev/github.com/go-resty/resty/v2#Request) Body can be `string`, `[]byte`, `struct`, `map`, `slice` and `io.Reader` too
* Auto detects `Content-Type`
* Buffer less processing for `io.Reader`
* Native `*http.Request` instance may be accessed during middleware and request execution via `Request.RawRequest`
* Request Body can be read multiple times via `Request.RawRequest.GetBody()`
* [Response](https://pkg.go.dev/github.com/go-resty/resty/v2#Response) object gives you more possibility
* Access as `[]byte` array - `response.Body()` OR Access as `string` - `response.String()`
* Know your `response.Time()` and when we `response.ReceivedAt()`
* Automatic marshal and unmarshal for `JSON` and `XML` content type
* Default is `JSON`, if you supply `struct/map` without header `Content-Type`
* For auto-unmarshal, refer to -
- Success scenario [Request.SetResult()](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetResult) and [Response.Result()](https://pkg.go.dev/github.com/go-resty/resty/v2#Response.Result).
- Error scenario [Request.SetError()](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetError) and [Response.Error()](https://pkg.go.dev/github.com/go-resty/resty/v2#Response.Error).
- Supports [RFC7807](https://tools.ietf.org/html/rfc7807) - `application/problem+json` & `application/problem+xml`
* Resty provides an option to override [JSON Marshal/Unmarshal and XML Marshal/Unmarshal](#override-json--xml-marshalunmarshal)
* Easy to upload one or more file(s) via `multipart/form-data`
* Auto detects file content type
* Request URL [Path Params (aka URI Params)](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetPathParams)
* Backoff Retry Mechanism with retry condition function [reference](retry_test.go)
* Resty client HTTP & REST [Request](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.OnBeforeRequest) and [Response](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.OnAfterResponse) middlewares
* `Request.SetContext` supported
* Authorization option of `BasicAuth` and `Bearer` token
* Set request `ContentLength` value for all request or particular request
* Custom [Root Certificates](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetRootCertificate) and Client [Certificates](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetCertificates)
* Download/Save HTTP response directly into File, like `curl -o` flag. See [SetOutputDirectory](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetOutputDirectory) & [SetOutput](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetOutput).
* Cookies for your request and CookieJar support
* SRV Record based request instead of Host URL
* Client settings like `Timeout`, `RedirectPolicy`, `Proxy`, `TLSClientConfig`, `Transport`, etc.
* Optionally allows GET request with payload, see [SetAllowGetMethodPayload](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetAllowGetMethodPayload)
* Supports registering external JSON library into resty, see [how to use](https://github.com/go-resty/resty/issues/76#issuecomment-314015250)
* Exposes Response reader without reading response (no auto-unmarshaling) if need be, see [how to use](https://github.com/go-resty/resty/issues/87#issuecomment-322100604)
* Option to specify expected `Content-Type` when response `Content-Type` header missing. Refer to [#92](https://github.com/go-resty/resty/issues/92)
* Resty design
* Have client level settings & options and also override at Request level if you want to
* Request and Response middleware
* Create Multiple clients if you want to `resty.New()`
* Supports `http.RoundTripper` implementation, see [SetTransport](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetTransport)
* goroutine concurrent safe
* Resty Client trace, see [Client.EnableTrace](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.EnableTrace) and [Request.EnableTrace](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.EnableTrace)
* Since v2.4.0, trace info contains a `RequestAttempt` value, and the `Request` object contains an `Attempt` attribute
* Debug mode - clean and informative logging presentation
* Gzip - Go does it automatically also resty has fallback handling too
* Works fine with `HTTP/2` and `HTTP/1.1`
* [Bazel support](#bazel-support)
* Easily mock Resty for testing, [for e.g.](#mocking-http-requests-using-httpmock-library)
* Well tested client library
### Included Batteries
* Redirect Policies - see [how to use](#redirect-policy)
* NoRedirectPolicy
* FlexibleRedirectPolicy
* DomainCheckRedirectPolicy
* etc. [more info](redirect.go)
* Retry Mechanism [how to use](#retries)
* Backoff Retry
* Conditional Retry
* Since v2.6.0, Retry Hooks - [Client](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.AddRetryHook), [Request](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.AddRetryHook)
* SRV Record based request instead of Host URL [how to use](resty_test.go#L1412)
* etc (upcoming - throw your idea's [here](https://github.com/go-resty/resty/issues)).
#### Supported Go Versions
Initially Resty started supporting `go modules` since `v1.10.0` release.
Starting Resty v2 and higher versions, it fully embraces [go modules](https://github.com/golang/go/wiki/Modules) package release. It requires a Go version capable of understanding `/vN` suffixed imports:
- 1.9.7+
- 1.10.3+
- 1.11+
## It might be beneficial for your project :smile:
Resty author also published following projects for Go Community.
* [aah framework](https://aahframework.org) - A secure, flexible, rapid Go web framework.
* [THUMBAI](https://thumbai.app) - Go Mod Repository, Go Vanity Service and Simple Proxy Server.
* [go-model](https://github.com/jeevatkm/go-model) - Robust & Easy to use model mapper and utility methods for Go `struct`.
## Installation
```bash
# Go Modules
require github.com/go-resty/resty/v2 v2.7.0
```
## Usage
The following samples will assist you to become as comfortable as possible with resty library.
```go
// Import resty into your code and refer it as `resty`.
import "github.com/go-resty/resty/v2"
```
#### Simple GET
```go
// Create a Resty Client
client := resty.New()
resp, err := client.R().
EnableTrace().
Get("https://httpbin.org/get")
// Explore response object
fmt.Println("Response Info:")
fmt.Println(" Error :", err)
fmt.Println(" Status Code:", resp.StatusCode())
fmt.Println(" Status :", resp.Status())
fmt.Println(" Proto :", resp.Proto())
fmt.Println(" Time :", resp.Time())
fmt.Println(" Received At:", resp.ReceivedAt())
fmt.Println(" Body :\n", resp)
fmt.Println()
// Explore trace info
fmt.Println("Request Trace Info:")
ti := resp.Request.TraceInfo()
fmt.Println(" DNSLookup :", ti.DNSLookup)
fmt.Println(" ConnTime :", ti.ConnTime)
fmt.Println(" TCPConnTime :", ti.TCPConnTime)
fmt.Println(" TLSHandshake :", ti.TLSHandshake)
fmt.Println(" ServerTime :", ti.ServerTime)
fmt.Println(" ResponseTime :", ti.ResponseTime)
fmt.Println(" TotalTime :", ti.TotalTime)
fmt.Println(" IsConnReused :", ti.IsConnReused)
fmt.Println(" IsConnWasIdle :", ti.IsConnWasIdle)
fmt.Println(" ConnIdleTime :", ti.ConnIdleTime)
fmt.Println(" RequestAttempt:", ti.RequestAttempt)
fmt.Println(" RemoteAddr :", ti.RemoteAddr.String())
/* Output
Response Info:
Error : <nil>
Status Code: 200
Status : 200 OK
Proto : HTTP/2.0
Time : 457.034718ms
Received At: 2020-09-14 15:35:29.784681 -0700 PDT m=+0.458137045
Body :
{
"args": {},
"headers": {
"Accept-Encoding": "gzip",
"Host": "httpbin.org",
"User-Agent": "go-resty/2.4.0 (https://github.com/go-resty/resty)",
"X-Amzn-Trace-Id": "Root=1-5f5ff031-000ff6292204aa6898e4de49"
},
"origin": "0.0.0.0",
"url": "https://httpbin.org/get"
}
Request Trace Info:
DNSLookup : 4.074657ms
ConnTime : 381.709936ms
TCPConnTime : 77.428048ms
TLSHandshake : 299.623597ms
ServerTime : 75.414703ms
ResponseTime : 79.337µs
TotalTime : 457.034718ms
IsConnReused : false
IsConnWasIdle : false
ConnIdleTime : 0s
RequestAttempt: 1
RemoteAddr : 3.221.81.55:443
*/
```
#### Enhanced GET
```go
// Create a Resty Client
client := resty.New()
resp, err := client.R().
SetQueryParams(map[string]string{
"page_no": "1",
"limit": "20",
"sort":"name",
"order": "asc",
"random":strconv.FormatInt(time.Now().Unix(), 10),
}).
SetHeader("Accept", "application/json").
SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F").
Get("/search_result")
// Sample of using Request.SetQueryString method
resp, err := client.R().
SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more").
SetHeader("Accept", "application/json").
SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F").
Get("/show_product")
// If necessary, you can force response content type to tell Resty to parse a JSON response into your struct
resp, err := client.R().
SetResult(result).
ForceContentType("application/json").
Get("v2/alpine/manifests/latest")
```
#### Various POST method combinations
```go
// Create a Resty Client
client := resty.New()
// POST JSON string
// No need to set content type, if you have client level setting
resp, err := client.R().
SetHeader("Content-Type", "application/json").
SetBody(`{"username":"testuser", "password":"testpass"}`).
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
Post("https://myapp.com/login")
// POST []byte array
// No need to set content type, if you have client level setting
resp, err := client.R().
SetHeader("Content-Type", "application/json").
SetBody([]byte(`{"username":"testuser", "password":"testpass"}`)).
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
Post("https://myapp.com/login")
// POST Struct, default is JSON content type. No need to set one
resp, err := client.R().
SetBody(User{Username: "testuser", Password: "testpass"}).
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
SetError(&AuthError{}). // or SetError(AuthError{}).
Post("https://myapp.com/login")
// POST Map, default is JSON content type. No need to set one
resp, err := client.R().
SetBody(map[string]interface{}{"username": "testuser", "password": "testpass"}).
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
SetError(&AuthError{}). // or SetError(AuthError{}).
Post("https://myapp.com/login")
// POST of raw bytes for file upload. For example: upload file to Dropbox
fileBytes, _ := ioutil.ReadFile("/Users/jeeva/mydocument.pdf")
// See we are not setting content-type header, since go-resty automatically detects Content-Type for you
resp, err := client.R().
SetBody(fileBytes).
SetContentLength(true). // Dropbox expects this value
SetAuthToken("<your-auth-token>").
SetError(&DropboxError{}). // or SetError(DropboxError{}).
Post("https://content.dropboxapi.com/1/files_put/auto/resty/mydocument.pdf") // for upload Dropbox supports PUT too
// Note: resty detects Content-Type for request body/payload if content type header is not set.
// * For struct and map data type defaults to 'application/json'
// * Fallback is plain text content type
```
#### Sample PUT
You can use various combinations of `PUT` method call like demonstrated for `POST`.
```go
// Note: This is one sample of PUT method usage, refer POST for more combination
// Create a Resty Client
client := resty.New()
// Request goes as JSON content type
// No need to set auth token, error, if you have client level settings
resp, err := client.R().
SetBody(Article{
Title: "go-resty",
Content: "This is my article content, oh ya!",
Author: "Jeevanandam M",
Tags: []string{"article", "sample", "resty"},
}).
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
SetError(&Error{}). // or SetError(Error{}).
Put("https://myapp.com/article/1234")
```
#### Sample PATCH
You can use various combinations of `PATCH` method call like demonstrated for `POST`.
```go
// Note: This is one sample of PUT method usage, refer POST for more combination
// Create a Resty Client
client := resty.New()
// Request goes as JSON content type
// No need to set auth token, error, if you have client level settings
resp, err := client.R().
SetBody(Article{
Tags: []string{"new tag1", "new tag2"},
}).
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
SetError(&Error{}). // or SetError(Error{}).
Patch("https://myapp.com/articles/1234")
```
#### Sample DELETE, HEAD, OPTIONS
```go
// Create a Resty Client
client := resty.New()
// DELETE a article
// No need to set auth token, error, if you have client level settings
resp, err := client.R().
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
SetError(&Error{}). // or SetError(Error{}).
Delete("https://myapp.com/articles/1234")
// DELETE a articles with payload/body as a JSON string
// No need to set auth token, error, if you have client level settings
resp, err := client.R().
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
SetError(&Error{}). // or SetError(Error{}).
SetHeader("Content-Type", "application/json").
SetBody(`{article_ids: [1002, 1006, 1007, 87683, 45432] }`).
Delete("https://myapp.com/articles")
// HEAD of resource
// No need to set auth token, if you have client level settings
resp, err := client.R().
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
Head("https://myapp.com/videos/hi-res-video")
// OPTIONS of resource
// No need to set auth token, if you have client level settings
resp, err := client.R().
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
Options("https://myapp.com/servers/nyc-dc-01")
```
#### Override JSON & XML Marshal/Unmarshal
User could register choice of JSON/XML library into resty or write your own. By default resty registers standard `encoding/json` and `encoding/xml` respectively.
```go
// Example of registering json-iterator
import jsoniter "github.com/json-iterator/go"
json := jsoniter.ConfigCompatibleWithStandardLibrary
client := resty.New()
client.JSONMarshal = json.Marshal
client.JSONUnmarshal = json.Unmarshal
// similarly user could do for XML too with -
client.XMLMarshal
client.XMLUnmarshal
```
### Multipart File(s) upload
#### Using io.Reader
```go
profileImgBytes, _ := ioutil.ReadFile("/Users/jeeva/test-img.png")
notesBytes, _ := ioutil.ReadFile("/Users/jeeva/text-file.txt")
// Create a Resty Client
client := resty.New()
resp, err := client.R().
SetFileReader("profile_img", "test-img.png", bytes.NewReader(profileImgBytes)).
SetFileReader("notes", "text-file.txt", bytes.NewReader(notesBytes)).
SetFormData(map[string]string{
"first_name": "Jeevanandam",
"last_name": "M",
}).
Post("http://myapp.com/upload")
```
#### Using File directly from Path
```go
// Create a Resty Client
client := resty.New()
// Single file scenario
resp, err := client.R().
SetFile("profile_img", "/Users/jeeva/test-img.png").
Post("http://myapp.com/upload")
// Multiple files scenario
resp, err := client.R().
SetFiles(map[string]string{
"profile_img": "/Users/jeeva/test-img.png",
"notes": "/Users/jeeva/text-file.txt",
}).
Post("http://myapp.com/upload")
// Multipart of form fields and files
resp, err := client.R().
SetFiles(map[string]string{
"profile_img": "/Users/jeeva/test-img.png",
"notes": "/Users/jeeva/text-file.txt",
}).
SetFormData(map[string]string{
"first_name": "Jeevanandam",
"last_name": "M",
"zip_code": "00001",
"city": "my city",
"access_token": "C6A79608-782F-4ED0-A11D-BD82FAD829CD",
}).
Post("http://myapp.com/profile")
```
#### Sample Form submission
```go
// Create a Resty Client
client := resty.New()
// just mentioning about POST as an example with simple flow
// User Login
resp, err := client.R().
SetFormData(map[string]string{
"username": "jeeva",
"password": "mypass",
}).
Post("http://myapp.com/login")
// Followed by profile update
resp, err := client.R().
SetFormData(map[string]string{
"first_name": "Jeevanandam",
"last_name": "M",
"zip_code": "00001",
"city": "new city update",
}).
Post("http://myapp.com/profile")
// Multi value form data
criteria := url.Values{
"search_criteria": []string{"book", "glass", "pencil"},
}
resp, err := client.R().
SetFormDataFromValues(criteria).
Post("http://myapp.com/search")
```
#### Save HTTP Response into File
```go
// Create a Resty Client
client := resty.New()
// Setting output directory path, If directory not exists then resty creates one!
// This is optional one, if you're planning using absoule path in
// `Request.SetOutput` and can used together.
client.SetOutputDirectory("/Users/jeeva/Downloads")
// HTTP response gets saved into file, similar to curl -o flag
_, err := client.R().
SetOutput("plugin/ReplyWithHeader-v5.1-beta.zip").
Get("http://bit.ly/1LouEKr")
// OR using absolute path
// Note: output directory path is not used for absolute path
_, err := client.R().
SetOutput("/MyDownloads/plugin/ReplyWithHeader-v5.1-beta.zip").
Get("http://bit.ly/1LouEKr")
```
#### Request URL Path Params
Resty provides easy to use dynamic request URL path params. Params can be set at client and request level. Client level params value can be overridden at request level.
```go
// Create a Resty Client
client := resty.New()
client.R().SetPathParams(map[string]string{
"userId": "sample@sample.com",
"subAccountId": "100002",
}).
Get("/v1/users/{userId}/{subAccountId}/details")
// Result:
// Composed URL - /v1/users/sample@sample.com/100002/details
```
#### Request and Response Middleware
Resty provides middleware ability to manipulate for Request and Response. It is more flexible than callback approach.
```go
// Create a Resty Client
client := resty.New()
// Registering Request Middleware
client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error {
// Now you have access to Client and current Request object
// manipulate it as per your need
return nil // if its success otherwise return error
})
// Registering Response Middleware
client.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
// Now you have access to Client and current Response object
// manipulate it as per your need
return nil // if its success otherwise return error
})
```
#### OnError Hooks
Resty provides OnError hooks that may be called because:
- The client failed to send the request due to connection timeout, TLS handshake failure, etc...
- The request was retried the maximum amount of times, and still failed.
If there was a response from the server, the original error will be wrapped in `*resty.ResponseError` which contains the last response received.
```go
// Create a Resty Client
client := resty.New()
client.OnError(func(req *resty.Request, err error) {
if v, ok := err.(*resty.ResponseError); ok {
// v.Response contains the last response from the server
// v.Err contains the original error
}
// Log the error, increment a metric, etc...
})
```
#### Redirect Policy
Resty provides few ready to use redirect policy(s) also it supports multiple policies together.
```go
// Create a Resty Client
client := resty.New()
// Assign Client Redirect Policy. Create one as per you need
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(15))
// Wanna multiple policies such as redirect count, domain name check, etc
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20),
resty.DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net"))
```
##### Custom Redirect Policy
Implement [RedirectPolicy](redirect.go#L20) interface and register it with resty client. Have a look [redirect.go](redirect.go) for more information.
```go
// Create a Resty Client
client := resty.New()
// Using raw func into resty.SetRedirectPolicy
client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
// Implement your logic here
// return nil for continue redirect otherwise return error to stop/prevent redirect
return nil
}))
//---------------------------------------------------
// Using struct create more flexible redirect policy
type CustomRedirectPolicy struct {
// variables goes here
}
func (c *CustomRedirectPolicy) Apply(req *http.Request, via []*http.Request) error {
// Implement your logic here
// return nil for continue redirect otherwise return error to stop/prevent redirect
return nil
}
// Registering in resty
client.SetRedirectPolicy(CustomRedirectPolicy{/* initialize variables */})
```
#### Custom Root Certificates and Client Certificates
```go
// Create a Resty Client
client := resty.New()
// Custom Root certificates, just supply .pem file.
// you can add one or more root certificates, its get appended
client.SetRootCertificate("/path/to/root/pemFile1.pem")
client.SetRootCertificate("/path/to/root/pemFile2.pem")
// ... and so on!
// Adding Client Certificates, you add one or more certificates
// Sample for creating certificate object
// Parsing public/private key pair from a pair of files. The files must contain PEM encoded data.
cert1, err := tls.LoadX509KeyPair("certs/client.pem", "certs/client.key")
if err != nil {
log.Fatalf("ERROR client certificate: %s", err)
}
// ...
// You add one or more certificates
client.SetCertificates(cert1, cert2, cert3)
```
#### Custom Root Certificates and Client Certificates from string
```go
// Custom Root certificates from string
// You can pass you certificates throught env variables as strings
// you can add one or more root certificates, its get appended
client.SetRootCertificateFromString("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----")
client.SetRootCertificateFromString("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----")
// ... and so on!
// Adding Client Certificates, you add one or more certificates
// Sample for creating certificate object
// Parsing public/private key pair from a pair of files. The files must contain PEM encoded data.
cert1, err := tls.X509KeyPair([]byte("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----"), []byte("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----"))
if err != nil {
log.Fatalf("ERROR client certificate: %s", err)
}
// ...
// You add one or more certificates
client.SetCertificates(cert1, cert2, cert3)
```
#### Proxy Settings - Client as well as at Request Level
Default `Go` supports Proxy via environment variable `HTTP_PROXY`. Resty provides support via `SetProxy` & `RemoveProxy`.
Choose as per your need.
**Client Level Proxy** settings applied to all the request
```go
// Create a Resty Client
client := resty.New()
// Setting a Proxy URL and Port
client.SetProxy("http://proxyserver:8888")
// Want to remove proxy setting
client.RemoveProxy()
```
#### Retries
Resty uses [backoff](http://www.awsarchitectureblog.com/2015/03/backoff.html)
to increase retry intervals after each attempt.
Usage example:
```go
// Create a Resty Client
client := resty.New()
// Retries are configured per client
client.
// Set retry count to non zero to enable retries
SetRetryCount(3).
// You can override initial retry wait time.
// Default is 100 milliseconds.
SetRetryWaitTime(5 * time.Second).
// MaxWaitTime can be overridden as well.
// Default is 2 seconds.
SetRetryMaxWaitTime(20 * time.Second).
// SetRetryAfter sets callback to calculate wait time between retries.
// Default (nil) implies exponential backoff with jitter
SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) {
return 0, errors.New("quota exceeded")
})
```
Above setup will result in resty retrying requests returned non nil error up to
3 times with delay increased after each attempt.
You can optionally provide client with [custom retry conditions](https://pkg.go.dev/github.com/go-resty/resty/v2#RetryConditionFunc):
```go
// Create a Resty Client
client := resty.New()
client.AddRetryCondition(
// RetryConditionFunc type is for retry condition function
// input: non-nil Response OR request execution error
func(r *resty.Response, err error) bool {
return r.StatusCode() == http.StatusTooManyRequests
},
)
```
Above example will make resty retry requests ended with `429 Too Many Requests`
status code.
Multiple retry conditions can be added.
It is also possible to use `resty.Backoff(...)` to get arbitrary retry scenarios
implemented. [Reference](retry_test.go).
#### Allow GET request with Payload
```go
// Create a Resty Client
client := resty.New()
// Allow GET request with Payload. This is disabled by default.
client.SetAllowGetMethodPayload(true)
```
#### Wanna Multiple Clients
```go
// Here you go!
// Client 1
client1 := resty.New()
client1.R().Get("http://httpbin.org")
// ...
// Client 2
client2 := resty.New()
client2.R().Head("http://httpbin.org")
// ...
// Bend it as per your need!!!
```
#### Remaining Client Settings & its Options
```go
// Create a Resty Client
client := resty.New()
// Unique settings at Client level
//--------------------------------
// Enable debug mode
client.SetDebug(true)
// Assign Client TLSClientConfig
// One can set custom root-certificate. Refer: http://golang.org/pkg/crypto/tls/#example_Dial
client.SetTLSClientConfig(&tls.Config{ RootCAs: roots })
// or One can disable security check (https)
client.SetTLSClientConfig(&tls.Config{ InsecureSkipVerify: true })
// Set client timeout as per your need
client.SetTimeout(1 * time.Minute)
// You can override all below settings and options at request level if you want to
//--------------------------------------------------------------------------------
// Host URL for all request. So you can use relative URL in the request
client.SetHostURL("http://httpbin.org")
// Headers for all request
client.SetHeader("Accept", "application/json")
client.SetHeaders(map[string]string{
"Content-Type": "application/json",
"User-Agent": "My custom User Agent String",
})
// Cookies for all request
client.SetCookie(&http.Cookie{
Name:"go-resty",
Value:"This is cookie value",
Path: "/",
Domain: "sample.com",
MaxAge: 36000,
HttpOnly: true,
Secure: false,
})
client.SetCookies(cookies)
// URL query parameters for all request
client.SetQueryParam("user_id", "00001")
client.SetQueryParams(map[string]string{ // sample of those who use this manner
"api_key": "api-key-here",
"api_secert": "api-secert",
})
client.R().SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more")
// Form data for all request. Typically used with POST and PUT
client.SetFormData(map[string]string{
"access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
})
// Basic Auth for all request
client.SetBasicAuth("myuser", "mypass")
// Bearer Auth Token for all request
client.SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
// Enabling Content length value for all request
client.SetContentLength(true)
// Registering global Error object structure for JSON/XML request
client.SetError(&Error{}) // or resty.SetError(Error{})
```
#### Unix Socket
```go
unixSocket := "/var/run/my_socket.sock"
// Create a Go's http.Transport so we can set it in resty.
transport := http.Transport{
Dial: func(_, _ string) (net.Conn, error) {
return net.Dial("unix", unixSocket)
},
}
// Create a Resty Client
client := resty.New()
// Set the previous transport that we created, set the scheme of the communication to the
// socket and set the unixSocket as the HostURL.
client.SetTransport(&transport).SetScheme("http").SetHostURL(unixSocket)
// No need to write the host's URL on the request, just the path.
client.R().Get("/index.html")
```
#### Bazel Support
Resty can be built, tested and depended upon via [Bazel](https://bazel.build).
For example, to run all tests:
```shell
bazel test :resty_test
```
#### Mocking http requests using [httpmock](https://github.com/jarcoal/httpmock) library
In order to mock the http requests when testing your application you
could use the `httpmock` library.
When using the default resty client, you should pass the client to the library as follow:
```go
// Create a Resty Client
client := resty.New()
// Get the underlying HTTP Client and set it to Mock
httpmock.ActivateNonDefault(client.GetClient())
```
More detailed example of mocking resty http requests using ginko could be found [here](https://github.com/jarcoal/httpmock#ginkgo--resty-example).
## Versioning
Resty releases versions according to [Semantic Versioning](http://semver.org)
* Resty v2 does not use `gopkg.in` service for library versioning.
* Resty fully adapted to `go mod` capabilities since `v1.10.0` release.
* Resty v1 series was using `gopkg.in` to provide versioning. `gopkg.in/resty.vX` points to appropriate tagged versions; `X` denotes version series number and it's a stable release for production use. For e.g. `gopkg.in/resty.v0`.
* Development takes place at the master branch. Although the code in master should always compile and test successfully, it might break API's. I aim to maintain backwards compatibility, but sometimes API's and behavior might be changed to fix a bug.
## Contribution
I would welcome your contribution! If you find any improvement or issue you want to fix, feel free to send a pull request, I like pull requests that include test cases for fix/enhancement. I have done my best to bring pretty good code coverage. Feel free to write tests.
BTW, I'd like to know what you think about `Resty`. Kindly open an issue or send me an email; it'd mean a lot to me.
## Creator
[Jeevanandam M.](https://github.com/jeevatkm) (jeeva@myjeeva.com)
## Core Team
Have a look on [Members](https://github.com/orgs/go-resty/people) page.
## Contributors
Have a look on [Contributors](https://github.com/go-resty/resty/graphs/contributors) page.
## License
Resty released under MIT license, refer [LICENSE](LICENSE) file.

View file

@ -1,31 +0,0 @@
workspace(name = "resty")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "io_bazel_rules_go",
sha256 = "69de5c704a05ff37862f7e0f5534d4f479418afc21806c887db544a316f3cb6b",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
"https://github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
],
)
http_archive(
name = "bazel_gazelle",
sha256 = "62ca106be173579c0a167deb23358fdfe71ffa1e4cfdddf5582af26520f1c66f",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz",
"https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz",
],
)
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
go_rules_dependencies()
go_register_toolchains(version = "1.16")
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
gazelle_dependencies()

File diff suppressed because it is too large Load diff

View file

@ -1,543 +0,0 @@
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package resty
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"strings"
"time"
)
const debugRequestLogKey = "__restyDebugRequestLog"
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Request Middleware(s)
//_______________________________________________________________________
func parseRequestURL(c *Client, r *Request) error {
// GitHub #103 Path Params
if len(r.PathParams) > 0 {
for p, v := range r.PathParams {
r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1)
}
}
if len(c.PathParams) > 0 {
for p, v := range c.PathParams {
r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1)
}
}
// Parsing request URL
reqURL, err := url.Parse(r.URL)
if err != nil {
return err
}
// If Request.URL is relative path then added c.HostURL into
// the request URL otherwise Request.URL will be used as-is
if !reqURL.IsAbs() {
r.URL = reqURL.String()
if len(r.URL) > 0 && r.URL[0] != '/' {
r.URL = "/" + r.URL
}
reqURL, err = url.Parse(c.HostURL + r.URL)
if err != nil {
return err
}
}
// GH #407 && #318
if reqURL.Scheme == "" && len(c.scheme) > 0 {
reqURL.Scheme = c.scheme
}
// Adding Query Param
query := make(url.Values)
for k, v := range c.QueryParam {
for _, iv := range v {
query.Add(k, iv)
}
}
for k, v := range r.QueryParam {
// remove query param from client level by key
// since overrides happens for that key in the request
query.Del(k)
for _, iv := range v {
query.Add(k, iv)
}
}
// GitHub #123 Preserve query string order partially.
// Since not feasible in `SetQuery*` resty methods, because
// standard package `url.Encode(...)` sorts the query params
// alphabetically
if len(query) > 0 {
if IsStringEmpty(reqURL.RawQuery) {
reqURL.RawQuery = query.Encode()
} else {
reqURL.RawQuery = reqURL.RawQuery + "&" + query.Encode()
}
}
r.URL = reqURL.String()
return nil
}
func parseRequestHeader(c *Client, r *Request) error {
hdr := make(http.Header)
for k := range c.Header {
hdr[k] = append(hdr[k], c.Header[k]...)
}
for k := range r.Header {
hdr.Del(k)
hdr[k] = append(hdr[k], r.Header[k]...)
}
if IsStringEmpty(hdr.Get(hdrUserAgentKey)) {
hdr.Set(hdrUserAgentKey, hdrUserAgentValue)
}
ct := hdr.Get(hdrContentTypeKey)
if IsStringEmpty(hdr.Get(hdrAcceptKey)) && !IsStringEmpty(ct) &&
(IsJSONType(ct) || IsXMLType(ct)) {
hdr.Set(hdrAcceptKey, hdr.Get(hdrContentTypeKey))
}
r.Header = hdr
return nil
}
func parseRequestBody(c *Client, r *Request) (err error) {
if isPayloadSupported(r.Method, c.AllowGetMethodPayload) {
// Handling Multipart
if r.isMultiPart && !(r.Method == MethodPatch) {
if err = handleMultipart(c, r); err != nil {
return
}
goto CL
}
// Handling Form Data
if len(c.FormData) > 0 || len(r.FormData) > 0 {
handleFormData(c, r)
goto CL
}
// Handling Request body
if r.Body != nil {
handleContentType(c, r)
if err = handleRequestBody(c, r); err != nil {
return
}
}
}
CL:
// by default resty won't set content length, you can if you want to :)
if (c.setContentLength || r.setContentLength) && r.bodyBuf != nil {
r.Header.Set(hdrContentLengthKey, fmt.Sprintf("%d", r.bodyBuf.Len()))
}
return
}
func createHTTPRequest(c *Client, r *Request) (err error) {
if r.bodyBuf == nil {
if reader, ok := r.Body.(io.Reader); ok {
r.RawRequest, err = http.NewRequest(r.Method, r.URL, reader)
} else if c.setContentLength || r.setContentLength {
r.RawRequest, err = http.NewRequest(r.Method, r.URL, http.NoBody)
} else {
r.RawRequest, err = http.NewRequest(r.Method, r.URL, nil)
}
} else {
r.RawRequest, err = http.NewRequest(r.Method, r.URL, r.bodyBuf)
}
if err != nil {
return
}
// Assign close connection option
r.RawRequest.Close = c.closeConnection
// Add headers into http request
r.RawRequest.Header = r.Header
// Add cookies from client instance into http request
for _, cookie := range c.Cookies {
r.RawRequest.AddCookie(cookie)
}
// Add cookies from request instance into http request
for _, cookie := range r.Cookies {
r.RawRequest.AddCookie(cookie)
}
// Enable trace
if c.trace || r.trace {
r.clientTrace = &clientTrace{}
r.ctx = r.clientTrace.createContext(r.Context())
}
// Use context if it was specified
if r.ctx != nil {
r.RawRequest = r.RawRequest.WithContext(r.ctx)
}
bodyCopy, err := getBodyCopy(r)
if err != nil {
return err
}
// assign get body func for the underlying raw request instance
r.RawRequest.GetBody = func() (io.ReadCloser, error) {
if bodyCopy != nil {
return ioutil.NopCloser(bytes.NewReader(bodyCopy.Bytes())), nil
}
return nil, nil
}
return
}
func addCredentials(c *Client, r *Request) error {
var isBasicAuth bool
// Basic Auth
if r.UserInfo != nil { // takes precedence
r.RawRequest.SetBasicAuth(r.UserInfo.Username, r.UserInfo.Password)
isBasicAuth = true
} else if c.UserInfo != nil {
r.RawRequest.SetBasicAuth(c.UserInfo.Username, c.UserInfo.Password)
isBasicAuth = true
}
if !c.DisableWarn {
if isBasicAuth && !strings.HasPrefix(r.URL, "https") {
c.log.Warnf("Using Basic Auth in HTTP mode is not secure, use HTTPS")
}
}
// Set the Authorization Header Scheme
var authScheme string
if !IsStringEmpty(r.AuthScheme) {
authScheme = r.AuthScheme
} else if !IsStringEmpty(c.AuthScheme) {
authScheme = c.AuthScheme
} else {
authScheme = "Bearer"
}
// Build the Token Auth header
if !IsStringEmpty(r.Token) { // takes precedence
r.RawRequest.Header.Set(c.HeaderAuthorizationKey, authScheme+" "+r.Token)
} else if !IsStringEmpty(c.Token) {
r.RawRequest.Header.Set(c.HeaderAuthorizationKey, authScheme+" "+c.Token)
}
return nil
}
func requestLogger(c *Client, r *Request) error {
if c.Debug {
rr := r.RawRequest
rl := &RequestLog{Header: copyHeaders(rr.Header), Body: r.fmtBodyString(c.debugBodySizeLimit)}
if c.requestLog != nil {
if err := c.requestLog(rl); err != nil {
return err
}
}
// fmt.Sprintf("COOKIES:\n%s\n", composeCookies(c.GetClient().Jar, *rr.URL)) +
reqLog := "\n==============================================================================\n" +
"~~~ REQUEST ~~~\n" +
fmt.Sprintf("%s %s %s\n", r.Method, rr.URL.RequestURI(), rr.Proto) +
fmt.Sprintf("HOST : %s\n", rr.URL.Host) +
fmt.Sprintf("HEADERS:\n%s\n", composeHeaders(c, r, rl.Header)) +
fmt.Sprintf("BODY :\n%v\n", rl.Body) +
"------------------------------------------------------------------------------\n"
r.initValuesMap()
r.values[debugRequestLogKey] = reqLog
}
return nil
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Response Middleware(s)
//_______________________________________________________________________
func responseLogger(c *Client, res *Response) error {
if c.Debug {
rl := &ResponseLog{Header: copyHeaders(res.Header()), Body: res.fmtBodyString(c.debugBodySizeLimit)}
if c.responseLog != nil {
if err := c.responseLog(rl); err != nil {
return err
}
}
debugLog := res.Request.values[debugRequestLogKey].(string)
debugLog += "~~~ RESPONSE ~~~\n" +
fmt.Sprintf("STATUS : %s\n", res.Status()) +
fmt.Sprintf("PROTO : %s\n", res.RawResponse.Proto) +
fmt.Sprintf("RECEIVED AT : %v\n", res.ReceivedAt().Format(time.RFC3339Nano)) +
fmt.Sprintf("TIME DURATION: %v\n", res.Time()) +
"HEADERS :\n" +
composeHeaders(c, res.Request, rl.Header) + "\n"
if res.Request.isSaveResponse {
debugLog += "BODY :\n***** RESPONSE WRITTEN INTO FILE *****\n"
} else {
debugLog += fmt.Sprintf("BODY :\n%v\n", rl.Body)
}
debugLog += "==============================================================================\n"
c.log.Debugf("%s", debugLog)
}
return nil
}
func parseResponseBody(c *Client, res *Response) (err error) {
if res.StatusCode() == http.StatusNoContent {
return
}
// Handles only JSON or XML content type
ct := firstNonEmpty(res.Request.forceContentType, res.Header().Get(hdrContentTypeKey), res.Request.fallbackContentType)
if IsJSONType(ct) || IsXMLType(ct) {
// HTTP status code > 199 and < 300, considered as Result
if res.IsSuccess() {
res.Request.Error = nil
if res.Request.Result != nil {
err = Unmarshalc(c, ct, res.body, res.Request.Result)
return
}
}
// HTTP status code > 399, considered as Error
if res.IsError() {
// global error interface
if res.Request.Error == nil && c.Error != nil {
res.Request.Error = reflect.New(c.Error).Interface()
}
if res.Request.Error != nil {
err = Unmarshalc(c, ct, res.body, res.Request.Error)
}
}
}
return
}
func handleMultipart(c *Client, r *Request) (err error) {
r.bodyBuf = acquireBuffer()
w := multipart.NewWriter(r.bodyBuf)
for k, v := range c.FormData {
for _, iv := range v {
if err = w.WriteField(k, iv); err != nil {
return err
}
}
}
for k, v := range r.FormData {
for _, iv := range v {
if strings.HasPrefix(k, "@") { // file
err = addFile(w, k[1:], iv)
if err != nil {
return
}
} else { // form value
if err = w.WriteField(k, iv); err != nil {
return err
}
}
}
}
// #21 - adding io.Reader support
if len(r.multipartFiles) > 0 {
for _, f := range r.multipartFiles {
err = addFileReader(w, f)
if err != nil {
return
}
}
}
// GitHub #130 adding multipart field support with content type
if len(r.multipartFields) > 0 {
for _, mf := range r.multipartFields {
if err = addMultipartFormField(w, mf); err != nil {
return
}
}
}
r.Header.Set(hdrContentTypeKey, w.FormDataContentType())
err = w.Close()
return
}
func handleFormData(c *Client, r *Request) {
formData := url.Values{}
for k, v := range c.FormData {
for _, iv := range v {
formData.Add(k, iv)
}
}
for k, v := range r.FormData {
// remove form data field from client level by key
// since overrides happens for that key in the request
formData.Del(k)
for _, iv := range v {
formData.Add(k, iv)
}
}
r.bodyBuf = bytes.NewBuffer([]byte(formData.Encode()))
r.Header.Set(hdrContentTypeKey, formContentType)
r.isFormData = true
}
func handleContentType(c *Client, r *Request) {
contentType := r.Header.Get(hdrContentTypeKey)
if IsStringEmpty(contentType) {
contentType = DetectContentType(r.Body)
r.Header.Set(hdrContentTypeKey, contentType)
}
}
func handleRequestBody(c *Client, r *Request) (err error) {
var bodyBytes []byte
contentType := r.Header.Get(hdrContentTypeKey)
kind := kindOf(r.Body)
r.bodyBuf = nil
if reader, ok := r.Body.(io.Reader); ok {
if c.setContentLength || r.setContentLength { // keep backward compatibility
r.bodyBuf = acquireBuffer()
_, err = r.bodyBuf.ReadFrom(reader)
r.Body = nil
} else {
// Otherwise buffer less processing for `io.Reader`, sounds good.
return
}
} else if b, ok := r.Body.([]byte); ok {
bodyBytes = b
} else if s, ok := r.Body.(string); ok {
bodyBytes = []byte(s)
} else if IsJSONType(contentType) &&
(kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {
r.bodyBuf, err = jsonMarshal(c, r, r.Body)
if err != nil {
return
}
} else if IsXMLType(contentType) && (kind == reflect.Struct) {
bodyBytes, err = c.XMLMarshal(r.Body)
if err != nil {
return
}
}
if bodyBytes == nil && r.bodyBuf == nil {
err = errors.New("unsupported 'Body' type/value")
}
// if any errors during body bytes handling, return it
if err != nil {
return
}
// []byte into Buffer
if bodyBytes != nil && r.bodyBuf == nil {
r.bodyBuf = acquireBuffer()
_, _ = r.bodyBuf.Write(bodyBytes)
}
return
}
func saveResponseIntoFile(c *Client, res *Response) error {
if res.Request.isSaveResponse {
file := ""
if len(c.outputDirectory) > 0 && !filepath.IsAbs(res.Request.outputFile) {
file += c.outputDirectory + string(filepath.Separator)
}
file = filepath.Clean(file + res.Request.outputFile)
if err := createDirectory(filepath.Dir(file)); err != nil {
return err
}
outFile, err := os.Create(file)
if err != nil {
return err
}
defer closeq(outFile)
// io.Copy reads maximum 32kb size, it is perfect for large file download too
defer closeq(res.RawResponse.Body)
written, err := io.Copy(outFile, res.RawResponse.Body)
if err != nil {
return err
}
res.size = written
}
return nil
}
func getBodyCopy(r *Request) (*bytes.Buffer, error) {
// If r.bodyBuf present, return the copy
if r.bodyBuf != nil {
return bytes.NewBuffer(r.bodyBuf.Bytes()), nil
}
// Maybe body is `io.Reader`.
// Note: Resty user have to watchout for large body size of `io.Reader`
if r.RawRequest.Body != nil {
b, err := ioutil.ReadAll(r.RawRequest.Body)
if err != nil {
return nil, err
}
// Restore the Body
closeq(r.RawRequest.Body)
r.RawRequest.Body = ioutil.NopCloser(bytes.NewBuffer(b))
// Return the Body bytes
return bytes.NewBuffer(b), nil
}
return nil, nil
}

View file

@ -1,101 +0,0 @@
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package resty
import (
"errors"
"fmt"
"net"
"net/http"
"strings"
)
type (
// RedirectPolicy to regulate the redirects in the resty client.
// Objects implementing the RedirectPolicy interface can be registered as
//
// Apply function should return nil to continue the redirect jounery, otherwise
// return error to stop the redirect.
RedirectPolicy interface {
Apply(req *http.Request, via []*http.Request) error
}
// The RedirectPolicyFunc type is an adapter to allow the use of ordinary functions as RedirectPolicy.
// If f is a function with the appropriate signature, RedirectPolicyFunc(f) is a RedirectPolicy object that calls f.
RedirectPolicyFunc func(*http.Request, []*http.Request) error
)
// Apply calls f(req, via).
func (f RedirectPolicyFunc) Apply(req *http.Request, via []*http.Request) error {
return f(req, via)
}
// NoRedirectPolicy is used to disable redirects in the HTTP client
// resty.SetRedirectPolicy(NoRedirectPolicy())
func NoRedirectPolicy() RedirectPolicy {
return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
return errors.New("auto redirect is disabled")
})
}
// FlexibleRedirectPolicy is convenient method to create No of redirect policy for HTTP client.
// resty.SetRedirectPolicy(FlexibleRedirectPolicy(20))
func FlexibleRedirectPolicy(noOfRedirect int) RedirectPolicy {
return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
if len(via) >= noOfRedirect {
return fmt.Errorf("stopped after %d redirects", noOfRedirect)
}
checkHostAndAddHeaders(req, via[0])
return nil
})
}
// DomainCheckRedirectPolicy is convenient method to define domain name redirect rule in resty client.
// Redirect is allowed for only mentioned host in the policy.
// resty.SetRedirectPolicy(DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net"))
func DomainCheckRedirectPolicy(hostnames ...string) RedirectPolicy {
hosts := make(map[string]bool)
for _, h := range hostnames {
hosts[strings.ToLower(h)] = true
}
fn := RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
if ok := hosts[getHostname(req.URL.Host)]; !ok {
return errors.New("redirect is not allowed as per DomainCheckRedirectPolicy")
}
return nil
})
return fn
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Package Unexported methods
//_______________________________________________________________________
func getHostname(host string) (hostname string) {
if strings.Index(host, ":") > 0 {
host, _, _ = net.SplitHostPort(host)
}
hostname = strings.ToLower(host)
return
}
// By default Golang will not redirect request headers
// after go throughing various discussion comments from thread
// https://github.com/golang/go/issues/4800
// Resty will add all the headers during a redirect for the same host
func checkHostAndAddHeaders(cur *http.Request, pre *http.Request) {
curHostname := getHostname(cur.URL.Host)
preHostname := getHostname(pre.URL.Host)
if strings.EqualFold(curHostname, preHostname) {
for key, val := range pre.Header {
cur.Header[key] = val
}
} else { // only library User-Agent header is added
cur.Header.Set(hdrUserAgentKey, hdrUserAgentValue)
}
}

View file

@ -1,896 +0,0 @@
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package resty
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net"
"net/http"
"net/url"
"reflect"
"strings"
"time"
)
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Request struct and methods
//_______________________________________________________________________
// Request struct is used to compose and fire individual request from
// resty client. Request provides an options to override client level
// settings and also an options for the request composition.
type Request struct {
URL string
Method string
Token string
AuthScheme string
QueryParam url.Values
FormData url.Values
PathParams map[string]string
Header http.Header
Time time.Time
Body interface{}
Result interface{}
Error interface{}
RawRequest *http.Request
SRV *SRVRecord
UserInfo *User
Cookies []*http.Cookie
// Attempt is to represent the request attempt made during a Resty
// request execution flow, including retry count.
//
// Since v2.4.0
Attempt int
isMultiPart bool
isFormData bool
setContentLength bool
isSaveResponse bool
notParseResponse bool
jsonEscapeHTML bool
trace bool
outputFile string
fallbackContentType string
forceContentType string
ctx context.Context
values map[string]interface{}
client *Client
bodyBuf *bytes.Buffer
clientTrace *clientTrace
multipartFiles []*File
multipartFields []*MultipartField
retryConditions []RetryConditionFunc
}
// Context method returns the Context if its already set in request
// otherwise it creates new one using `context.Background()`.
func (r *Request) Context() context.Context {
if r.ctx == nil {
return context.Background()
}
return r.ctx
}
// SetContext method sets the context.Context for current Request. It allows
// to interrupt the request execution if ctx.Done() channel is closed.
// See https://blog.golang.org/context article and the "context" package
// documentation.
func (r *Request) SetContext(ctx context.Context) *Request {
r.ctx = ctx
return r
}
// SetHeader method is to set a single header field and its value in the current request.
//
// For Example: To set `Content-Type` and `Accept` as `application/json`.
// client.R().
// SetHeader("Content-Type", "application/json").
// SetHeader("Accept", "application/json")
//
// Also you can override header value, which was set at client instance level.
func (r *Request) SetHeader(header, value string) *Request {
r.Header.Set(header, value)
return r
}
// SetHeaders method sets multiple headers field and its values at one go in the current request.
//
// For Example: To set `Content-Type` and `Accept` as `application/json`
//
// client.R().
// SetHeaders(map[string]string{
// "Content-Type": "application/json",
// "Accept": "application/json",
// })
// Also you can override header value, which was set at client instance level.
func (r *Request) SetHeaders(headers map[string]string) *Request {
for h, v := range headers {
r.SetHeader(h, v)
}
return r
}
// SetHeaderMultiValues sets multiple headers fields and its values is list of strings at one go in the current request.
//
// For Example: To set `Accept` as `text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8`
//
// client.R().
// SetHeaderMultiValues(map[string][]string{
// "Accept": []string{"text/html", "application/xhtml+xml", "application/xml;q=0.9", "image/webp", "*/*;q=0.8"},
// })
// Also you can override header value, which was set at client instance level.
func (r *Request) SetHeaderMultiValues(headers map[string][]string) *Request {
for key, values := range headers {
r.SetHeader(key, strings.Join(values, ", "))
}
return r
}
// SetHeaderVerbatim method is to set a single header field and its value verbatim in the current request.
//
// For Example: To set `all_lowercase` and `UPPERCASE` as `available`.
// client.R().
// SetHeaderVerbatim("all_lowercase", "available").
// SetHeaderVerbatim("UPPERCASE", "available")
//
// Also you can override header value, which was set at client instance level.
//
// Since v2.6.0
func (r *Request) SetHeaderVerbatim(header, value string) *Request {
r.Header[header] = []string{value}
return r
}
// SetQueryParam method sets single parameter and its value in the current request.
// It will be formed as query string for the request.
//
// For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark.
// client.R().
// SetQueryParam("search", "kitchen papers").
// SetQueryParam("size", "large")
// Also you can override query params value, which was set at client instance level.
func (r *Request) SetQueryParam(param, value string) *Request {
r.QueryParam.Set(param, value)
return r
}
// SetQueryParams method sets multiple parameters and its values at one go in the current request.
// It will be formed as query string for the request.
//
// For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark.
// client.R().
// SetQueryParams(map[string]string{
// "search": "kitchen papers",
// "size": "large",
// })
// Also you can override query params value, which was set at client instance level.
func (r *Request) SetQueryParams(params map[string]string) *Request {
for p, v := range params {
r.SetQueryParam(p, v)
}
return r
}
// SetQueryParamsFromValues method appends multiple parameters with multi-value
// (`url.Values`) at one go in the current request. It will be formed as
// query string for the request.
//
// For Example: `status=pending&status=approved&status=open` in the URL after `?` mark.
// client.R().
// SetQueryParamsFromValues(url.Values{
// "status": []string{"pending", "approved", "open"},
// })
// Also you can override query params value, which was set at client instance level.
func (r *Request) SetQueryParamsFromValues(params url.Values) *Request {
for p, v := range params {
for _, pv := range v {
r.QueryParam.Add(p, pv)
}
}
return r
}
// SetQueryString method provides ability to use string as an input to set URL query string for the request.
//
// Using String as an input
// client.R().
// SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more")
func (r *Request) SetQueryString(query string) *Request {
params, err := url.ParseQuery(strings.TrimSpace(query))
if err == nil {
for p, v := range params {
for _, pv := range v {
r.QueryParam.Add(p, pv)
}
}
} else {
r.client.log.Errorf("%v", err)
}
return r
}
// SetFormData method sets Form parameters and their values in the current request.
// It's applicable only HTTP method `POST` and `PUT` and requests content type would be set as
// `application/x-www-form-urlencoded`.
// client.R().
// SetFormData(map[string]string{
// "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
// "user_id": "3455454545",
// })
// Also you can override form data value, which was set at client instance level.
func (r *Request) SetFormData(data map[string]string) *Request {
for k, v := range data {
r.FormData.Set(k, v)
}
return r
}
// SetFormDataFromValues method appends multiple form parameters with multi-value
// (`url.Values`) at one go in the current request.
// client.R().
// SetFormDataFromValues(url.Values{
// "search_criteria": []string{"book", "glass", "pencil"},
// })
// Also you can override form data value, which was set at client instance level.
func (r *Request) SetFormDataFromValues(data url.Values) *Request {
for k, v := range data {
for _, kv := range v {
r.FormData.Add(k, kv)
}
}
return r
}
// SetBody method sets the request body for the request. It supports various realtime needs as easy.
// We can say its quite handy or powerful. Supported request body data types is `string`,
// `[]byte`, `struct`, `map`, `slice` and `io.Reader`. Body value can be pointer or non-pointer.
// Automatic marshalling for JSON and XML content type, if it is `struct`, `map`, or `slice`.
//
// Note: `io.Reader` is processed as bufferless mode while sending request.
//
// For Example: Struct as a body input, based on content type, it will be marshalled.
// client.R().
// SetBody(User{
// Username: "jeeva@myjeeva.com",
// Password: "welcome2resty",
// })
//
// Map as a body input, based on content type, it will be marshalled.
// client.R().
// SetBody(map[string]interface{}{
// "username": "jeeva@myjeeva.com",
// "password": "welcome2resty",
// "address": &Address{
// Address1: "1111 This is my street",
// Address2: "Apt 201",
// City: "My City",
// State: "My State",
// ZipCode: 00000,
// },
// })
//
// String as a body input. Suitable for any need as a string input.
// client.R().
// SetBody(`{
// "username": "jeeva@getrightcare.com",
// "password": "admin"
// }`)
//
// []byte as a body input. Suitable for raw request such as file upload, serialize & deserialize, etc.
// client.R().
// SetBody([]byte("This is my raw request, sent as-is"))
func (r *Request) SetBody(body interface{}) *Request {
r.Body = body
return r
}
// SetResult method is to register the response `Result` object for automatic unmarshalling for the request,
// if response status code is between 200 and 299 and content type either JSON or XML.
//
// Note: Result object can be pointer or non-pointer.
// client.R().SetResult(&AuthToken{})
// // OR
// client.R().SetResult(AuthToken{})
//
// Accessing a result value from response instance.
// response.Result().(*AuthToken)
func (r *Request) SetResult(res interface{}) *Request {
r.Result = getPointer(res)
return r
}
// SetError method is to register the request `Error` object for automatic unmarshalling for the request,
// if response status code is greater than 399 and content type either JSON or XML.
//
// Note: Error object can be pointer or non-pointer.
// client.R().SetError(&AuthError{})
// // OR
// client.R().SetError(AuthError{})
//
// Accessing a error value from response instance.
// response.Error().(*AuthError)
func (r *Request) SetError(err interface{}) *Request {
r.Error = getPointer(err)
return r
}
// SetFile method is to set single file field name and its path for multipart upload.
// client.R().
// SetFile("my_file", "/Users/jeeva/Gas Bill - Sep.pdf")
func (r *Request) SetFile(param, filePath string) *Request {
r.isMultiPart = true
r.FormData.Set("@"+param, filePath)
return r
}
// SetFiles method is to set multiple file field name and its path for multipart upload.
// client.R().
// SetFiles(map[string]string{
// "my_file1": "/Users/jeeva/Gas Bill - Sep.pdf",
// "my_file2": "/Users/jeeva/Electricity Bill - Sep.pdf",
// "my_file3": "/Users/jeeva/Water Bill - Sep.pdf",
// })
func (r *Request) SetFiles(files map[string]string) *Request {
r.isMultiPart = true
for f, fp := range files {
r.FormData.Set("@"+f, fp)
}
return r
}
// SetFileReader method is to set single file using io.Reader for multipart upload.
// client.R().
// SetFileReader("profile_img", "my-profile-img.png", bytes.NewReader(profileImgBytes)).
// SetFileReader("notes", "user-notes.txt", bytes.NewReader(notesBytes))
func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Request {
r.isMultiPart = true
r.multipartFiles = append(r.multipartFiles, &File{
Name: fileName,
ParamName: param,
Reader: reader,
})
return r
}
// SetMultipartFormData method allows simple form data to be attached to the request as `multipart:form-data`
func (r *Request) SetMultipartFormData(data map[string]string) *Request {
for k, v := range data {
r = r.SetMultipartField(k, "", "", strings.NewReader(v))
}
return r
}
// SetMultipartField method is to set custom data using io.Reader for multipart upload.
func (r *Request) SetMultipartField(param, fileName, contentType string, reader io.Reader) *Request {
r.isMultiPart = true
r.multipartFields = append(r.multipartFields, &MultipartField{
Param: param,
FileName: fileName,
ContentType: contentType,
Reader: reader,
})
return r
}
// SetMultipartFields method is to set multiple data fields using io.Reader for multipart upload.
//
// For Example:
// client.R().SetMultipartFields(
// &resty.MultipartField{
// Param: "uploadManifest1",
// FileName: "upload-file-1.json",
// ContentType: "application/json",
// Reader: strings.NewReader(`{"input": {"name": "Uploaded document 1", "_filename" : ["file1.txt"]}}`),
// },
// &resty.MultipartField{
// Param: "uploadManifest2",
// FileName: "upload-file-2.json",
// ContentType: "application/json",
// Reader: strings.NewReader(`{"input": {"name": "Uploaded document 2", "_filename" : ["file2.txt"]}}`),
// })
//
// If you have slice already, then simply call-
// client.R().SetMultipartFields(fields...)
func (r *Request) SetMultipartFields(fields ...*MultipartField) *Request {
r.isMultiPart = true
r.multipartFields = append(r.multipartFields, fields...)
return r
}
// SetContentLength method sets the HTTP header `Content-Length` value for current request.
// By default Resty won't set `Content-Length`. Also you have an option to enable for every
// request.
//
// See `Client.SetContentLength`
// client.R().SetContentLength(true)
func (r *Request) SetContentLength(l bool) *Request {
r.setContentLength = l
return r
}
// SetBasicAuth method sets the basic authentication header in the current HTTP request.
//
// For Example:
// Authorization: Basic <base64-encoded-value>
//
// To set the header for username "go-resty" and password "welcome"
// client.R().SetBasicAuth("go-resty", "welcome")
//
// This method overrides the credentials set by method `Client.SetBasicAuth`.
func (r *Request) SetBasicAuth(username, password string) *Request {
r.UserInfo = &User{Username: username, Password: password}
return r
}
// SetAuthToken method sets the auth token header(Default Scheme: Bearer) in the current HTTP request. Header example:
// Authorization: Bearer <auth-token-value-comes-here>
//
// For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F
//
// client.R().SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
//
// This method overrides the Auth token set by method `Client.SetAuthToken`.
func (r *Request) SetAuthToken(token string) *Request {
r.Token = token
return r
}
// SetAuthScheme method sets the auth token scheme type in the HTTP request. For Example:
// Authorization: <auth-scheme-value-set-here> <auth-token-value>
//
// For Example: To set the scheme to use OAuth
//
// client.R().SetAuthScheme("OAuth")
//
// This auth header scheme gets added to all the request rasied from this client instance.
// Also it can be overridden or set one at the request level is supported.
//
// Information about Auth schemes can be found in RFC7235 which is linked to below along with the page containing
// the currently defined official authentication schemes:
// https://tools.ietf.org/html/rfc7235
// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes
//
// This method overrides the Authorization scheme set by method `Client.SetAuthScheme`.
func (r *Request) SetAuthScheme(scheme string) *Request {
r.AuthScheme = scheme
return r
}
// SetOutput method sets the output file for current HTTP request. Current HTTP response will be
// saved into given file. It is similar to `curl -o` flag. Absolute path or relative path can be used.
// If is it relative path then output file goes under the output directory, as mentioned
// in the `Client.SetOutputDirectory`.
// client.R().
// SetOutput("/Users/jeeva/Downloads/ReplyWithHeader-v5.1-beta.zip").
// Get("http://bit.ly/1LouEKr")
//
// Note: In this scenario `Response.Body` might be nil.
func (r *Request) SetOutput(file string) *Request {
r.outputFile = file
r.isSaveResponse = true
return r
}
// SetSRV method sets the details to query the service SRV record and execute the
// request.
// client.R().
// SetSRV(SRVRecord{"web", "testservice.com"}).
// Get("/get")
func (r *Request) SetSRV(srv *SRVRecord) *Request {
r.SRV = srv
return r
}
// SetDoNotParseResponse method instructs `Resty` not to parse the response body automatically.
// Resty exposes the raw response body as `io.ReadCloser`. Also do not forget to close the body,
// otherwise you might get into connection leaks, no connection reuse.
//
// Note: Response middlewares are not applicable, if you use this option. Basically you have
// taken over the control of response parsing from `Resty`.
func (r *Request) SetDoNotParseResponse(parse bool) *Request {
r.notParseResponse = parse
return r
}
// SetPathParam method sets single URL path key-value pair in the
// Resty current request instance.
// client.R().SetPathParam("userId", "sample@sample.com")
//
// Result:
// URL - /v1/users/{userId}/details
// Composed URL - /v1/users/sample@sample.com/details
// It replaces the value of the key while composing the request URL. Also you can
// override Path Params value, which was set at client instance level.
func (r *Request) SetPathParam(param, value string) *Request {
r.PathParams[param] = value
return r
}
// SetPathParams method sets multiple URL path key-value pairs at one go in the
// Resty current request instance.
// client.R().SetPathParams(map[string]string{
// "userId": "sample@sample.com",
// "subAccountId": "100002",
// })
//
// Result:
// URL - /v1/users/{userId}/{subAccountId}/details
// Composed URL - /v1/users/sample@sample.com/100002/details
// It replaces the value of the key while composing request URL. Also you can
// override Path Params value, which was set at client instance level.
func (r *Request) SetPathParams(params map[string]string) *Request {
for p, v := range params {
r.SetPathParam(p, v)
}
return r
}
// ExpectContentType method allows to provide fallback `Content-Type` for automatic unmarshalling
// when `Content-Type` response header is unavailable.
func (r *Request) ExpectContentType(contentType string) *Request {
r.fallbackContentType = contentType
return r
}
// ForceContentType method provides a strong sense of response `Content-Type` for automatic unmarshalling.
// Resty gives this a higher priority than the `Content-Type` response header. This means that if both
// `Request.ForceContentType` is set and the response `Content-Type` is available, `ForceContentType` will win.
func (r *Request) ForceContentType(contentType string) *Request {
r.forceContentType = contentType
return r
}
// SetJSONEscapeHTML method is to enable/disable the HTML escape on JSON marshal.
//
// Note: This option only applicable to standard JSON Marshaller.
func (r *Request) SetJSONEscapeHTML(b bool) *Request {
r.jsonEscapeHTML = b
return r
}
// SetCookie method appends a single cookie in the current request instance.
// client.R().SetCookie(&http.Cookie{
// Name:"go-resty",
// Value:"This is cookie value",
// })
//
// Note: Method appends the Cookie value into existing Cookie if already existing.
//
// Since v2.1.0
func (r *Request) SetCookie(hc *http.Cookie) *Request {
r.Cookies = append(r.Cookies, hc)
return r
}
// SetCookies method sets an array of cookies in the current request instance.
// cookies := []*http.Cookie{
// &http.Cookie{
// Name:"go-resty-1",
// Value:"This is cookie 1 value",
// },
// &http.Cookie{
// Name:"go-resty-2",
// Value:"This is cookie 2 value",
// },
// }
//
// // Setting a cookies into resty's current request
// client.R().SetCookies(cookies)
//
// Note: Method appends the Cookie value into existing Cookie if already existing.
//
// Since v2.1.0
func (r *Request) SetCookies(rs []*http.Cookie) *Request {
r.Cookies = append(r.Cookies, rs...)
return r
}
// AddRetryCondition method adds a retry condition function to the request's
// array of functions that are checked to determine if the request is retried.
// The request will retry if any of the functions return true and error is nil.
//
// Note: These retry conditions are checked before all retry conditions of the client.
//
// Since v2.7.0
func (r *Request) AddRetryCondition(condition RetryConditionFunc) *Request {
r.retryConditions = append(r.retryConditions, condition)
return r
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// HTTP request tracing
//_______________________________________________________________________
// EnableTrace method enables trace for the current request
// using `httptrace.ClientTrace` and provides insights.
//
// client := resty.New()
//
// resp, err := client.R().EnableTrace().Get("https://httpbin.org/get")
// fmt.Println("Error:", err)
// fmt.Println("Trace Info:", resp.Request.TraceInfo())
//
// See `Client.EnableTrace` available too to get trace info for all requests.
//
// Since v2.0.0
func (r *Request) EnableTrace() *Request {
r.trace = true
return r
}
// TraceInfo method returns the trace info for the request.
// If either the Client or Request EnableTrace function has not been called
// prior to the request being made, an empty TraceInfo object will be returned.
//
// Since v2.0.0
func (r *Request) TraceInfo() TraceInfo {
ct := r.clientTrace
if ct == nil {
return TraceInfo{}
}
ti := TraceInfo{
DNSLookup: ct.dnsDone.Sub(ct.dnsStart),
TLSHandshake: ct.tlsHandshakeDone.Sub(ct.tlsHandshakeStart),
ServerTime: ct.gotFirstResponseByte.Sub(ct.gotConn),
IsConnReused: ct.gotConnInfo.Reused,
IsConnWasIdle: ct.gotConnInfo.WasIdle,
ConnIdleTime: ct.gotConnInfo.IdleTime,
RequestAttempt: r.Attempt,
}
// Calculate the total time accordingly,
// when connection is reused
if ct.gotConnInfo.Reused {
ti.TotalTime = ct.endTime.Sub(ct.getConn)
} else {
ti.TotalTime = ct.endTime.Sub(ct.dnsStart)
}
// Only calculate on successful connections
if !ct.connectDone.IsZero() {
ti.TCPConnTime = ct.connectDone.Sub(ct.dnsDone)
}
// Only calculate on successful connections
if !ct.gotConn.IsZero() {
ti.ConnTime = ct.gotConn.Sub(ct.getConn)
}
// Only calculate on successful connections
if !ct.gotFirstResponseByte.IsZero() {
ti.ResponseTime = ct.endTime.Sub(ct.gotFirstResponseByte)
}
// Capture remote address info when connection is non-nil
if ct.gotConnInfo.Conn != nil {
ti.RemoteAddr = ct.gotConnInfo.Conn.RemoteAddr()
}
return ti
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// HTTP verb method starts here
//_______________________________________________________________________
// Get method does GET HTTP request. It's defined in section 4.3.1 of RFC7231.
func (r *Request) Get(url string) (*Response, error) {
return r.Execute(MethodGet, url)
}
// Head method does HEAD HTTP request. It's defined in section 4.3.2 of RFC7231.
func (r *Request) Head(url string) (*Response, error) {
return r.Execute(MethodHead, url)
}
// Post method does POST HTTP request. It's defined in section 4.3.3 of RFC7231.
func (r *Request) Post(url string) (*Response, error) {
return r.Execute(MethodPost, url)
}
// Put method does PUT HTTP request. It's defined in section 4.3.4 of RFC7231.
func (r *Request) Put(url string) (*Response, error) {
return r.Execute(MethodPut, url)
}
// Delete method does DELETE HTTP request. It's defined in section 4.3.5 of RFC7231.
func (r *Request) Delete(url string) (*Response, error) {
return r.Execute(MethodDelete, url)
}
// Options method does OPTIONS HTTP request. It's defined in section 4.3.7 of RFC7231.
func (r *Request) Options(url string) (*Response, error) {
return r.Execute(MethodOptions, url)
}
// Patch method does PATCH HTTP request. It's defined in section 2 of RFC5789.
func (r *Request) Patch(url string) (*Response, error) {
return r.Execute(MethodPatch, url)
}
// Send method performs the HTTP request using the method and URL already defined
// for current `Request`.
// req := client.R()
// req.Method = resty.GET
// req.URL = "http://httpbin.org/get"
// resp, err := client.R().Send()
func (r *Request) Send() (*Response, error) {
return r.Execute(r.Method, r.URL)
}
// Execute method performs the HTTP request with given HTTP method and URL
// for current `Request`.
// resp, err := client.R().Execute(resty.GET, "http://httpbin.org/get")
func (r *Request) Execute(method, url string) (*Response, error) {
var addrs []*net.SRV
var resp *Response
var err error
if r.isMultiPart && !(method == MethodPost || method == MethodPut || method == MethodPatch) {
// No OnError hook here since this is a request validation error
return nil, fmt.Errorf("multipart content is not allowed in HTTP verb [%v]", method)
}
if r.SRV != nil {
_, addrs, err = net.LookupSRV(r.SRV.Service, "tcp", r.SRV.Domain)
if err != nil {
r.client.onErrorHooks(r, nil, err)
return nil, err
}
}
r.Method = method
r.URL = r.selectAddr(addrs, url, 0)
if r.client.RetryCount == 0 {
r.Attempt = 1
resp, err = r.client.execute(r)
r.client.onErrorHooks(r, resp, unwrapNoRetryErr(err))
return resp, unwrapNoRetryErr(err)
}
err = Backoff(
func() (*Response, error) {
r.Attempt++
r.URL = r.selectAddr(addrs, url, r.Attempt)
resp, err = r.client.execute(r)
if err != nil {
r.client.log.Errorf("%v, Attempt %v", err, r.Attempt)
}
return resp, err
},
Retries(r.client.RetryCount),
WaitTime(r.client.RetryWaitTime),
MaxWaitTime(r.client.RetryMaxWaitTime),
RetryConditions(append(r.retryConditions, r.client.RetryConditions...)),
RetryHooks(r.client.RetryHooks),
)
r.client.onErrorHooks(r, resp, unwrapNoRetryErr(err))
return resp, unwrapNoRetryErr(err)
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// SRVRecord struct
//_______________________________________________________________________
// SRVRecord struct holds the data to query the SRV record for the
// following service.
type SRVRecord struct {
Service string
Domain string
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Request Unexported methods
//_______________________________________________________________________
func (r *Request) fmtBodyString(sl int64) (body string) {
body = "***** NO CONTENT *****"
if !isPayloadSupported(r.Method, r.client.AllowGetMethodPayload) {
return
}
if _, ok := r.Body.(io.Reader); ok {
body = "***** BODY IS io.Reader *****"
return
}
// multipart or form-data
if r.isMultiPart || r.isFormData {
bodySize := int64(r.bodyBuf.Len())
if bodySize > sl {
body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize)
return
}
body = r.bodyBuf.String()
return
}
// request body data
if r.Body == nil {
return
}
var prtBodyBytes []byte
var err error
contentType := r.Header.Get(hdrContentTypeKey)
kind := kindOf(r.Body)
if canJSONMarshal(contentType, kind) {
prtBodyBytes, err = json.MarshalIndent(&r.Body, "", " ")
} else if IsXMLType(contentType) && (kind == reflect.Struct) {
prtBodyBytes, err = xml.MarshalIndent(&r.Body, "", " ")
} else if b, ok := r.Body.(string); ok {
if IsJSONType(contentType) {
bodyBytes := []byte(b)
out := acquireBuffer()
defer releaseBuffer(out)
if err = json.Indent(out, bodyBytes, "", " "); err == nil {
prtBodyBytes = out.Bytes()
}
} else {
body = b
}
} else if b, ok := r.Body.([]byte); ok {
body = fmt.Sprintf("***** BODY IS byte(s) (size - %d) *****", len(b))
return
}
if prtBodyBytes != nil && err == nil {
body = string(prtBodyBytes)
}
if len(body) > 0 {
bodySize := int64(len([]byte(body)))
if bodySize > sl {
body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize)
}
}
return
}
func (r *Request) selectAddr(addrs []*net.SRV, path string, attempt int) string {
if addrs == nil {
return path
}
idx := attempt % len(addrs)
domain := strings.TrimRight(addrs[idx].Target, ".")
path = strings.TrimLeft(path, "/")
return fmt.Sprintf("%s://%s:%d/%s", r.client.scheme, domain, addrs[idx].Port, path)
}
func (r *Request) initValuesMap() {
if r.values == nil {
r.values = make(map[string]interface{})
}
}
var noescapeJSONMarshal = func(v interface{}) (*bytes.Buffer, error) {
buf := acquireBuffer()
encoder := json.NewEncoder(buf)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(v); err != nil {
releaseBuffer(buf)
return nil, err
}
return buf, nil
}

View file

@ -1,175 +0,0 @@
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package resty
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Response struct and methods
//_______________________________________________________________________
// Response struct holds response values of executed request.
type Response struct {
Request *Request
RawResponse *http.Response
body []byte
size int64
receivedAt time.Time
}
// Body method returns HTTP response as []byte array for the executed request.
//
// Note: `Response.Body` might be nil, if `Request.SetOutput` is used.
func (r *Response) Body() []byte {
if r.RawResponse == nil {
return []byte{}
}
return r.body
}
// Status method returns the HTTP status string for the executed request.
// Example: 200 OK
func (r *Response) Status() string {
if r.RawResponse == nil {
return ""
}
return r.RawResponse.Status
}
// StatusCode method returns the HTTP status code for the executed request.
// Example: 200
func (r *Response) StatusCode() int {
if r.RawResponse == nil {
return 0
}
return r.RawResponse.StatusCode
}
// Proto method returns the HTTP response protocol used for the request.
func (r *Response) Proto() string {
if r.RawResponse == nil {
return ""
}
return r.RawResponse.Proto
}
// Result method returns the response value as an object if it has one
func (r *Response) Result() interface{} {
return r.Request.Result
}
// Error method returns the error object if it has one
func (r *Response) Error() interface{} {
return r.Request.Error
}
// Header method returns the response headers
func (r *Response) Header() http.Header {
if r.RawResponse == nil {
return http.Header{}
}
return r.RawResponse.Header
}
// Cookies method to access all the response cookies
func (r *Response) Cookies() []*http.Cookie {
if r.RawResponse == nil {
return make([]*http.Cookie, 0)
}
return r.RawResponse.Cookies()
}
// String method returns the body of the server response as String.
func (r *Response) String() string {
if r.body == nil {
return ""
}
return strings.TrimSpace(string(r.body))
}
// Time method returns the time of HTTP response time that from request we sent and received a request.
//
// See `Response.ReceivedAt` to know when client received response and see `Response.Request.Time` to know
// when client sent a request.
func (r *Response) Time() time.Duration {
if r.Request.clientTrace != nil {
return r.Request.TraceInfo().TotalTime
}
return r.receivedAt.Sub(r.Request.Time)
}
// ReceivedAt method returns when response got received from server for the request.
func (r *Response) ReceivedAt() time.Time {
return r.receivedAt
}
// Size method returns the HTTP response size in bytes. Ya, you can relay on HTTP `Content-Length` header,
// however it won't be good for chucked transfer/compressed response. Since Resty calculates response size
// at the client end. You will get actual size of the http response.
func (r *Response) Size() int64 {
return r.size
}
// RawBody method exposes the HTTP raw response body. Use this method in-conjunction with `SetDoNotParseResponse`
// option otherwise you get an error as `read err: http: read on closed response body`.
//
// Do not forget to close the body, otherwise you might get into connection leaks, no connection reuse.
// Basically you have taken over the control of response parsing from `Resty`.
func (r *Response) RawBody() io.ReadCloser {
if r.RawResponse == nil {
return nil
}
return r.RawResponse.Body
}
// IsSuccess method returns true if HTTP status `code >= 200 and <= 299` otherwise false.
func (r *Response) IsSuccess() bool {
return r.StatusCode() > 199 && r.StatusCode() < 300
}
// IsError method returns true if HTTP status `code >= 400` otherwise false.
func (r *Response) IsError() bool {
return r.StatusCode() > 399
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Response Unexported methods
//_______________________________________________________________________
func (r *Response) setReceivedAt() {
r.receivedAt = time.Now()
if r.Request.clientTrace != nil {
r.Request.clientTrace.endTime = r.receivedAt
}
}
func (r *Response) fmtBodyString(sl int64) string {
if r.body != nil {
if int64(len(r.body)) > sl {
return fmt.Sprintf("***** RESPONSE TOO LARGE (size - %d) *****", len(r.body))
}
ct := r.Header().Get(hdrContentTypeKey)
if IsJSONType(ct) {
out := acquireBuffer()
defer releaseBuffer(out)
err := json.Indent(out, r.body, "", " ")
if err != nil {
return fmt.Sprintf("*** Error: Unable to format response body - \"%s\" ***\n\nLog Body as-is:\n%s", err, r.String())
}
return out.String()
}
return r.String()
}
return "***** NO CONTENT *****"
}

View file

@ -1,40 +0,0 @@
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
// Package resty provides Simple HTTP and REST client library for Go.
package resty
import (
"net"
"net/http"
"net/http/cookiejar"
"golang.org/x/net/publicsuffix"
)
// Version # of resty
const Version = "2.7.0"
// New method creates a new Resty client.
func New() *Client {
cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
return createClient(&http.Client{
Jar: cookieJar,
})
}
// NewWithClient method creates a new Resty client with given `http.Client`.
func NewWithClient(hc *http.Client) *Client {
return createClient(hc)
}
// NewWithLocalAddr method creates a new Resty client with given Local Address
// to dial from.
func NewWithLocalAddr(localAddr net.Addr) *Client {
cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
return createClient(&http.Client{
Jar: cookieJar,
Transport: createTransport(localAddr),
})
}

View file

@ -1,221 +0,0 @@
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package resty
import (
"context"
"math"
"math/rand"
"sync"
"time"
)
const (
defaultMaxRetries = 3
defaultWaitTime = time.Duration(100) * time.Millisecond
defaultMaxWaitTime = time.Duration(2000) * time.Millisecond
)
type (
// Option is to create convenient retry options like wait time, max retries, etc.
Option func(*Options)
// RetryConditionFunc type is for retry condition function
// input: non-nil Response OR request execution error
RetryConditionFunc func(*Response, error) bool
// OnRetryFunc is for side-effecting functions triggered on retry
OnRetryFunc func(*Response, error)
// RetryAfterFunc returns time to wait before retry
// For example, it can parse HTTP Retry-After header
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
// Non-nil error is returned if it is found that request is not retryable
// (0, nil) is a special result means 'use default algorithm'
RetryAfterFunc func(*Client, *Response) (time.Duration, error)
// Options struct is used to hold retry settings.
Options struct {
maxRetries int
waitTime time.Duration
maxWaitTime time.Duration
retryConditions []RetryConditionFunc
retryHooks []OnRetryFunc
}
)
// Retries sets the max number of retries
func Retries(value int) Option {
return func(o *Options) {
o.maxRetries = value
}
}
// WaitTime sets the default wait time to sleep between requests
func WaitTime(value time.Duration) Option {
return func(o *Options) {
o.waitTime = value
}
}
// MaxWaitTime sets the max wait time to sleep between requests
func MaxWaitTime(value time.Duration) Option {
return func(o *Options) {
o.maxWaitTime = value
}
}
// RetryConditions sets the conditions that will be checked for retry.
func RetryConditions(conditions []RetryConditionFunc) Option {
return func(o *Options) {
o.retryConditions = conditions
}
}
// RetryHooks sets the hooks that will be executed after each retry
func RetryHooks(hooks []OnRetryFunc) Option {
return func(o *Options) {
o.retryHooks = hooks
}
}
// Backoff retries with increasing timeout duration up until X amount of retries
// (Default is 3 attempts, Override with option Retries(n))
func Backoff(operation func() (*Response, error), options ...Option) error {
// Defaults
opts := Options{
maxRetries: defaultMaxRetries,
waitTime: defaultWaitTime,
maxWaitTime: defaultMaxWaitTime,
retryConditions: []RetryConditionFunc{},
}
for _, o := range options {
o(&opts)
}
var (
resp *Response
err error
)
for attempt := 0; attempt <= opts.maxRetries; attempt++ {
resp, err = operation()
ctx := context.Background()
if resp != nil && resp.Request.ctx != nil {
ctx = resp.Request.ctx
}
if ctx.Err() != nil {
return err
}
err1 := unwrapNoRetryErr(err) // raw error, it used for return users callback.
needsRetry := err != nil && err == err1 // retry on a few operation errors by default
for _, condition := range opts.retryConditions {
needsRetry = condition(resp, err1)
if needsRetry {
break
}
}
if !needsRetry {
return err
}
for _, hook := range opts.retryHooks {
hook(resp, err)
}
// Don't need to wait when no retries left.
// Still run retry hooks even on last retry to keep compatibility.
if attempt == opts.maxRetries {
return err
}
waitTime, err2 := sleepDuration(resp, opts.waitTime, opts.maxWaitTime, attempt)
if err2 != nil {
if err == nil {
err = err2
}
return err
}
select {
case <-time.After(waitTime):
case <-ctx.Done():
return ctx.Err()
}
}
return err
}
func sleepDuration(resp *Response, min, max time.Duration, attempt int) (time.Duration, error) {
const maxInt = 1<<31 - 1 // max int for arch 386
if max < 0 {
max = maxInt
}
if resp == nil {
return jitterBackoff(min, max, attempt), nil
}
retryAfterFunc := resp.Request.client.RetryAfter
// Check for custom callback
if retryAfterFunc == nil {
return jitterBackoff(min, max, attempt), nil
}
result, err := retryAfterFunc(resp.Request.client, resp)
if err != nil {
return 0, err // i.e. 'API quota exceeded'
}
if result == 0 {
return jitterBackoff(min, max, attempt), nil
}
if result < 0 || max < result {
result = max
}
if result < min {
result = min
}
return result, nil
}
// Return capped exponential backoff with jitter
// http://www.awsarchitectureblog.com/2015/03/backoff.html
func jitterBackoff(min, max time.Duration, attempt int) time.Duration {
base := float64(min)
capLevel := float64(max)
temp := math.Min(capLevel, base*math.Exp2(float64(attempt)))
ri := time.Duration(temp / 2)
result := randDuration(ri)
if result < min {
result = min
}
return result
}
var rnd = newRnd()
var rndMu sync.Mutex
func randDuration(center time.Duration) time.Duration {
rndMu.Lock()
defer rndMu.Unlock()
var ri = int64(center)
var jitter = rnd.Int63n(ri)
return time.Duration(math.Abs(float64(ri + jitter)))
}
func newRnd() *rand.Rand {
var seed = time.Now().UnixNano()
var src = rand.NewSource(seed)
return rand.New(src)
}

View file

@ -1,130 +0,0 @@
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package resty
import (
"context"
"crypto/tls"
"net"
"net/http/httptrace"
"time"
)
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// TraceInfo struct
//_______________________________________________________________________
// TraceInfo struct is used provide request trace info such as DNS lookup
// duration, Connection obtain duration, Server processing duration, etc.
//
// Since v2.0.0
type TraceInfo struct {
// DNSLookup is a duration that transport took to perform
// DNS lookup.
DNSLookup time.Duration
// ConnTime is a duration that took to obtain a successful connection.
ConnTime time.Duration
// TCPConnTime is a duration that took to obtain the TCP connection.
TCPConnTime time.Duration
// TLSHandshake is a duration that TLS handshake took place.
TLSHandshake time.Duration
// ServerTime is a duration that server took to respond first byte.
ServerTime time.Duration
// ResponseTime is a duration since first response byte from server to
// request completion.
ResponseTime time.Duration
// TotalTime is a duration that total request took end-to-end.
TotalTime time.Duration
// IsConnReused is whether this connection has been previously
// used for another HTTP request.
IsConnReused bool
// IsConnWasIdle is whether this connection was obtained from an
// idle pool.
IsConnWasIdle bool
// ConnIdleTime is a duration how long the connection was previously
// idle, if IsConnWasIdle is true.
ConnIdleTime time.Duration
// RequestAttempt is to represent the request attempt made during a Resty
// request execution flow, including retry count.
RequestAttempt int
// RemoteAddr returns the remote network address.
RemoteAddr net.Addr
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// ClientTrace struct and its methods
//_______________________________________________________________________
// tracer struct maps the `httptrace.ClientTrace` hooks into Fields
// with same naming for easy understanding. Plus additional insights
// Request.
type clientTrace struct {
getConn time.Time
dnsStart time.Time
dnsDone time.Time
connectDone time.Time
tlsHandshakeStart time.Time
tlsHandshakeDone time.Time
gotConn time.Time
gotFirstResponseByte time.Time
endTime time.Time
gotConnInfo httptrace.GotConnInfo
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Trace unexported methods
//_______________________________________________________________________
func (t *clientTrace) createContext(ctx context.Context) context.Context {
return httptrace.WithClientTrace(
ctx,
&httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) {
t.dnsStart = time.Now()
},
DNSDone: func(_ httptrace.DNSDoneInfo) {
t.dnsDone = time.Now()
},
ConnectStart: func(_, _ string) {
if t.dnsDone.IsZero() {
t.dnsDone = time.Now()
}
if t.dnsStart.IsZero() {
t.dnsStart = t.dnsDone
}
},
ConnectDone: func(net, addr string, err error) {
t.connectDone = time.Now()
},
GetConn: func(_ string) {
t.getConn = time.Now()
},
GotConn: func(ci httptrace.GotConnInfo) {
t.gotConn = time.Now()
t.gotConnInfo = ci
},
GotFirstResponseByte: func() {
t.gotFirstResponseByte = time.Now()
},
TLSHandshakeStart: func() {
t.tlsHandshakeStart = time.Now()
},
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
t.tlsHandshakeDone = time.Now()
},
},
)
}

View file

@ -1,35 +0,0 @@
// +build go1.13
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package resty
import (
"net"
"net/http"
"runtime"
"time"
)
func createTransport(localAddr net.Addr) *http.Transport {
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}
if localAddr != nil {
dialer.LocalAddr = localAddr
}
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialer.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
}
}

View file

@ -1,34 +0,0 @@
// +build !go1.13
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package resty
import (
"net"
"net/http"
"runtime"
"time"
)
func createTransport(localAddr net.Addr) *http.Transport {
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}
if localAddr != nil {
dialer.LocalAddr = localAddr
}
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialer.DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
}
}

View file

@ -1,391 +0,0 @@
// Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package resty
import (
"bytes"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"sync"
)
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Logger interface
//_______________________________________________________________________
// Logger interface is to abstract the logging from Resty. Gives control to
// the Resty users, choice of the logger.
type Logger interface {
Errorf(format string, v ...interface{})
Warnf(format string, v ...interface{})
Debugf(format string, v ...interface{})
}
func createLogger() *logger {
l := &logger{l: log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds)}
return l
}
var _ Logger = (*logger)(nil)
type logger struct {
l *log.Logger
}
func (l *logger) Errorf(format string, v ...interface{}) {
l.output("ERROR RESTY "+format, v...)
}
func (l *logger) Warnf(format string, v ...interface{}) {
l.output("WARN RESTY "+format, v...)
}
func (l *logger) Debugf(format string, v ...interface{}) {
l.output("DEBUG RESTY "+format, v...)
}
func (l *logger) output(format string, v ...interface{}) {
if len(v) == 0 {
l.l.Print(format)
return
}
l.l.Printf(format, v...)
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Package Helper methods
//_______________________________________________________________________
// IsStringEmpty method tells whether given string is empty or not
func IsStringEmpty(str string) bool {
return len(strings.TrimSpace(str)) == 0
}
// DetectContentType method is used to figure out `Request.Body` content type for request header
func DetectContentType(body interface{}) string {
contentType := plainTextType
kind := kindOf(body)
switch kind {
case reflect.Struct, reflect.Map:
contentType = jsonContentType
case reflect.String:
contentType = plainTextType
default:
if b, ok := body.([]byte); ok {
contentType = http.DetectContentType(b)
} else if kind == reflect.Slice {
contentType = jsonContentType
}
}
return contentType
}
// IsJSONType method is to check JSON content type or not
func IsJSONType(ct string) bool {
return jsonCheck.MatchString(ct)
}
// IsXMLType method is to check XML content type or not
func IsXMLType(ct string) bool {
return xmlCheck.MatchString(ct)
}
// Unmarshalc content into object from JSON or XML
func Unmarshalc(c *Client, ct string, b []byte, d interface{}) (err error) {
if IsJSONType(ct) {
err = c.JSONUnmarshal(b, d)
} else if IsXMLType(ct) {
err = c.XMLUnmarshal(b, d)
}
return
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// RequestLog and ResponseLog type
//_______________________________________________________________________
// RequestLog struct is used to collected information from resty request
// instance for debug logging. It sent to request log callback before resty
// actually logs the information.
type RequestLog struct {
Header http.Header
Body string
}
// ResponseLog struct is used to collected information from resty response
// instance for debug logging. It sent to response log callback before resty
// actually logs the information.
type ResponseLog struct {
Header http.Header
Body string
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Package Unexported methods
//_______________________________________________________________________
// way to disable the HTML escape as opt-in
func jsonMarshal(c *Client, r *Request, d interface{}) (*bytes.Buffer, error) {
if !r.jsonEscapeHTML || !c.jsonEscapeHTML {
return noescapeJSONMarshal(d)
}
data, err := c.JSONMarshal(d)
if err != nil {
return nil, err
}
buf := acquireBuffer()
_, _ = buf.Write(data)
return buf, nil
}
func firstNonEmpty(v ...string) string {
for _, s := range v {
if !IsStringEmpty(s) {
return s
}
}
return ""
}
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}
func createMultipartHeader(param, fileName, contentType string) textproto.MIMEHeader {
hdr := make(textproto.MIMEHeader)
var contentDispositionValue string
if IsStringEmpty(fileName) {
contentDispositionValue = fmt.Sprintf(`form-data; name="%s"`, param)
} else {
contentDispositionValue = fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
param, escapeQuotes(fileName))
}
hdr.Set("Content-Disposition", contentDispositionValue)
if !IsStringEmpty(contentType) {
hdr.Set(hdrContentTypeKey, contentType)
}
return hdr
}
func addMultipartFormField(w *multipart.Writer, mf *MultipartField) error {
partWriter, err := w.CreatePart(createMultipartHeader(mf.Param, mf.FileName, mf.ContentType))
if err != nil {
return err
}
_, err = io.Copy(partWriter, mf.Reader)
return err
}
func writeMultipartFormFile(w *multipart.Writer, fieldName, fileName string, r io.Reader) error {
// Auto detect actual multipart content type
cbuf := make([]byte, 512)
size, err := r.Read(cbuf)
if err != nil && err != io.EOF {
return err
}
partWriter, err := w.CreatePart(createMultipartHeader(fieldName, fileName, http.DetectContentType(cbuf)))
if err != nil {
return err
}
if _, err = partWriter.Write(cbuf[:size]); err != nil {
return err
}
_, err = io.Copy(partWriter, r)
return err
}
func addFile(w *multipart.Writer, fieldName, path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer closeq(file)
return writeMultipartFormFile(w, fieldName, filepath.Base(path), file)
}
func addFileReader(w *multipart.Writer, f *File) error {
return writeMultipartFormFile(w, f.ParamName, f.Name, f.Reader)
}
func getPointer(v interface{}) interface{} {
vv := valueOf(v)
if vv.Kind() == reflect.Ptr {
return v
}
return reflect.New(vv.Type()).Interface()
}
func isPayloadSupported(m string, allowMethodGet bool) bool {
return !(m == MethodHead || m == MethodOptions || (m == MethodGet && !allowMethodGet))
}
func typeOf(i interface{}) reflect.Type {
return indirect(valueOf(i)).Type()
}
func valueOf(i interface{}) reflect.Value {
return reflect.ValueOf(i)
}
func indirect(v reflect.Value) reflect.Value {
return reflect.Indirect(v)
}
func kindOf(v interface{}) reflect.Kind {
return typeOf(v).Kind()
}
func createDirectory(dir string) (err error) {
if _, err = os.Stat(dir); err != nil {
if os.IsNotExist(err) {
if err = os.MkdirAll(dir, 0755); err != nil {
return
}
}
}
return
}
func canJSONMarshal(contentType string, kind reflect.Kind) bool {
return IsJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice)
}
func functionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
func acquireBuffer() *bytes.Buffer {
return bufPool.Get().(*bytes.Buffer)
}
func releaseBuffer(buf *bytes.Buffer) {
if buf != nil {
buf.Reset()
bufPool.Put(buf)
}
}
// requestBodyReleaser wraps requests's body and implements custom Close for it.
// The Close method closes original body and releases request body back to sync.Pool.
type requestBodyReleaser struct {
releaseOnce sync.Once
reqBuf *bytes.Buffer
io.ReadCloser
}
func newRequestBodyReleaser(respBody io.ReadCloser, reqBuf *bytes.Buffer) io.ReadCloser {
if reqBuf == nil {
return respBody
}
return &requestBodyReleaser{
reqBuf: reqBuf,
ReadCloser: respBody,
}
}
func (rr *requestBodyReleaser) Close() error {
err := rr.ReadCloser.Close()
rr.releaseOnce.Do(func() {
releaseBuffer(rr.reqBuf)
})
return err
}
func closeq(v interface{}) {
if c, ok := v.(io.Closer); ok {
silently(c.Close())
}
}
func silently(_ ...interface{}) {}
func composeHeaders(c *Client, r *Request, hdrs http.Header) string {
str := make([]string, 0, len(hdrs))
for _, k := range sortHeaderKeys(hdrs) {
var v string
if k == "Cookie" {
cv := strings.TrimSpace(strings.Join(hdrs[k], ", "))
if c.GetClient().Jar != nil {
for _, c := range c.GetClient().Jar.Cookies(r.RawRequest.URL) {
if cv != "" {
cv = cv + "; " + c.String()
} else {
cv = c.String()
}
}
}
v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, cv))
} else {
v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, strings.Join(hdrs[k], ", ")))
}
if v != "" {
str = append(str, "\t"+v)
}
}
return strings.Join(str, "\n")
}
func sortHeaderKeys(hdrs http.Header) []string {
keys := make([]string, 0, len(hdrs))
for key := range hdrs {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func copyHeaders(hdrs http.Header) http.Header {
nh := http.Header{}
for k, v := range hdrs {
nh[k] = v
}
return nh
}
type noRetryErr struct {
err error
}
func (e *noRetryErr) Error() string {
return e.err.Error()
}
func wrapNoRetryErr(err error) error {
if err != nil {
err = &noRetryErr{err: err}
}
return err
}
func unwrapNoRetryErr(err error) error {
if e, ok := err.(*noRetryErr); ok {
err = e.err
}
return err
}

3
vendor/modules.txt vendored
View file

@ -129,9 +129,6 @@ github.com/go-openapi/swag
# github.com/go-openapi/validate v0.22.1
## explicit; go 1.14
github.com/go-openapi/validate
# github.com/go-resty/resty/v2 v2.7.0
## explicit; go 1.11
github.com/go-resty/resty/v2
# github.com/go-sql-driver/mysql v1.7.0
## explicit; go 1.13
github.com/go-sql-driver/mysql