github.com/Axway/agent-sdk@v1.1.101/pkg/apic/apiserver/clients/api/v1/client.go (about)

     1  package v1
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"net/http"
     9  	"net/url"
    10  	"strings"
    11  	"time"
    12  
    13  	apiv1 "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/api/v1"
    14  	"github.com/Axway/agent-sdk/pkg/apic/auth"
    15  	"github.com/Axway/agent-sdk/pkg/config"
    16  	"github.com/tomnomnom/linkheader"
    17  )
    18  
    19  // HTTPClient allows you to replace the default client for different use cases
    20  func HTTPClient(client requestDoer) Options {
    21  	return func(c *ClientBase) {
    22  		c.client = client
    23  	}
    24  }
    25  
    26  // Authenticate Basic authentication
    27  func (ba *basicAuth) Authenticate(req *http.Request) error {
    28  	req.SetBasicAuth(ba.user, ba.pass)
    29  	req.Header.Set("X-Axway-Tenant-Id", ba.tenantID)
    30  	req.Header.Set("X-Axway-Instance-Id", ba.instanceID)
    31  	return nil
    32  }
    33  
    34  func (ba *basicAuth) impersonate(req *http.Request, toImpersonate string) error {
    35  	req.Header.Set("X-Axway-User-Id", toImpersonate)
    36  	return nil
    37  }
    38  
    39  // Authenticate JWT Authentication
    40  func (j *jwtAuth) Authenticate(req *http.Request) error {
    41  	t, err := j.tokenGetter.GetToken()
    42  	if err != nil {
    43  		return err
    44  	}
    45  	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t))
    46  	req.Header.Set("X-Axway-Tenant-Id", j.tenantID)
    47  	return nil
    48  }
    49  
    50  type modifier interface {
    51  	Modify()
    52  }
    53  
    54  // BasicAuth auth with user/pass
    55  func BasicAuth(user, password, tenantID, instanceID string) Options {
    56  	return func(c *ClientBase) {
    57  		ba := &basicAuth{
    58  			user:       user,
    59  			pass:       password,
    60  			tenantID:   tenantID,
    61  			instanceID: instanceID,
    62  		}
    63  
    64  		c.auth = ba
    65  		c.impersonator = ba
    66  	}
    67  }
    68  
    69  // JWTAuth auth with token
    70  func JWTAuth(tenantID, privKey, pubKey, password, url, aud, clientID string, timeout time.Duration) Options {
    71  	cfg := &config.CentralConfiguration{}
    72  	altConn := cfg.GetSingleURL()
    73  	return func(c *ClientBase) {
    74  		tokenGetter := auth.NewPlatformTokenGetter(privKey, pubKey, password, url, aud, clientID, altConn, timeout)
    75  		c.auth = &jwtAuth{
    76  			tenantID:    tenantID,
    77  			tokenGetter: tokenGetter,
    78  		}
    79  	}
    80  }
    81  
    82  type Logger interface {
    83  	Log(kv ...interface{}) error
    84  }
    85  
    86  type noOpLogger struct{}
    87  
    88  func (noOpLogger) Log(_ ...interface{}) error {
    89  	return nil
    90  }
    91  
    92  func WithLogger(log Logger) Options {
    93  	return func(cb *ClientBase) {
    94  		cb.client = loggingDoerWrapper{log, cb.client}
    95  	}
    96  }
    97  
    98  func UserAgent(ua string) Options {
    99  	return func(cb *ClientBase) {
   100  		cb.userAgent = ua
   101  	}
   102  }
   103  
   104  // NewClient creates a new HTTP client
   105  func NewClient(baseURL string, options ...Options) *ClientBase {
   106  	c := &ClientBase{
   107  		client:       &http.Client{},
   108  		url:          baseURL,
   109  		auth:         noopAuth{},
   110  		impersonator: noImpersonator{},
   111  		userAgent:    "",
   112  	}
   113  
   114  	for _, o := range options {
   115  		o(c)
   116  	}
   117  
   118  	return c
   119  }
   120  
   121  func (cb *ClientBase) intercept(req *http.Request) error {
   122  	if cb.userAgent != "" {
   123  		req.Header.Add("User-Agent", cb.userAgent)
   124  	}
   125  	return cb.auth.Authenticate(req)
   126  }
   127  
   128  func (cb *ClientBase) forKindInternal(gvk apiv1.GroupVersionKind) (*Client, error) {
   129  	resource, ok := apiv1.GetResource(gvk.GroupKind)
   130  	if !ok {
   131  		return nil, fmt.Errorf("no resource for gvk: %s", gvk)
   132  	}
   133  
   134  	sk, ok := apiv1.GetScope(gvk.GroupKind)
   135  	if !ok {
   136  		return nil, fmt.Errorf("no scope for gvk: %s", gvk)
   137  	}
   138  
   139  	scopeResource := ""
   140  
   141  	if sk != "" {
   142  		sGV := apiv1.GroupKind{Group: gvk.Group, Kind: sk}
   143  		scopeResource, ok = apiv1.GetResource(sGV)
   144  		if !ok {
   145  			return nil, fmt.Errorf("no resource for scope gv: %s", sGV)
   146  		}
   147  	}
   148  
   149  	return &Client{
   150  		ClientBase:    cb,
   151  		version:       gvk.APIVersion,
   152  		group:         gvk.Group,
   153  		resource:      resource,
   154  		scopeResource: scopeResource,
   155  	}, nil
   156  }
   157  
   158  // ForKindCtx registers a client with a given group/version
   159  func (cb *ClientBase) ForKindCtx(gvk apiv1.GroupVersionKind) (UnscopedCtx, error) {
   160  	c, err := cb.forKindInternal(gvk)
   161  	return &ClientCtx{*c}, err
   162  }
   163  
   164  // ForKind registers a client with a given group/version
   165  func (cb *ClientBase) ForKind(gvk apiv1.GroupVersionKind) (Unscoped, error) {
   166  	return cb.forKindInternal(gvk)
   167  }
   168  
   169  const (
   170  	// baseURL/group/version/scopeResource/scope/resource
   171  	scopedURLFormat   = "%s/%s/%s/%s/%s/%s"
   172  	unscopedURLFormat = "%s/%s/%s/%s"
   173  )
   174  
   175  type ClientCtx struct {
   176  	Client
   177  }
   178  
   179  func (c *ClientCtx) WithScope(scope string) ScopedCtx {
   180  	return c
   181  }
   182  
   183  // handleError handles an api-server error response. caller should close body.
   184  func handleError(res *http.Response) error {
   185  	var errors Errors
   186  	errRes := apiv1.ErrorResponse{}
   187  	err := json.NewDecoder(res.Body).Decode(&errRes)
   188  	if err != nil {
   189  		errors = []apiv1.Error{{
   190  			Status: 0,
   191  			Detail: err.Error(),
   192  		}}
   193  	} else {
   194  		errors = errRes.Errors
   195  	}
   196  
   197  	switch res.StatusCode {
   198  	case 400:
   199  		return BadRequestError{errors}
   200  	case 401:
   201  		return UnauthorizedError{errors}
   202  	case 403:
   203  		return ForbiddenError{errors}
   204  	case 404:
   205  		return NotFoundError{errors}
   206  	case 409:
   207  		return ConflictError{errors}
   208  	case 500:
   209  		return InternalServerError{errors}
   210  	default:
   211  		return UnexpectedError{res.StatusCode, errors}
   212  	}
   213  }
   214  
   215  func (c *Client) url(rm apiv1.ResourceMeta) string {
   216  	if c.scopeResource != "" {
   217  		scope := c.scope
   218  		if c.scope == "" {
   219  			scope = rm.Metadata.Scope.Name
   220  		}
   221  
   222  		return fmt.Sprintf(scopedURLFormat, c.ClientBase.url, c.group, c.version, c.scopeResource, scope, c.resource)
   223  	}
   224  
   225  	return fmt.Sprintf(unscopedURLFormat, c.ClientBase.url, c.group, c.version, c.resource)
   226  }
   227  
   228  func (c *Client) urlForResource(rm apiv1.ResourceMeta) string {
   229  	return c.url(rm) + "/" + rm.Name
   230  }
   231  
   232  // WithScope creates a request within the given scope. ex: env/$name/services
   233  func (c *Client) WithScope(scope string) Scoped {
   234  	return &Client{
   235  		ClientBase:    c.ClientBase,
   236  		version:       c.version,
   237  		group:         c.group,
   238  		resource:      c.resource,
   239  		scopeResource: c.scopeResource,
   240  		scope:         scope,
   241  	}
   242  }
   243  
   244  // WithQuery applies a query on the list operation
   245  func WithQuery(n QueryNode) ListOptions {
   246  	return func(lo *listOptions) {
   247  		lo.query = n
   248  	}
   249  }
   250  
   251  // List -
   252  func (c *Client) List(options ...ListOptions) ([]*apiv1.ResourceInstance, error) {
   253  	return c.ListCtx(context.Background(), options...)
   254  }
   255  
   256  // ListCtx returns a list of resources
   257  func (c *Client) ListCtx(ctx context.Context, options ...ListOptions) ([]*apiv1.ResourceInstance, error) {
   258  	req, err := http.NewRequestWithContext(ctx, "GET", c.url(apiv1.ResourceMeta{}), nil)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	err = c.intercept(req)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	opts := listOptions{}
   269  	for _, o := range options {
   270  		o(&opts)
   271  	}
   272  
   273  	if opts.query != nil {
   274  		rv := newRSQLVisitor()
   275  		rv.Visit(opts.query)
   276  		q := req.URL.Query()
   277  		q.Add("query", rv.String())
   278  		req.URL.RawQuery = q.Encode()
   279  	}
   280  	return c.listAll(req)
   281  }
   282  
   283  func (c *Client) doOneRequest(req *http.Request) ([]*apiv1.ResourceInstance, linkheader.Links, error) {
   284  	res, err := c.client.Do(req)
   285  	if err != nil {
   286  		return nil, nil, err
   287  	}
   288  	defer res.Body.Close()
   289  
   290  	if res.StatusCode != 200 {
   291  		return nil, nil, handleError(res)
   292  	}
   293  	dec := json.NewDecoder(res.Body)
   294  	var objs []*apiv1.ResourceInstance
   295  	err = dec.Decode(&objs)
   296  	if err != nil {
   297  		return nil, nil, err
   298  	}
   299  	links := linkheader.Parse(res.Header.Get("Link"))
   300  	return objs, links.FilterByRel("next"), nil
   301  }
   302  
   303  // fetch all items based on the Link headers
   304  func (c *Client) listAll(req *http.Request) ([]*apiv1.ResourceInstance, error) {
   305  	var objs []*apiv1.ResourceInstance
   306  	for {
   307  		res, links, err := c.doOneRequest(req)
   308  		if err != nil {
   309  			return nil, err
   310  		}
   311  		objs = append(objs, res...)
   312  		if links == nil || len(links) == 0 {
   313  			break
   314  		}
   315  		link := links[0]
   316  		parsedLink, err := url.Parse(link.URL)
   317  		if err != nil {
   318  			return nil, err
   319  		}
   320  		req.URL.RawQuery = parsedLink.RawQuery
   321  	}
   322  	return objs, nil
   323  }
   324  
   325  func (c *Client) Get(name string) (*apiv1.ResourceInstance, error) {
   326  	return c.GetCtx(context.Background(), name)
   327  }
   328  
   329  // GetCtx2 returns a single resource.
   330  func (c *Client) GetCtx2(ctx context.Context, toGet *apiv1.ResourceInstance) (*apiv1.ResourceInstance, error) {
   331  	if toGet.Name == "" {
   332  		return nil, fmt.Errorf("empty resource name")
   333  	}
   334  
   335  	req, err := http.NewRequestWithContext(ctx, "GET", c.urlForResource(toGet.ResourceMeta), nil)
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  
   340  	err = c.intercept(req)
   341  	if err != nil {
   342  		return nil, err
   343  	}
   344  
   345  	res, err := c.client.Do(req)
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  	defer res.Body.Close()
   350  
   351  	if res.StatusCode != 200 {
   352  		return nil, handleError(res)
   353  	}
   354  	dec := json.NewDecoder(res.Body)
   355  	obj := &apiv1.ResourceInstance{}
   356  	err = dec.Decode(&obj)
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  
   361  	return obj, nil
   362  }
   363  
   364  // GetCtx returns a single resource. If client is unscoped then name can be "<scopeName>/<name>".
   365  // If client is scoped then name can be "<name>" or "<scopeName>/<name>" but <scopeName> is ignored.
   366  func (c *Client) GetCtx(ctx context.Context, name string) (*apiv1.ResourceInstance, error) {
   367  	split := strings.SplitN(name, `/`, 2)
   368  
   369  	url := ""
   370  
   371  	switch len(split) {
   372  	case 2:
   373  		if split[0] == "" {
   374  			return nil, fmt.Errorf("empty scope name")
   375  		}
   376  
   377  		if split[1] == "" {
   378  			return nil, fmt.Errorf("empty resource name")
   379  		}
   380  
   381  		url = c.urlForResource(apiv1.ResourceMeta{Name: split[1], Metadata: apiv1.Metadata{Scope: apiv1.MetadataScope{Name: split[0]}}})
   382  	case 1:
   383  		if split[0] == "" {
   384  			return nil, fmt.Errorf("empty resource name")
   385  		}
   386  
   387  		url = c.urlForResource(apiv1.ResourceMeta{Name: name})
   388  	default:
   389  		return nil, fmt.Errorf("invalid resource name")
   390  
   391  	}
   392  
   393  	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  
   398  	err = c.intercept(req)
   399  	if err != nil {
   400  		return nil, err
   401  	}
   402  
   403  	res, err := c.client.Do(req)
   404  	if err != nil {
   405  		return nil, err
   406  	}
   407  	defer res.Body.Close()
   408  
   409  	if res.StatusCode != 200 {
   410  		return nil, handleError(res)
   411  	}
   412  	dec := json.NewDecoder(res.Body)
   413  	obj := &apiv1.ResourceInstance{}
   414  	err = dec.Decode(&obj)
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  
   419  	return obj, nil
   420  }
   421  
   422  func (c *Client) Delete(ri *apiv1.ResourceInstance) error {
   423  	return c.DeleteCtx(context.Background(), ri)
   424  }
   425  
   426  // DeleteCtx deletes a single resource
   427  func (c *Client) DeleteCtx(ctx context.Context, ri *apiv1.ResourceInstance) error {
   428  	req, err := http.NewRequestWithContext(ctx, "DELETE", c.urlForResource(ri.ResourceMeta), nil)
   429  	if err != nil {
   430  		return err
   431  	}
   432  
   433  	err = c.intercept(req)
   434  	if err != nil {
   435  		return err
   436  	}
   437  
   438  	res, err := c.client.Do(req)
   439  	if err != nil {
   440  		return err
   441  	}
   442  	defer res.Body.Close()
   443  
   444  	if res.StatusCode != 202 && res.StatusCode != 204 {
   445  		return handleError(res)
   446  	}
   447  	if err != nil {
   448  		return err
   449  	}
   450  
   451  	return nil
   452  }
   453  
   454  func CUserID(userID string) CreateOption {
   455  	return func(co *createOptions) {
   456  		co.impersonateUserID = userID
   457  	}
   458  }
   459  
   460  // Create creates a single resource
   461  func (c *Client) Create(ri *apiv1.ResourceInstance, opts ...CreateOption) (*apiv1.ResourceInstance, error) {
   462  	return c.CreateCtx(context.Background(), ri, opts...)
   463  }
   464  
   465  // CreateCtx creates a single resource
   466  func (c *Client) CreateCtx(ctx context.Context, ri *apiv1.ResourceInstance, opts ...CreateOption) (*apiv1.ResourceInstance, error) {
   467  	buf := &bytes.Buffer{}
   468  	enc := json.NewEncoder(buf)
   469  
   470  	co := createOptions{}
   471  
   472  	for _, opt := range opts {
   473  		opt(&co)
   474  	}
   475  
   476  	err := enc.Encode(ri)
   477  	if err != nil {
   478  		return nil, err
   479  	}
   480  
   481  	req, err := http.NewRequestWithContext(ctx, "POST", c.url(ri.ResourceMeta), buf)
   482  	if err != nil {
   483  		return nil, err
   484  	}
   485  	err = c.intercept(req)
   486  	if err != nil {
   487  		return nil, err
   488  	}
   489  	req.Header.Add("Content-Type", "application/json")
   490  
   491  	if co.impersonateUserID != "" {
   492  		err = c.impersonator.impersonate(req, co.impersonateUserID)
   493  		if err != nil {
   494  			return nil, err
   495  		}
   496  	}
   497  
   498  	res, err := c.client.Do(req)
   499  	if err != nil {
   500  		return nil, err
   501  	}
   502  	defer res.Body.Close()
   503  
   504  	if res.StatusCode != 201 {
   505  		return nil, handleError(res)
   506  	}
   507  
   508  	dec := json.NewDecoder(res.Body)
   509  	obj := &apiv1.ResourceInstance{}
   510  	err = dec.Decode(obj)
   511  	if err != nil {
   512  		return nil, err
   513  	}
   514  
   515  	return obj, err
   516  }
   517  
   518  func UUserID(userID string) UpdateOption {
   519  	return func(co *updateOptions) {
   520  		co.impersonateUserID = userID
   521  	}
   522  }
   523  
   524  type MergeFunc func(fetched apiv1.Interface, new apiv1.Interface) (apiv1.Interface, error)
   525  
   526  // Merge option first fetches the resource and then
   527  // applies the merge function and uses the result for the actual update
   528  // fetched will be the old resource
   529  // new will be the resource passed to the Update call
   530  // If the resource doesn't exist it will fetched will be set to null
   531  // If the merge function returns an error, the update operation will be cancelled
   532  func Merge(merge MergeFunc) UpdateOption {
   533  	return func(co *updateOptions) {
   534  		co.mergeFunc = merge
   535  	}
   536  }
   537  
   538  // Update updates a single resource. If the merge option is passed it will first fetch the resource and then apply the merge function.
   539  func (c *Client) Update(ri *apiv1.ResourceInstance, opts ...UpdateOption) (*apiv1.ResourceInstance, error) {
   540  	return c.UpdateCtx(context.Background(), ri, opts...)
   541  }
   542  
   543  // UpdateCtx updates a single resource
   544  func (c *Client) UpdateCtx(ctx context.Context, ri *apiv1.ResourceInstance, opts ...UpdateOption) (*apiv1.ResourceInstance, error) {
   545  	buf := &bytes.Buffer{}
   546  	enc := json.NewEncoder(buf)
   547  
   548  	uo := updateOptions{}
   549  
   550  	for _, opt := range opts {
   551  		opt(&uo)
   552  	}
   553  
   554  	if uo.mergeFunc != nil {
   555  		old, err := c.GetCtx2(ctx, ri)
   556  		if err != nil {
   557  			switch err.(type) {
   558  			case NotFoundError:
   559  				old = nil
   560  			default:
   561  				return nil, err
   562  			}
   563  		}
   564  
   565  		i, err := uo.mergeFunc(old, ri)
   566  		if err != nil {
   567  			return nil, err
   568  		}
   569  		newRi, err := i.AsInstance()
   570  		if err != nil {
   571  			return nil, err
   572  		}
   573  
   574  		if old == nil {
   575  			return c.CreateCtx(ctx, newRi, CUserID(uo.impersonateUserID))
   576  		}
   577  
   578  		ri = newRi
   579  	}
   580  
   581  	err := enc.Encode(ri)
   582  	if err != nil {
   583  		return nil, err
   584  	}
   585  
   586  	req, err := http.NewRequestWithContext(ctx, "PUT", c.urlForResource(ri.ResourceMeta), buf)
   587  	if err != nil {
   588  		return nil, err
   589  	}
   590  
   591  	err = c.intercept(req)
   592  	if err != nil {
   593  		return nil, err
   594  	}
   595  
   596  	if uo.impersonateUserID != "" {
   597  		err = c.impersonator.impersonate(req, uo.impersonateUserID)
   598  		if err != nil {
   599  			return nil, err
   600  		}
   601  	}
   602  
   603  	req.Header.Add("Content-Type", "application/json")
   604  
   605  	res, err := c.client.Do(req)
   606  	if err != nil {
   607  		return nil, err
   608  	}
   609  	defer res.Body.Close()
   610  	if res.StatusCode != 200 {
   611  		return nil, handleError(res)
   612  	}
   613  
   614  	dec := json.NewDecoder(res.Body)
   615  	obj := &apiv1.ResourceInstance{}
   616  	err = dec.Decode(obj)
   617  	if err != nil {
   618  		return nil, err
   619  	}
   620  
   621  	return obj, err
   622  }