github.com/splunk/dan1-qbec@v0.7.3/internal/remote/client.go (about) 1 /* 2 Copyright 2019 Splunk Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package remote 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "strings" 23 "time" 24 25 "github.com/ghodss/yaml" 26 "github.com/jonboulle/clockwork" 27 "github.com/pkg/errors" 28 "github.com/splunk/qbec/internal/model" 29 "github.com/splunk/qbec/internal/remote/k8smeta" 30 "github.com/splunk/qbec/internal/sio" 31 "github.com/splunk/qbec/internal/types" 32 apiErrors "k8s.io/apimachinery/pkg/api/errors" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 35 "k8s.io/apimachinery/pkg/runtime/schema" 36 apiTypes "k8s.io/apimachinery/pkg/types" 37 "k8s.io/client-go/discovery" 38 "k8s.io/client-go/dynamic" 39 ) 40 41 const ( 42 identicalObjects = "objects are identical" 43 opUpdate = "update object" 44 opCreate = "create object" 45 ) 46 47 // structured errors 48 var ( 49 ErrForbidden = errors.New("forbidden") // returned due to an authn/ authz error 50 ErrNotFound = errors.New("not found") // returned when a remote object does not exist 51 errMetadataNotFound = errors.New("server type not found") // returned when metadata could not be found for a gvk 52 ) 53 54 // this file contains the client definition and supported CRUD operations. 55 56 // SyncOptions provides the caller with options for the sync operation. 57 type SyncOptions struct { 58 DryRun bool // do not actually create or update objects, return what would happen 59 DisableCreate bool // only update objects if they exist, do not create new ones 60 ShowSecrets bool // show secrets in patches and creations 61 } 62 63 type internalSyncOptions struct { 64 secretDryRun bool // dry-run phase for objects having secrets info 65 pristiner pristineReadWriter // pristine writer 66 pristineAnnotation string // pristine annotation to manipulate for secrets dry-run 67 } 68 69 // Client is a thick remote client that provides high-level operations for commands as opposed to 70 // granular ones. 71 type Client struct { 72 resources *k8smeta.Resources // the server metadata loaded once and never updated 73 schema *k8smeta.ServerSchema // the server schema 74 pool dynamic.ClientPool // the client pool for resource interfaces 75 disco k8smeta.ResourceDiscovery // the discovery interface 76 defaultNs string // the default namespace to set for namespaced objects that do not define one 77 verbosity int // log verbosity 78 dynamicTypes map[schema.GroupVersionKind]bool // crds seen by this client 79 } 80 81 func newClient(pool dynamic.ClientPool, disco discovery.DiscoveryInterface, ns string, verbosity int) (*Client, error) { 82 start := time.Now() 83 resources, err := k8smeta.NewResources(disco, k8smeta.ResourceOpts{WarnFn: sio.Warnln}) 84 if err != nil { 85 return nil, errors.Wrap(err, "get server metadata") 86 } 87 if verbosity > 0 { 88 resources.Dump(sio.Debugln) 89 } 90 duration := time.Since(start).Round(time.Millisecond) 91 sio.Debugln("cluster metadata load took", duration) 92 93 ss := k8smeta.NewServerSchema(disco) 94 c := &Client{ 95 resources: resources, 96 schema: ss, 97 pool: pool, 98 disco: disco, 99 defaultNs: ns, 100 verbosity: verbosity, 101 dynamicTypes: map[schema.GroupVersionKind]bool{}, 102 } 103 return c, nil 104 } 105 106 // ValidatorFor returns a validator for the supplied group version kind. 107 func (c *Client) ValidatorFor(gvk schema.GroupVersionKind) (k8smeta.Validator, error) { 108 return c.schema.ValidatorFor(gvk) 109 } 110 111 // objectNamespace returns the namespace for the specified object. It returns a blank 112 // string when the object is cluster-scoped. For namespace-scoped objects it returns 113 // the default namespace when the object does not have one set. It does not fail if the 114 // object type is not known and just returns whatever is specified for the object. 115 func (c *Client) objectNamespace(o model.K8sMeta) string { 116 info := c.resources.APIResource(o.GroupVersionKind()) 117 ns := o.GetNamespace() 118 if info != nil { 119 if info.Namespaced { 120 if ns == "" { 121 ns = c.defaultNs 122 } 123 } else { 124 ns = "" 125 } 126 } 127 return ns 128 } 129 130 // DisplayName returns the display name of the supplied K8s object. 131 func (c *Client) DisplayName(o model.K8sMeta) string { 132 sm := c.resources 133 gvk := o.GroupVersionKind() 134 info := sm.APIResource(gvk) 135 136 displayType := func() string { 137 if info != nil { 138 return info.Name 139 } 140 return strings.ToLower(gvk.Kind) 141 } 142 143 displayName := func() string { 144 ns := c.objectNamespace(o) 145 name := model.NameForDisplay(o) 146 if ns == "" { 147 return name 148 } 149 return name + " -n " + ns 150 } 151 name := fmt.Sprintf("%s %s", displayType(), displayName()) 152 if l, ok := o.(model.K8sLocalObject); ok { 153 comp := l.Component() 154 if comp != "" { 155 name += fmt.Sprintf(" (source %s)", comp) 156 } 157 } 158 return name 159 } 160 161 func (c *Client) apiResourceFor(gvk schema.GroupVersionKind) (*metav1.APIResource, error) { 162 info := c.resources.APIResource(gvk) 163 if info == nil { 164 return nil, fmt.Errorf("resource not found for %s/%s %s", gvk.Group, gvk.Version, gvk.Kind) 165 } 166 return info, nil 167 } 168 169 // IsNamespaced returns if the supplied group version kind is namespaced. 170 func (c *Client) IsNamespaced(gvk schema.GroupVersionKind) (bool, error) { 171 res, err := c.apiResourceFor(gvk) 172 if err != nil { 173 return false, err 174 } 175 return res.Namespaced, nil 176 } 177 178 func (c *Client) canonicalGroupVersionKind(in schema.GroupVersionKind) (schema.GroupVersionKind, error) { 179 return c.resources.CanonicalGroupVersionKind(in) 180 } 181 182 // Get returns the remote object matching the supplied metadata as an unstructured bag of attributes. 183 func (c *Client) Get(obj model.K8sMeta) (*unstructured.Unstructured, error) { 184 rc, err := c.resourceInterfaceWithDefaultNs(obj.GroupVersionKind(), obj.GetNamespace()) 185 if err != nil { 186 return nil, err 187 } 188 u, err := rc.Get(obj.GetName(), metav1.GetOptions{}) 189 if err != nil { 190 if apiErrors.IsNotFound(err) { 191 return nil, ErrNotFound 192 } 193 if apiErrors.IsForbidden(err) { 194 return nil, ErrForbidden 195 } 196 return nil, err 197 } 198 return u, nil 199 } 200 201 // ObjectKey returns a string key for the supplied object that includes its group-kind, 202 // namespace and name. Input values are used in case canonical values cannot be derived 203 // (e.g. for custom resources whose CRDs haven't yet been created). 204 func (c *Client) ObjectKey(obj model.K8sMeta) string { 205 gvk := obj.GroupVersionKind() 206 if canon, err := c.resources.CanonicalGroupVersionKind(gvk); err == nil { 207 gvk = canon 208 } 209 ns := c.objectNamespace(obj) 210 return fmt.Sprintf("%s:%s:%s:%s", gvk.Group, gvk.Kind, ns, obj.GetName()) 211 } 212 213 // ListQueryScope defines the scope at which list queries need to be executed. 214 type ListQueryScope struct { 215 Namespaces []string // namespaces of interest 216 ClusterObjects bool // whether to query for cluster objects 217 } 218 219 // ListQueryConfig is the config with which to execute list queries. 220 type ListQueryConfig struct { 221 Application string // must be non-blank 222 Tag string // may be blank 223 Environment string // must be non-blank 224 ListQueryScope // the query scope for namespaces and non-namespaced resources 225 KindFilter model.Filter // filters for object kind 226 Concurrency int // concurrent queries to execute 227 DisableAllNsQueries bool // do not perform list queries across namespaces when multiple namespaces in picture 228 } 229 230 // Collection represents a set of k8s objects with the ability to remove a subset of objects from it. 231 type Collection interface { 232 Remove(obj []model.K8sQbecMeta) error // remove all objects represented by the input list 233 ToList() []model.K8sQbecMeta // return a list of remaining objects 234 } 235 236 // ListObjects returns all objects for the application and environment for the namespace /cluster scopes 237 // and kind filtering indicated by the query configuration. 238 func (c *Client) ListObjects(scope ListQueryConfig) (Collection, error) { 239 if scope.KindFilter == nil { 240 kf, _ := model.NewKindFilter(nil, nil) 241 scope.KindFilter = kf 242 } 243 244 // handle special cases 245 filterEligibleTypes := func(types []schema.GroupVersionKind) []schema.GroupVersionKind { 246 var ret []schema.GroupVersionKind 247 for _, t := range types { 248 switch { 249 // the issue with endpoints is that every service creates endpoints objects and 250 // propagates its own labels to it. These have not been created by qbec. 251 case t.Group == "" && t.Kind == "Endpoints": 252 if c.verbosity > 0 { 253 sio.Debugf("not listing objects of type %v\n", t) 254 } 255 default: 256 ret = append(ret, t) 257 } 258 } 259 return ret 260 } 261 262 var namespacedTypes, clusterTypes []schema.GroupVersionKind 263 for _, v := range c.resources.CanonicalResources() { 264 gvk := schema.GroupVersionKind{Group: v.Group, Version: v.Version, Kind: v.Kind} 265 if v.Namespaced { 266 namespacedTypes = append(namespacedTypes, gvk) 267 } else { 268 clusterTypes = append(clusterTypes, gvk) 269 } 270 } 271 272 qc := queryConfig{ 273 scope: scope, 274 resourceProvider: c.ResourceInterface, 275 namespacedTypes: filterEligibleTypes(namespacedTypes), 276 clusterTypes: filterEligibleTypes(clusterTypes), 277 verbosity: c.verbosity, 278 } 279 ol := objectLister{qc} 280 coll := newCollection(c.defaultNs, c) 281 if err := ol.serverObjects(coll); err != nil { 282 return nil, err 283 } 284 return coll, nil 285 } 286 287 type updateResult struct { 288 SkipReason string `json:"skip,omitempty"` 289 Operation string `json:"operation,omitempty"` 290 Source string `json:"source,omitempty"` 291 Kind apiTypes.PatchType `json:"kind,omitempty"` 292 DisplayPatch string `json:"patch,omitempty"` 293 GeneratedName string `json:"generatedName,omitempty"` 294 patch []byte 295 } 296 297 func (u *updateResult) String() string { 298 b, err := yaml.Marshal(u) 299 if err != nil { 300 sio.Warnln("unable to marshal result to YAML") 301 } 302 return string(b) 303 } 304 305 func (u *updateResult) toSyncResult() *SyncResult { 306 switch { 307 case u.SkipReason == identicalObjects: 308 return &SyncResult{ 309 Type: SyncObjectsIdentical, 310 Details: u.SkipReason, 311 } 312 case u.SkipReason != "": 313 return &SyncResult{ 314 Type: SyncSkip, 315 Details: u.SkipReason, 316 } 317 case u.Operation == opCreate: 318 return &SyncResult{ 319 Type: SyncCreated, 320 GeneratedName: u.GeneratedName, // only set when name actually generated 321 Details: u.String(), 322 } 323 case u.Operation == opUpdate: 324 return &SyncResult{ 325 Type: SyncUpdated, 326 Details: u.String(), 327 } 328 default: 329 panic(fmt.Errorf("invalid operation:%s, %v", u.Operation, u)) 330 } 331 } 332 333 // SyncResultType indicates what notionally happened in a sync operation. 334 type SyncResultType int 335 336 // Sync result types 337 const ( 338 _ SyncResultType = iota 339 SyncObjectsIdentical // sync was a noop due to local and remote being identical 340 SyncSkip // object was skipped for sync (e.g. creation needed but disabled) 341 SyncCreated // object was created 342 SyncUpdated // object was updated 343 SyncDeleted // object was deleted 344 ) 345 346 // SyncResult is the result of a sync operation. There is no difference in the output for a real versus 347 // a dry-run. 348 type SyncResult struct { 349 Type SyncResultType // the result type 350 GeneratedName string // the actual name of an object that has generateName set 351 Details string // additional details that are safe to print to console (e.g. no secrets) 352 } 353 354 func extractCustomTypes(obj model.K8sObject) (schema.GroupVersionKind, error) { 355 var ret schema.GroupVersionKind 356 var crd struct { 357 Spec struct { 358 Group string `json:"group"` 359 Version string `json:"version"` 360 Names struct { 361 Kind string `json:"kind"` 362 } `json:"names"` 363 } `json:"spec"` 364 } 365 b, err := obj.ToUnstructured().MarshalJSON() 366 if err != nil { 367 return ret, err 368 } 369 if err := json.Unmarshal(b, &crd); err != nil { 370 return ret, err 371 } 372 return schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Spec.Names.Kind}, nil 373 } 374 375 // Sync syncs the local object by either creating a new one or patching an existing one. 376 // It does not do anything in dry-run mode. It also does not create new objects if the caller has disabled the feature. 377 func (c *Client) Sync(original model.K8sLocalObject, opts SyncOptions) (_ *SyncResult, finalError error) { 378 // set up the pristine strategy. 379 var prw pristineReadWriter = qbecPristine{} 380 sensitive := types.HasSensitiveInfo(original.ToUnstructured()) 381 382 internal := internalSyncOptions{ 383 secretDryRun: false, 384 pristiner: prw, 385 pristineAnnotation: model.QbecNames.PristineAnnotation, 386 } 387 388 if sensitive && !opts.ShowSecrets { 389 internal.secretDryRun = true 390 } 391 392 defer func() { 393 if finalError != nil { 394 finalError = errors.Wrap(finalError, "sync "+c.DisplayName(original)) 395 } 396 }() 397 398 gvk := original.GroupVersionKind() 399 if gvk.Kind == "CustomResourceDefinition" && gvk.Group == "apiextensions.k8s.io" { 400 t, err := extractCustomTypes(original) 401 if err != nil { 402 sio.Warnf("error extracting types for custom resource %s, %v\n", original.GetName(), err) 403 } else { 404 c.dynamicTypes[t] = true 405 } 406 } 407 408 result, err := c.doSync(original, opts, internal) 409 if err != nil { 410 return nil, err 411 } 412 // exit if we are done 413 if !internal.secretDryRun || opts.DryRun { 414 return result.toSyncResult(), nil 415 } 416 internal.secretDryRun = false 417 _, err = c.doSync(original, opts, internal) // do the real sync 418 if err != nil { 419 return nil, err 420 } 421 return result.toSyncResult(), err 422 } 423 424 func (c *Client) doSync(original model.K8sLocalObject, opts SyncOptions, internal internalSyncOptions) (*updateResult, error) { 425 gvk := original.GroupVersionKind() 426 var remObj *unstructured.Unstructured 427 var objErr error 428 if original.GetName() != "" { 429 remObj, objErr = c.Get(original) 430 } 431 switch { 432 // empty name, always create 433 case original.GetName() == "": 434 break 435 // ignore object not found errors 436 case objErr == ErrNotFound: 437 break 438 // treat metadata errors (server type not found) as a "not found" error under the following conditions: 439 // - dry-run mode is active 440 // - a prior custom resource with that GVK has been applied 441 case objErr == errMetadataNotFound && opts.DryRun && c.dynamicTypes[gvk]: 442 break 443 // error but with better message 444 case objErr == errMetadataNotFound && opts.DryRun: 445 return nil, fmt.Errorf("server type %v not found and no prior CRD installs it", gvk) 446 // report all other errors 447 case objErr != nil: 448 return nil, objErr 449 } 450 451 var obj model.K8sLocalObject 452 if internal.secretDryRun { 453 opts.DryRun = true // won't affect caller since passed by value 454 obj, _ = types.HideSensitiveLocalInfo(original) 455 } else { 456 o, err := internal.pristiner.createFromPristine(original) 457 if err != nil { 458 return nil, errors.Wrap(err, "create from pristine") 459 } 460 obj = o 461 } 462 463 // create or update as needed, each of these routines is responsible for correct dry-run handling. 464 var result *updateResult 465 var err error 466 if remObj == nil { 467 result, err = c.maybeCreate(obj, opts) 468 } else { 469 if internal.secretDryRun { 470 ann := remObj.GetAnnotations() 471 if ann == nil { 472 ann = map[string]string{} 473 } 474 delete(ann, internal.pristineAnnotation) 475 remObj.SetAnnotations(ann) 476 c, _ := types.HideSensitiveInfo(remObj) 477 remObj = c 478 } 479 result, err = c.maybeUpdate(obj, remObj, opts) 480 } 481 if err != nil { 482 return nil, err 483 } 484 485 // create a prettier patch for display, if needed 486 result.DisplayPatch = string(result.patch) 487 if result.patch != nil { 488 var data interface{} 489 if err := json.Unmarshal(result.patch, &data); err == nil { 490 b, err := json.MarshalIndent(data, "", " ") 491 if err == nil { 492 result.DisplayPatch = string(b) 493 } 494 } 495 } 496 return result, nil 497 } 498 499 // Delete delete the supplied object if it exists. It does not do anything in dry-run mode. 500 func (c *Client) Delete(obj model.K8sMeta, dryRun bool) (_ *SyncResult, finalError error) { 501 ret := &SyncResult{ 502 Type: SyncDeleted, 503 } 504 if dryRun { 505 return ret, nil 506 } 507 defer func() { 508 if finalError != nil { 509 finalError = errors.Wrap(finalError, "delete "+c.DisplayName(obj)) 510 } 511 }() 512 513 ri, err := c.resourceInterfaceWithDefaultNs(obj.GroupVersionKind(), obj.GetNamespace()) 514 if err != nil { 515 return nil, errors.Wrap(err, "get resource interface") 516 } 517 518 pp := metav1.DeletePropagationForeground 519 err = ri.Delete(obj.GetName(), &metav1.DeleteOptions{PropagationPolicy: &pp}) 520 if err != nil { 521 if apiErrors.IsNotFound(err) { 522 ret.Type = SyncSkip 523 ret.Details = "object not found on the server" 524 return ret, nil 525 } 526 if apiErrors.IsConflict(err) && obj.GetKind() == "Namespace" { 527 ret.Type = SyncSkip 528 ret.Details = "namespace delete had conflict, ignore" 529 return ret, nil 530 } 531 return nil, err 532 } 533 return ret, nil 534 } 535 536 func (c *Client) jitResource(gvk schema.GroupVersionKind) (*metav1.APIResource, error) { 537 rl, err := c.disco.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) 538 if err != nil { 539 return nil, err 540 } 541 for _, r := range rl.APIResources { 542 if strings.Contains(r.Name, "/") { // ignore sub-resources 543 continue 544 } 545 if r.Kind == gvk.Kind { 546 return &r, nil 547 } 548 } 549 return nil, fmt.Errorf("server does not recognize gvk %s", gvk) 550 } 551 552 // ResourceInterface returns a dynamic resource interface for the supplied group version kind and namespace. 553 func (c *Client) ResourceInterface(gvk schema.GroupVersionKind, namespace string) (dynamic.ResourceInterface, error) { 554 client, err := c.pool.ClientForGroupVersionKind(gvk) 555 if err != nil { 556 return nil, err 557 } 558 res, err := c.apiResourceFor(gvk) 559 if err != nil { // could be a resource for a CRD that was just created, re-query discovery 560 res, err = c.jitResource(gvk) 561 if err != nil { 562 return nil, errMetadataNotFound 563 } 564 } 565 return client.Resource(res, namespace), nil 566 } 567 568 func (c *Client) resourceInterfaceWithDefaultNs(gvk schema.GroupVersionKind, namespace string) (dynamic.ResourceInterface, error) { 569 if namespace == "" { 570 namespace = c.defaultNs 571 } 572 return c.ResourceInterface(gvk, namespace) 573 } 574 575 func (c *Client) maybeCreate(obj model.K8sLocalObject, opts SyncOptions) (*updateResult, error) { 576 if opts.DisableCreate { 577 return &updateResult{ 578 SkipReason: "creation disabled due to user request", 579 }, nil 580 } 581 b, err := json.Marshal(obj) 582 if err != nil { 583 return nil, errors.Wrap(err, "json marshal") 584 } 585 result := &updateResult{ 586 Operation: opCreate, 587 Source: "local", 588 patch: b, 589 } 590 if opts.DryRun { 591 return result, nil 592 } 593 ri, err := c.resourceInterfaceWithDefaultNs(obj.GroupVersionKind(), obj.GetNamespace()) 594 if err != nil { 595 return nil, errors.Wrap(err, "get resource interface") 596 } 597 out, err := ri.Create(obj.ToUnstructured()) 598 if err != nil { 599 return nil, err 600 } 601 if obj.GetName() == "" { 602 result.GeneratedName = out.GetName() 603 } 604 return result, nil 605 } 606 607 func (c *Client) maybeUpdate(obj model.K8sLocalObject, remObj *unstructured.Unstructured, opts SyncOptions) (*updateResult, error) { 608 res, err := c.schema.OpenAPIResources() 609 if err != nil { 610 sio.Warnln("get open API resources", err) 611 } 612 var lookup openAPILookup 613 if res != nil { 614 lookup = res.LookupResource 615 } 616 617 p := patcher{ 618 provider: c.resourceInterfaceWithDefaultNs, 619 cfgProvider: func(obj *unstructured.Unstructured) ([]byte, error) { 620 pristine, _ := getPristineVersion(obj, false) 621 if pristine == nil { 622 p := map[string]interface{}{ 623 "kind": obj.GetKind(), 624 "apiVersion": obj.GetAPIVersion(), 625 "metadata": map[string]interface{}{ 626 "name": obj.GetName(), 627 }, 628 } 629 pb, _ := json.Marshal(p) 630 return pb, nil 631 } 632 b, _ := json.Marshal(pristine) 633 return b, nil 634 }, 635 overwrite: true, 636 backOff: clockwork.NewRealClock(), 637 openAPILookup: lookup, 638 } 639 640 var result *updateResult 641 if opts.DryRun { 642 result, err = p.getPatchContents(remObj, obj) 643 } else { 644 result, err = p.patch(remObj, obj) 645 } 646 return result, err 647 }