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 }