kubeclient 4.3.0 → 4.8.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of kubeclient might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f53d064748148a041be5853562e7789f8cc792735e4916c1859c63bf93f1f8bc
4
- data.tar.gz: 15a4ba1b298c4bfb3469765a92ad58e8dd806f470870bbadaaf81a0c7ac3dbf4
3
+ metadata.gz: 48d448677cbf44234e974ccb8abd00002c729b059f8857120f11d7a017c63f96
4
+ data.tar.gz: 1db9a01ab5819636816562bd60b563db6e1aa20aa45f8e8b1283da35e1c77a37
5
5
  SHA512:
6
- metadata.gz: 39b85eead8370920f1dbf6f30720506de756894ae4d34b55a5e589b9ff1c8bce0a6e93c5517d4afbc7bb039b729e760250c7b4ead17b4fced76f52941b6bcf44
7
- data.tar.gz: 6c95743f7680fac757b90804050b81488d084fe083749890f069ca8ed1a97158c789d3a912fcccc72f64d089695a9fe0c2085b45dffa492773c82d2ae5b3626d
6
+ metadata.gz: f3ba57f5db5c09035f1ae3265203f5eb64858b7383c9cb73a9e2186befc4b7c120db3984634626895143c5409c9b943103d93a106274b4d61f57579d2e22ba93
7
+ data.tar.gz: 2154232e7359a4e3eae7191093338cce2f6daa44abf30f663bbe59631435927519f7e342dd103a485726375bdfde5d556c3a825b3dbd063d23f055e5eb16ad1e
@@ -1,13 +1,29 @@
1
1
  language: ruby
2
+ sudo: false
3
+ cache: bundler
4
+ script: bundle exec rake $TASK
5
+
6
+ os:
7
+ - linux
8
+ - osx
2
9
  rvm:
3
- - "2.2"
10
+ - "2.2.0"
4
11
  - "2.3.0"
5
12
  - "2.4.0"
6
13
  - "2.5.0"
7
- - "2.6.0-preview1"
8
- sudo: false
9
- cache: bundler
10
- script: bundle exec rake $TASK
14
+ - "2.6.0"
15
+ - "2.7.0"
11
16
  env:
12
17
  - TASK=test
13
- - TASK=rubocop
18
+ matrix:
19
+ exclude:
20
+ - os: osx
21
+ rvm: "2.2.0"
22
+ - os: osx
23
+ rvm: "2.3.0"
24
+ # No point running Rubocop on different rubies, results will be same.
25
+ # The rubies it expects the code to target are controlled in .rubocop.yml.
26
+ include:
27
+ - os: linux
28
+ rvm: "2.5.0"
29
+ env: TASK=rubocop
@@ -4,13 +4,57 @@ Notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
5
5
  Kubeclient release versioning follows [SemVer](https://semver.org/).
6
6
 
7
+ ## 4.8.0 — 2020-07-03
8
+
9
+ ### Added
10
+ - Support for server-side apply (#448).
11
+
12
+ ### Fixed
13
+ - Declared forgotten dependency on jsonpath, needed for `gcp` provider with `cmd-path` (#450).
14
+
15
+ ## 4.7.0 — 2020-06-14
16
+
17
+ ### Fixed
18
+ - Ruby 2.7 compatibility: bumped minimum recursive-open-struct to one that works on 2.7 (#439).
19
+ - Ruby 2.7 warnings (#433, #438).
20
+ - Improved watch documentation, including behavior planned to change in 5.0.0 (#436).
21
+
22
+ ### Added
23
+ - Google Application Default Credentials: Added `userinfo.email` to requested scopes, which is necessary for RBAC policies (#441).
24
+
25
+ ## 4.6.0 — 2019-12-30
26
+
27
+ ### Fixed
28
+ - AmazonEksCredentials was sometimes leaving base64 padding that IAM auth of the EKS cluster rejects. Now padding is always stripped. (#424, #423)
29
+
30
+ ### Added
31
+ - Allow calling `watch_foos` methods with a block, simpler to use and guarantees closing the connection. (#425)
32
+
33
+ - Support `limitBytes` query parameter for `get_pod_log`. (#426)
34
+
35
+ ## 4.5.0 — 2019-09-27
36
+
37
+ ### Added
38
+ - Support `:resourceVersion` parameter in `get_foos` methods (similar to existing support in `watch_foos` methods). (#420)
39
+
40
+ - Relax dependency on `http` gem to allow both 3.x and 4.x. (#413)
41
+
42
+ ## 4.4.0 — 2019-05-03
43
+
44
+ ### Added
45
+ - GCP configs with `user[auth-provider][name] == 'gcp'` will execute credential plugin (normally the `gcloud config config-helper` subcommand) when the config specifies it in `cmd-path`, `cmd-args` fields (similar to `exec` support). This code path works without `googleauth` gem. Otherwise, `GoogleApplicationDefaultCredentials` path will be tried as before. (#410)
46
+ - `AmazonEksCredentials` helper for obtaining a token to authenticate against Amazon EKS. This is not currently integrated in `Config`, you will need to invoke it yourself. You'll need some aws gems that Kubeclient _does not_ include. (#404, #406)
47
+
48
+ ### Changed
49
+ - OpenID Connect tokens which cannot be validaded because we cannot identify the key they were signed with will be considered expired and refreshed as usual. (#407)
50
+
7
51
  ## 4.3.0 — 2019-03-03
8
52
 
9
53
  ### Changed
10
- - `GoogleApplicationDefaultCredentials` will automatically be used in `fetch_user_auth_options` if the `user[auth-provider][name] == 'gcp'` in the provided context. Note that `user[exec]` is checked first in anticipation of this functionality being added to GCP sometime in the future. Kubeclient _does not_ import the required `googleauth` gem, so you will need to import it in your calling application. (#394)
54
+ - `GoogleApplicationDefaultCredentials` will now automatically be used by `Config` if the `user[auth-provider][name] == 'gcp'` in the provided context. Note that `user[exec]` is checked first in anticipation of this functionality being added to GCP sometime in the future. Kubeclient _does not_ include the required `googleauth` gem, so you will need to include it in your calling application. (#394)
11
55
 
12
56
  ### Added
13
- - OpenID Connect credentials will automatically be used if the `user[auth-provider][name] == 'oidc'` in the provided context. Note that `user[exec]` is checked first. Kubeclient _does not_ import the required `openid_connect` gem, so you will need to import it in your calling application. (#396)
57
+ - OpenID Connect credentials will automatically be used if the `user[auth-provider][name] == 'oidc'` in the provided context. Note that `user[exec]` is checked first. Kubeclient _does not_ include the required `openid_connect` gem, so you will need to include it in your calling application. (#396)
14
58
 
15
59
  - Support for `json_patch_#{entity}` and `merge_patch_#{entity}`. `patch_#{entity}` will continue to use strategic merge patch. (#390)
16
60
 
data/README.md CHANGED
@@ -207,7 +207,7 @@ client = Kubeclient::Client.new(
207
207
 
208
208
  ### Timeouts
209
209
 
210
- Watching never times out.
210
+ Watching configures the socket to never time out (however, sooner or later all watches terminate).
211
211
 
212
212
  One-off actions like `.get_*`, `.delete_*` have a configurable timeout:
213
213
  ```ruby
@@ -287,6 +287,45 @@ Kubeclient::Client.new(
287
287
  ```
288
288
 
289
289
 
290
+ #### Amazon EKS Credentials
291
+
292
+ On Amazon EKS by default the authentication method is IAM. When running kubectl a temporary token is generated by shelling out to
293
+ the aws-iam-authenticator binary which is sent to authenticate the user.
294
+ See [aws-iam-authenticator](https://github.com/kubernetes-sigs/aws-iam-authenticator).
295
+ To replicate that functionality, the `Kubeclient::AmazonEksCredentials` class can accept a set of IAM credentials and
296
+ contains a helper method to generate the authentication token for you.
297
+
298
+ This requires a set of gems which are _not_ included in
299
+ `kubeclient` dependencies (`aws-sigv4`) so you should add them to your bundle.
300
+ You will also require either the `aws-sdk` v2 or `aws-sdk-core` v3 gems to generate the required `Aws:Credentials` object to pass to this method.
301
+
302
+ To obtain a token:
303
+
304
+ ```ruby
305
+ require 'aws-sdk-core'
306
+ # Use keys
307
+ credentials = Aws::Credentials.new(access_key, secret_key)
308
+ # Or a profile
309
+ credentials = Aws::SharedCredentials.new(profile_name: 'default').credentials
310
+
311
+ auth_options = {
312
+ bearer_token: Kubeclient::AmazonEksCredentials.token(credentials, eks_cluster_name)
313
+ }
314
+ client = Kubeclient::Client.new(
315
+ eks_cluster_https_endpoint, 'v1', auth_options: auth_options
316
+ )
317
+ ```
318
+
319
+ Note that this returns a token good for one minute. If your code requires authorization for longer than that, you should plan to
320
+ acquire a new one, see [How to manually renew](#how-to-manually-renew-expired-credentials) section.
321
+
322
+ #### Google GCP credential plugin
323
+
324
+ If kubeconfig file has `user: {auth-provider: {name: gcp, cmd-path: ..., cmd-args: ..., token-key: ...}}`, the command will be executed to obtain a token.
325
+ (Normally this would be a `gcloud config config-helper` command.)
326
+
327
+ Note that this returns an expiring token. If your code requires authorization for a long time, you should plan to acquire a new one, see [How to manually renew](#how-to-manually-renew-expired-credentials) section.
328
+
290
329
  #### Google's Application Default Credentials
291
330
 
292
331
  On Google Compute Engine, Google App Engine, or Google Cloud Functions, as well as `gcloud`-configured systems
@@ -296,7 +335,7 @@ kubeclient can use `googleauth` gem to authorize.
296
335
  This requires the [`googleauth` gem](https://github.com/google/google-auth-library-ruby) that is _not_ included in
297
336
  `kubeclient` dependencies so you should add it to your bundle.
298
337
 
299
- If you use `Config.context(...).auth_options` and the kubeconfig file has `user: {auth-provider: {name: gcp}}`, kubeclient will automatically try this (raising LoadError if you don't have `googleauth` in your bundle).
338
+ If you use `Config.context(...).auth_options` and the kubeconfig file has `user: {auth-provider: {name: gcp}}`, but does not contain `cmd-path` key, kubeclient will automatically try this (raising LoadError if you don't have `googleauth` in your bundle).
300
339
 
301
340
  Or you can obtain a token manually:
302
341
 
@@ -381,16 +420,48 @@ We try to support the last 3 minor versions, matching the [official support poli
381
420
  Kubernetes 1.2 and below have known issues and are unsupported.
382
421
  Kubernetes 1.3 presumed to still work although nobody is really testing on such old versions...
383
422
 
384
- ## Examples:
423
+ ## Supported actions & examples:
424
+
425
+ Summary of main CRUD actions:
426
+
427
+ ```
428
+ get_foos(namespace: 'namespace', **opts) # namespaced collection
429
+ get_foos(**opts) # all namespaces or global collection
430
+
431
+ get_foo('name', 'namespace', opts) # namespaced
432
+ get_foo('name', nil, opts) # global
433
+
434
+ watch_foos(namespace: ns, **opts) # namespaced collection
435
+ watch_foos(**opts) # all namespaces or global collection
436
+ watch_foos(namespace: ns, name: 'name', **opts) # namespaced single object
437
+ watch_foos(name: 'name', **opts) # global single object
438
+
439
+ delete_foo('name', 'namespace', opts) # namespaced
440
+ delete_foo('name', nil, opts) # global
441
+
442
+ create_foo(Kubeclient::Resource.new({metadata: {name: 'name', namespace: 'namespace', ...}, ...}))
443
+ create_foo(Kubeclient::Resource.new({metadata: {name: 'name', ...}, ...})) # global
444
+
445
+ update_foo(Kubeclient::Resource.new({metadata: {name: 'name', namespace: 'namespace', ...}, ...}))
446
+ update_foo(Kubeclient::Resource.new({metadata: {name: 'name', ...}, ...})) # global
447
+
448
+ patch_foo('name', patch, 'namespace') # namespaced
449
+ patch_foo('name', patch) # global
450
+
451
+ apply_foo(Kubeclient::Resource.new({metadata: {name: 'name', namespace: 'namespace', ...}, ...}), field_manager: 'myapp', **opts)
452
+ apply_foo(Kubeclient::Resource.new({metadata: {name: 'name', ...}, ...}), field_manager: 'myapp', **opts) # global
453
+ ```
385
454
 
386
- #### Get all instances of a specific entity type
455
+ These grew to be quite inconsistent :confounded:, see https://github.com/abonas/kubeclient/issues/312 and https://github.com/abonas/kubeclient/issues/332 for improvement plans.
456
+
457
+ ### Get all instances of a specific entity type
387
458
  Such as: `get_pods`, `get_secrets`, `get_services`, `get_nodes`, `get_replication_controllers`, `get_resource_quotas`, `get_limit_ranges`, `get_persistent_volumes`, `get_persistent_volume_claims`, `get_component_statuses`, `get_service_accounts`
388
459
 
389
460
  ```ruby
390
461
  pods = client.get_pods
391
462
  ```
392
463
 
393
- Get all entities of a specific type in a namespace:<br>
464
+ Get all entities of a specific type in a namespace:
394
465
 
395
466
  ```ruby
396
467
  services = client.get_services(namespace: 'development')
@@ -408,7 +479,22 @@ You can specify multiple labels (that option will return entities which have bot
408
479
  pods = client.get_pods(label_selector: 'name=redis-master,app=redis')
409
480
  ```
410
481
 
411
- Get all entities of a specific type in chunks:
482
+ There is also [a limited ability to filter by *some* fields](https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/). Which fields are supported is not documented, you can try and see if you get an error...
483
+ ```ruby
484
+ client.get_pods(field_selector: 'spec.nodeName=master-0')
485
+ ```
486
+
487
+ You can ask for entities at a specific version by specifying a parameter named `resource_version`:
488
+ ```ruby
489
+ pods = client.get_pods(resource_version: '0')
490
+ ```
491
+ but it's not guaranteed you'll get it. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions to understand the semantics.
492
+
493
+ With default (`as: :ros`) return format, the returned object acts like an array of the individual pods, but also supports a `.resourceVersion` method.
494
+
495
+ With `:parsed` and `:parsed_symbolized` formats, the returned data structure matches kubernetes list structure: it's a hash containing `metadata` and `items` keys, the latter containing the individual pods.
496
+
497
+ #### Get all entities of a specific type in chunks
412
498
 
413
499
  ```ruby
414
500
  continue = nil
@@ -463,7 +549,87 @@ Other formats are:
463
549
  - `:parsed` for `JSON.parse`
464
550
  - `:parsed_symbolized` for `JSON.parse(..., symbolize_names: true)`
465
551
 
466
- #### Delete an entity (by name)
552
+ ### Watch Receive entities updates
553
+
554
+ See https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes for an overview.
555
+
556
+ It is possible to receive live update notices watching the relevant entities:
557
+
558
+ ```ruby
559
+ client.watch_pods do |notice|
560
+ # process notice data
561
+ end
562
+ ```
563
+
564
+ The notices have `.type` field which may be `'ADDED'`, `'MODIFIED'`, `'DELETED'`, or currently `'ERROR'`, and an `.object` field containing the object. **UPCOMING CHANGE**: In next major version, we plan to raise exceptions instead of passing on ERROR into the block.
565
+
566
+ For namespaced entities, the default watches across all namespaces, and you can specify `client.watch_secrets(namespace: 'foo')` to only watch in a single namespace.
567
+
568
+ You can narrow down using `label_selector:` and `field_selector:` params, like with `get_pods` methods.
569
+
570
+ You can also watch a single object by specifying `name:` e.g. `client.watch_nodes(name: 'gandalf')` (not namespaced so a name is enough) or `client.watch_pods(namespace: 'foo', name: 'bar')` (namespaced, need both params).
571
+ Note the method name is still plural! There is no `watch_pod`, only `watch_pods`. The yielded "type" remains the same — watch notices, it's just they'll always refer to the same object.
572
+
573
+ You can use `as:` param to control the format of the yielded notices.
574
+
575
+ #### All watches come to an end!
576
+
577
+ While nominally the watch block *looks* like an infinite loop, that's unrealistic. Network connections eventually get severed, and kubernetes apiserver is known to terminate watches.
578
+
579
+ Unfortunately, this sometimes raises an exception and sometimes the loop just exits. **UPCOMING CHANGE**: In next major version, non-deliberate termination will always raise an exception; the block will only exit silenty if stopped deliberately.
580
+
581
+ #### Deliberately stopping a watch
582
+
583
+ You can use `break` or `return` inside the watch block.
584
+
585
+ It is possible to interrupt the watcher from another thread with:
586
+
587
+ ```ruby
588
+ watcher = client.watch_pods
589
+
590
+ watcher.each do |notice|
591
+ # process notice data
592
+ end
593
+ # <- control will pass here after .finish is called
594
+
595
+ ### In another thread ###
596
+ watcher.finish
597
+ ```
598
+
599
+ #### Starting watch version
600
+
601
+ You can specify version to start from, commonly used in "List+Watch" pattern:
602
+ ```
603
+ list = client.get_pods
604
+ collection_version = list.resourceVersion
605
+ # or with other return formats:
606
+ list = client.get_pods(as: :parsed)
607
+ collection_version = list['metadata']['resourceVersion']
608
+
609
+ # note spelling resource_version vs resourceVersion.
610
+ client.watch_pods(resource_version: collection_version) do |notice|
611
+ # process notice data
612
+ end
613
+ ```
614
+ It's important to understand [the effects of unset/0/specific resource_version](https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions) as it modifies the behavior of the watch — in some modes you'll first see a burst of synthetic 'ADDED' notices for all existing objects.
615
+
616
+ If you re-try a terminated watch again without specific resourceVersion, you might see previously seen notices again, and might miss some events.
617
+
618
+ To attempt resuming a watch from same point, you can try using last resourceVersion observed during the watch. Or do list+watch again.
619
+
620
+ Whenever you ask for a specific version, you must be prepared for an 410 "Gone" error if the server no longer recognizes it.
621
+
622
+ #### Watch events about a particular object
623
+ Events are [entities in their own right](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#event-v1-core).
624
+ You can use the `field_selector` option as part of the watch methods.
625
+
626
+ ```ruby
627
+ client.watch_events(namespace: 'development', field_selector: 'involvedObject.name=redis-master') do |notice|
628
+ # process notice date
629
+ end
630
+ ```
631
+
632
+ ### Delete an entity (by name)
467
633
 
468
634
  For example: `delete_pod "pod name"` , `delete_replication_controller "rc name"`, `delete_node "node name"`, `delete_secret "secret name"`
469
635
 
@@ -487,7 +653,7 @@ delete_options = Kubeclient::Resource.new(
487
653
  client.delete_deployment(deployment_name, namespace, delete_options: delete_options)
488
654
  ```
489
655
 
490
- #### Create an entity
656
+ ### Create an entity
491
657
  For example: `create_pod pod_object`, `create_replication_controller rc_obj`, `create_secret secret_object`, `create_resource_quota resource_quota_object`, `create_limit_range limit_range_object`, `create_persistent_volume persistent_volume_object`, `create_persistent_volume_claim persistent_volume_claim_object`, `create_service_account service_account_object`
492
658
 
493
659
  Input parameter - object of type `Service`, `Pod`, `ReplicationController`.
@@ -513,7 +679,7 @@ service.metadata.labels.role = 'slave'
513
679
  client.create_service(service)
514
680
  ```
515
681
 
516
- #### Update an entity
682
+ ### Update an entity
517
683
  For example: `update_pod`, `update_service`, `update_replication_controller`, `update_secret`, `update_resource_quota`, `update_limit_range`, `update_persistent_volume`, `update_persistent_volume_claim`, `update_service_account`
518
684
 
519
685
  Input parameter - object of type `Pod`, `Service`, `ReplicationController` etc.
@@ -524,7 +690,7 @@ The below example is for v1
524
690
  updated = client.update_service(service1)
525
691
  ```
526
692
 
527
- #### Patch an entity (by name)
693
+ ### Patch an entity (by name)
528
694
  For example: `patch_pod`, `patch_service`, `patch_secret`, `patch_resource_quota`, `patch_persistent_volume`
529
695
 
530
696
  Input parameters - name (string) specifying the entity name, patch (hash) to be applied to the resource, optional: namespace name (string)
@@ -539,41 +705,41 @@ patched = client.patch_pod("docker-registry", {metadata: {annotations: {key: 'va
539
705
 
540
706
  `patch_#{entity}` is called using a [strategic merge patch](https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/#notes-on-the-strategic-merge-patch). `json_patch_#{entity}` and `merge_patch_#{entity}` are also available that use JSON patch and JSON merge patch, respectively. These strategies are useful for resources that do not support strategic merge patch, such as Custom Resources. Consult the [Kubernetes docs](https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment) for more information about the different patch strategies.
541
707
 
542
- #### Get all entities of all types : all_entities
543
- Returns a hash with the following keys (node, secret, service, pod, replication_controller, namespace, resource_quota, limit_range, endpoint, event, persistent_volume, persistent_volume_claim, component_status and service_account). Each key points to an EntityList of same type.
544
- This method is a convenience method instead of calling each entity's get method separately.
708
+ ### Apply an entity
545
709
 
546
- ```ruby
547
- client.all_entities
548
- ```
710
+ This is similar to `kubectl apply --server-side` (kubeclient doesn't implement logic for client-side apply). See https://kubernetes.io/docs/reference/using-api/api-concepts/#server-side-apply
549
711
 
550
- #### Receive entity updates
551
- It is possible to receive live update notices watching the relevant entities:
712
+ For example: `apply_pod`
552
713
 
553
- ```ruby
554
- watcher = client.watch_pods
555
- watcher.each do |notice|
556
- # process notice data
557
- end
558
- ```
714
+ Input parameters - resource (Kubeclient::Resource) representing the desired state of the resource, field_manager (String) to identify the system managing the state of the resource, force (Boolean) whether or not to override a field managed by someone else.
559
715
 
560
- It is possible to interrupt the watcher from another thread with:
716
+ Example:
561
717
 
562
718
  ```ruby
563
- watcher.finish
719
+ service = Kubeclient::Resource.new(
720
+ metadata: {
721
+ name: 'redis-master',
722
+ namespace: 'staging',
723
+ },
724
+ spec: {
725
+ ...
726
+ }
727
+ )
728
+
729
+ client.apply_service(service, field_manager: 'myapp')
564
730
  ```
565
731
 
566
- #### Watch events for a particular object
567
- You can use the `field_selector` option as part of the watch methods.
732
+ ### Get all entities of all types : all_entities
733
+
734
+ Makes requests for all entities of each discovered kind (in this client's API group). This method is a convenience method instead of calling each entity's get method separately.
735
+
736
+ Returns a hash with keys being the *singular* entity kind, in lowercase underscore style. For example for core API group may return keys `"node'`, `"secret"`, `"service"`, `"pod"`, `"replication_controller"`, `"namespace"`, `"resource_quota"`, `"limit_range"`, `"endpoint"`, `"event"`, `"persistent_volume"`, `"persistent_volume_claim"`, `"component_status"`, `"service_account"`. Each key points to an EntityList of same type.
568
737
 
569
738
  ```ruby
570
- watcher = client.watch_events(namespace: 'development', field_selector: 'involvedObject.name=redis-master')
571
- watcher.each do |notice|
572
- # process notice date
573
- end
739
+ client.all_entities
574
740
  ```
575
741
 
576
- #### Get a proxy URL
742
+ ### Get a proxy URL
577
743
  You can get a complete URL for connecting a kubernetes entity via the proxy.
578
744
 
579
745
  ```ruby
@@ -588,7 +754,7 @@ client.proxy_url('pod', 'podname', 5001, 'ns')
588
754
  # => "https://localhost.localdomain:8443/api/v1/namespaces/ns/pods/podname:5001/proxy"
589
755
  ```
590
756
 
591
- #### Get the logs of a pod
757
+ ### Get the logs of a pod
592
758
  You can get the logs of a running pod, specifying the name of the pod and the
593
759
  namespace where the pod is running:
594
760
 
@@ -625,16 +791,21 @@ client.get_pod_log('pod-name', 'default', tail_lines: 10)
625
791
  # => "..."
626
792
  ```
627
793
 
794
+ Kubernetes can fetch a specific number of bytes from the log, but the exact size is not guaranteed and last line may not be terminated:
795
+ ```ruby
796
+ client.get_pod_log('pod-name', 'default', limit_bytes: 10)
797
+ # => "..."
798
+ ```
799
+
628
800
  You can also watch the logs of a pod to get a stream of data:
629
801
 
630
802
  ```ruby
631
- watcher = client.watch_pod_log('pod-name', 'default', container: 'ruby')
632
- watcher.each do |line|
803
+ client.watch_pod_log('pod-name', 'default', container: 'ruby') do |line|
633
804
  puts line
634
805
  end
635
806
  ```
636
807
 
637
- #### Process a template
808
+ ### OpenShift: Process a template
638
809
  Returns a processed template containing a list of objects to create.
639
810
  Input parameter - template (hash)
640
811
  Besides its metadata, the template should include a list of objects to be processed and a list of parameters
@@ -20,7 +20,10 @@ Edit `CHANGELOG.md` as necessary. Even if all included changes remembered to up
20
20
 
21
21
  Bump `lib/kubeclient/version.rb` manually, or by using:
22
22
  ```bash
23
- git checkout -b release-$RELEASE_VERSION
23
+ RELEASE_VERSION=x.y.z
24
+
25
+ git checkout -b "release-$RELEASE_VERSION" $RELEASE_BRANCH
26
+ # Won't work with uncommitted changes, you have to commit the changelog first.
24
27
  gem bump --version $RELEASE_VERSION
25
28
  git show # View version bump change.
26
29
  ```
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.require_paths = ['lib']
21
21
  spec.required_ruby_version = '>= 2.2.0'
22
22
 
23
- spec.add_development_dependency 'bundler', '~> 1.6'
23
+ spec.add_development_dependency 'bundler', '>= 1.6'
24
24
  spec.add_development_dependency 'rake', '~> 12.0'
25
25
  spec.add_development_dependency 'minitest'
26
26
  spec.add_development_dependency 'minitest-rg'
@@ -31,7 +31,8 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency('mocha', '~> 1.5')
32
32
  spec.add_development_dependency 'openid_connect', '~> 1.1'
33
33
 
34
+ spec.add_dependency 'jsonpath', '~> 1.0'
34
35
  spec.add_dependency 'rest-client', '~> 2.0'
35
- spec.add_dependency 'recursive-open-struct', '~> 1.0', '>= 1.0.4'
36
- spec.add_dependency 'http', '~> 3.0'
36
+ spec.add_dependency 'recursive-open-struct', '~> 1.1', '>= 1.1.1'
37
+ spec.add_dependency 'http', '>= 3.0', '< 5.0'
37
38
  end
@@ -1,11 +1,12 @@
1
1
  require 'json'
2
2
  require 'rest-client'
3
3
 
4
+ require 'kubeclient/aws_eks_credentials'
4
5
  require 'kubeclient/common'
5
6
  require 'kubeclient/config'
6
7
  require 'kubeclient/entity_list'
7
- require 'kubeclient/google_application_default_credentials'
8
8
  require 'kubeclient/exec_credentials'
9
+ require 'kubeclient/gcp_auth_provider'
9
10
  require 'kubeclient/http_error'
10
11
  require 'kubeclient/missing_kind_compatibility'
11
12
  require 'kubeclient/oidc_auth_provider'
@@ -27,7 +28,7 @@ module Kubeclient
27
28
  uri,
28
29
  '/api',
29
30
  version,
30
- options
31
+ **options
31
32
  )
32
33
  end
33
34
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kubeclient
4
+ # Get a bearer token to authenticate against aws eks.
5
+ class AmazonEksCredentials
6
+ class AmazonEksDependencyError < LoadError # rubocop:disable Lint/InheritException
7
+ end
8
+
9
+ class << self
10
+ def token(credentials, eks_cluster)
11
+ begin
12
+ require 'aws-sigv4'
13
+ require 'base64'
14
+ require 'cgi'
15
+ rescue LoadError => e
16
+ raise AmazonEksDependencyError,
17
+ 'Error requiring aws gems. Kubeclient itself does not include the following ' \
18
+ 'gems: [aws-sigv4]. To support auth-provider eks, you must ' \
19
+ "include it in your calling application. Failed with: #{e.message}"
20
+ end
21
+ # https://github.com/aws/aws-sdk-ruby/pull/1848
22
+ # Get a signer
23
+ # Note - sts only has ONE endpoint (not regional) so 'us-east-1' hardcoding should be OK
24
+ signer = Aws::Sigv4::Signer.new(
25
+ service: 'sts',
26
+ region: 'us-east-1',
27
+ credentials: credentials
28
+ )
29
+
30
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Sigv4/Signer.html#presign_url-instance_method
31
+ presigned_url_string = signer.presign_url(
32
+ http_method: 'GET',
33
+ url: 'https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15',
34
+ body: '',
35
+ credentials: credentials,
36
+ expires_in: 60,
37
+ headers: {
38
+ 'X-K8s-Aws-Id' => eks_cluster
39
+ }
40
+ )
41
+ kube_token = 'k8s-aws-v1.' + Base64.urlsafe_encode64(presigned_url_string.to_s).sub(/=*$/, '') # rubocop:disable Metrics/LineLength
42
+ kube_token
43
+ end
44
+ end
45
+ end
46
+ end
@@ -5,7 +5,7 @@ module Kubeclient
5
5
  # Common methods
6
6
  # this is mixed in by other gems
7
7
  module ClientMixin
8
- ENTITY_METHODS = %w[get watch delete create update patch json_patch merge_patch].freeze
8
+ ENTITY_METHODS = %w[get watch delete create update patch json_patch merge_patch apply].freeze
9
9
 
10
10
  DEFAULT_SSL_OPTIONS = {
11
11
  client_cert: nil,
@@ -37,10 +37,11 @@ module Kubeclient
37
37
  DEFAULT_HTTP_MAX_REDIRECTS = 10
38
38
 
39
39
  SEARCH_ARGUMENTS = {
40
- 'labelSelector' => :label_selector,
41
- 'fieldSelector' => :field_selector,
42
- 'limit' => :limit,
43
- 'continue' => :continue
40
+ 'labelSelector' => :label_selector,
41
+ 'fieldSelector' => :field_selector,
42
+ 'resourceVersion' => :resource_version,
43
+ 'limit' => :limit,
44
+ 'continue' => :continue
44
45
  }.freeze
45
46
 
46
47
  WATCH_ARGUMENTS = {
@@ -212,12 +213,12 @@ module Kubeclient
212
213
  end
213
214
 
214
215
  # watch all entities of a type e.g. watch_nodes, watch_pods, etc.
215
- define_singleton_method("watch_#{entity.method_names[1]}") do |options = {}|
216
+ define_singleton_method("watch_#{entity.method_names[1]}") do |options = {}, &block|
216
217
  # This method used to take resource_version as a param, so
217
218
  # this conversion is to keep backwards compatibility
218
219
  options = { resource_version: options } unless options.is_a?(Hash)
219
220
 
220
- watch_entities(entity.resource_name, options)
221
+ watch_entities(entity.resource_name, options, &block)
221
222
  end
222
223
 
223
224
  # get a single entity of a specific type by name
@@ -228,7 +229,7 @@ module Kubeclient
228
229
 
229
230
  define_singleton_method("delete_#{entity.method_names[0]}") \
230
231
  do |name, namespace = nil, opts = {}|
231
- delete_entity(entity.resource_name, name, namespace, opts)
232
+ delete_entity(entity.resource_name, name, namespace, **opts)
232
233
  end
233
234
 
234
235
  define_singleton_method("create_#{entity.method_names[0]}") do |entity_config|
@@ -253,6 +254,10 @@ module Kubeclient
253
254
  do |name, patch, namespace = nil|
254
255
  patch_entity(entity.resource_name, name, patch, 'merge-patch', namespace)
255
256
  end
257
+
258
+ define_singleton_method("apply_#{entity.method_names[0]}") do |*args|
259
+ apply_entity(entity.resource_name, *args)
260
+ end
256
261
  end
257
262
  end
258
263
  # rubocop:enable Metrics/BlockLength
@@ -294,7 +299,9 @@ module Kubeclient
294
299
  # :as (:raw|:ros) - defaults to :ros
295
300
  # :raw - return the raw response body as a string
296
301
  # :ros - return a collection of RecursiveOpenStruct objects
297
- def watch_entities(resource_name, options = {})
302
+ # Accepts an optional block, that will be called with each entity,
303
+ # otherwise returns a WatchStream
304
+ def watch_entities(resource_name, options = {}, &block)
298
305
  ns = build_namespace_prefix(options[:namespace])
299
306
 
300
307
  path = "watch/#{ns}#{resource_name}"
@@ -305,11 +312,13 @@ module Kubeclient
305
312
  WATCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
306
313
  uri.query = URI.encode_www_form(params) if params.any?
307
314
 
308
- Kubeclient::Common::WatchStream.new(
315
+ watcher = Kubeclient::Common::WatchStream.new(
309
316
  uri,
310
317
  http_options(uri),
311
318
  formatter: ->(value) { format_response(options[:as] || @as, value) }
312
319
  )
320
+
321
+ return_or_yield_to_watcher(watcher, &block)
313
322
  end
314
323
 
315
324
  # Accepts the following options:
@@ -406,6 +415,19 @@ module Kubeclient
406
415
  format_response(@as, response.body)
407
416
  end
408
417
 
418
+ def apply_entity(resource_name, resource, field_manager:, force: true)
419
+ name = "#{resource[:metadata][:name]}?fieldManager=#{field_manager}&force=#{force}"
420
+ ns_prefix = build_namespace_prefix(resource[:metadata][:namespace])
421
+ response = handle_exception do
422
+ rest_client[ns_prefix + resource_name + "/#{name}"]
423
+ .patch(
424
+ resource.to_json,
425
+ { 'Content-Type' => 'application/apply-patch+yaml' }.merge(@headers)
426
+ )
427
+ end
428
+ format_response(@as, response.body)
429
+ end
430
+
409
431
  def all_entities(options = {})
410
432
  discover unless @discovered
411
433
  @entities.values.each_with_object({}) do |entity, result_hash|
@@ -422,13 +444,14 @@ module Kubeclient
422
444
 
423
445
  def get_pod_log(pod_name, namespace,
424
446
  container: nil, previous: false,
425
- timestamps: false, since_time: nil, tail_lines: nil)
447
+ timestamps: false, since_time: nil, tail_lines: nil, limit_bytes: nil)
426
448
  params = {}
427
449
  params[:previous] = true if previous
428
450
  params[:container] = container if container
429
451
  params[:timestamps] = timestamps if timestamps
430
452
  params[:sinceTime] = format_datetime(since_time) if since_time
431
453
  params[:tailLines] = tail_lines if tail_lines
454
+ params[:limitBytes] = limit_bytes if limit_bytes
432
455
 
433
456
  ns = build_namespace_prefix(namespace)
434
457
  handle_exception do
@@ -437,7 +460,7 @@ module Kubeclient
437
460
  end
438
461
  end
439
462
 
440
- def watch_pod_log(pod_name, namespace, container: nil)
463
+ def watch_pod_log(pod_name, namespace, container: nil, &block)
441
464
  # Adding the "follow=true" query param tells the Kubernetes API to keep
442
465
  # the connection open and stream updates to the log.
443
466
  params = { follow: true }
@@ -449,7 +472,10 @@ module Kubeclient
449
472
  uri.path += "/#{@api_version}/#{ns}pods/#{pod_name}/log"
450
473
  uri.query = URI.encode_www_form(params)
451
474
 
452
- Kubeclient::Common::WatchStream.new(uri, http_options(uri), formatter: ->(value) { value })
475
+ watcher = Kubeclient::Common::WatchStream.new(
476
+ uri, http_options(uri), formatter: ->(value) { value }
477
+ )
478
+ return_or_yield_to_watcher(watcher, &block)
453
479
  end
454
480
 
455
481
  def proxy_url(kind, name, port, namespace = '')
@@ -586,6 +612,16 @@ module Kubeclient
586
612
  raise ArgumentError, msg unless File.readable?(@auth_options[:bearer_token_file])
587
613
  end
588
614
 
615
+ def return_or_yield_to_watcher(watcher, &block)
616
+ return watcher unless block_given?
617
+
618
+ begin
619
+ watcher.each(&block)
620
+ ensure
621
+ watcher.finish
622
+ end
623
+ end
624
+
589
625
  def http_options(uri)
590
626
  options = {
591
627
  basic_auth_user: @auth_options[:username],
@@ -150,17 +150,10 @@ module Kubeclient
150
150
  if user.key?('token')
151
151
  options[:bearer_token] = user['token']
152
152
  elsif user.key?('exec')
153
- exec_opts = user['exec'].dup
154
- exec_opts['command'] = ext_command_path(exec_opts['command']) if exec_opts['command']
153
+ exec_opts = expand_command_option(user['exec'], 'command')
155
154
  options[:bearer_token] = Kubeclient::ExecCredentials.token(exec_opts)
156
155
  elsif user.key?('auth-provider')
157
- auth_provider = user['auth-provider']
158
- options[:bearer_token] = case auth_provider['name']
159
- when 'gcp'
160
- then Kubeclient::GoogleApplicationDefaultCredentials.token
161
- when 'oidc'
162
- then Kubeclient::OIDCAuthProvider.token(auth_provider['config'])
163
- end
156
+ options[:bearer_token] = fetch_token_from_provider(user['auth-provider'])
164
157
  else
165
158
  %w[username password].each do |attr|
166
159
  options[attr.to_sym] = user[attr] if user.key?(attr)
@@ -168,5 +161,22 @@ module Kubeclient
168
161
  end
169
162
  options
170
163
  end
164
+
165
+ def fetch_token_from_provider(auth_provider)
166
+ case auth_provider['name']
167
+ when 'gcp'
168
+ config = expand_command_option(auth_provider['config'], 'cmd-path')
169
+ Kubeclient::GCPAuthProvider.token(config)
170
+ when 'oidc'
171
+ Kubeclient::OIDCAuthProvider.token(auth_provider['config'])
172
+ end
173
+ end
174
+
175
+ def expand_command_option(config, key)
176
+ config = config.dup
177
+ config[key] = ext_command_path(config[key]) if config[key]
178
+
179
+ config
180
+ end
171
181
  end
172
182
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kubeclient/google_application_default_credentials'
4
+ require 'kubeclient/gcp_command_credentials'
5
+
6
+ module Kubeclient
7
+ # Handle different ways to get a bearer token for Google Cloud Platform.
8
+ class GCPAuthProvider
9
+ class << self
10
+ def token(config)
11
+ if config.key?('cmd-path')
12
+ Kubeclient::GCPCommandCredentials.token(config)
13
+ else
14
+ Kubeclient::GoogleApplicationDefaultCredentials.token
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kubeclient
4
+ # Generates a bearer token for Google Cloud Platform.
5
+ class GCPCommandCredentials
6
+ class << self
7
+ def token(config)
8
+ require 'open3'
9
+ require 'shellwords'
10
+ require 'json'
11
+ require 'jsonpath'
12
+
13
+ cmd = config['cmd-path']
14
+ args = config['cmd-args']
15
+ token_key = config['token-key']
16
+
17
+ out, err, st = Open3.capture3(cmd, *args.split(' '))
18
+
19
+ raise "exec command failed: #{err}" unless st.success?
20
+
21
+ extract_token(out, token_key)
22
+ end
23
+
24
+ private
25
+
26
+ def extract_token(output, token_key)
27
+ JsonPath.on(output, token_key.gsub(/^{|}$/, '')).first
28
+ end
29
+ end
30
+ end
31
+ end
@@ -16,7 +16,12 @@ module Kubeclient
16
16
  'googleauth gem. To support auth-provider gcp, you must include it in your ' \
17
17
  "calling application. Failed with: #{e.message}"
18
18
  end
19
- scopes = ['https://www.googleapis.com/auth/cloud-platform']
19
+
20
+ scopes = [
21
+ 'https://www.googleapis.com/auth/cloud-platform',
22
+ 'https://www.googleapis.com/auth/userinfo.email'
23
+ ]
24
+
20
25
  authorization = Google::Auth.get_application_default(scopes)
21
26
  authorization.apply({})
22
27
  authorization.access_token
@@ -21,9 +21,7 @@ module Kubeclient
21
21
  discovery = OpenIDConnect::Discovery::Provider::Config.discover! issuer_url
22
22
 
23
23
  if provider_config.key? 'id-token'
24
- id_token = OpenIDConnect::ResponseObject::IdToken.decode provider_config['id-token'],
25
- discovery.jwks
26
- return provider_config['id-token'] unless expired?(id_token)
24
+ return provider_config['id-token'] unless expired?(provider_config['id-token'], discovery)
27
25
  end
28
26
 
29
27
  client = OpenIDConnect::Client.new(
@@ -37,9 +35,17 @@ module Kubeclient
37
35
  client.access_token!.id_token
38
36
  end
39
37
 
40
- def expired?(id_token)
38
+ def expired?(id_token, discovery)
39
+ decoded_token = OpenIDConnect::ResponseObject::IdToken.decode(
40
+ id_token,
41
+ discovery.jwks
42
+ )
41
43
  # If token expired or expiring within 60 seconds
42
- Time.now.to_i + 60 > id_token.exp.to_i
44
+ Time.now.to_i + 60 > decoded_token.exp.to_i
45
+ rescue JSON::JWK::Set::KidNotFound
46
+ # Token cannot be verified: the kid it was signed with is not available for discovery
47
+ # Consider it expired and fetch a new one.
48
+ true
43
49
  end
44
50
  end
45
51
  end
@@ -1,4 +1,4 @@
1
1
  # Kubernetes REST-API Client
2
2
  module Kubeclient
3
- VERSION = '4.3.0'.freeze
3
+ VERSION = '4.8.0'.freeze
4
4
  end
@@ -0,0 +1,26 @@
1
+ apiVersion: v1
2
+ clusters:
3
+ - cluster:
4
+ server: https://localhost:8443
5
+ insecure-skip-tls-verify: true
6
+ name: localhost:8443
7
+ contexts:
8
+ - context:
9
+ cluster: localhost:8443
10
+ namespace: default
11
+ user: application-default-credentials
12
+ name: localhost/application-default-credentials
13
+ kind: Config
14
+ preferences: {}
15
+ users:
16
+ - name: application-default-credentials
17
+ user:
18
+ auth-provider:
19
+ config:
20
+ access-token: <fake_token>
21
+ cmd-args: config config-helper --format=json
22
+ cmd-path: /path/to/gcloud
23
+ expiry: 2019-04-09T19:26:18Z
24
+ expiry-key: '{.credential.token_expiry}'
25
+ token-key: '{.credential.access_token}'
26
+ name: gcp
@@ -146,6 +146,20 @@ class KubeclientConfigTest < MiniTest::Test
146
146
  assert_equal({ bearer_token: 'token1' }, context.auth_options)
147
147
  end
148
148
 
149
+ def test_gcp_command_auth
150
+ Kubeclient::GCPCommandCredentials.expects(:token)
151
+ .with('access-token' => '<fake_token>',
152
+ 'cmd-args' => 'config config-helper --format=json',
153
+ 'cmd-path' => '/path/to/gcloud',
154
+ 'expiry' => '2019-04-09 19:26:18 UTC',
155
+ 'expiry-key' => '{.credential.token_expiry}',
156
+ 'token-key' => '{.credential.access_token}')
157
+ .returns('token1')
158
+ .once
159
+ config = Kubeclient::Config.read(config_file('gcpcmdauth.kubeconfig'))
160
+ config.context(config.contexts.first)
161
+ end
162
+
149
163
  def test_oidc_auth_provider
150
164
  Kubeclient::OIDCAuthProvider.expects(:token)
151
165
  .with('client-id' => 'fake-client-id',
@@ -0,0 +1,27 @@
1
+ require_relative 'test_helper'
2
+ require 'open3'
3
+
4
+ # Unit tests for the GCPCommandCredentials token provider
5
+ class GCPCommandCredentialsTest < MiniTest::Test
6
+ def test_token
7
+ opts = { 'cmd-args' => 'config config-helper --format=json',
8
+ 'cmd-path' => '/path/to/gcloud',
9
+ 'expiry-key' => '{.credential.token_expiry}',
10
+ 'token-key' => '{.credential.access_token}' }
11
+
12
+ creds = JSON.dump(
13
+ 'credential' => {
14
+ 'access_token' => '9A3A941836F2458175BE18AA1971EBBF47949B07',
15
+ 'token_expiry' => '2019-04-12T15:02:51Z'
16
+ }
17
+ )
18
+
19
+ st = Minitest::Mock.new
20
+ st.expect(:success?, true)
21
+
22
+ Open3.stub(:capture3, [creds, nil, st]) do
23
+ assert_equal('9A3A941836F2458175BE18AA1971EBBF47949B07',
24
+ Kubeclient::GCPCommandCredentials.token(opts))
25
+ end
26
+ end
27
+ end
@@ -296,6 +296,22 @@ class KubeclientTest < MiniTest::Test
296
296
  )
297
297
  end
298
298
 
299
+ def test_entities_with_resource_version
300
+ version = '329'
301
+
302
+ stub_core_api_list
303
+ stub_get_services
304
+
305
+ services = client.get_services(resource_version: version)
306
+
307
+ assert_instance_of(Kubeclient::Common::EntityList, services)
308
+ assert_requested(
309
+ :get,
310
+ "http://localhost:8080/api/v1/services?resourceVersion=#{version}",
311
+ times: 1
312
+ )
313
+ end
314
+
299
315
  def test_entities_with_field_selector
300
316
  selector = 'involvedObject.name=redis-master'
301
317
 
@@ -57,6 +57,25 @@ class OIDCAuthProviderTest < MiniTest::Test
57
57
  end
58
58
  end
59
59
 
60
+ def test_token_with_unknown_kid
61
+ OpenIDConnect::Discovery::Provider::Config.stub(:discover!, discovery_mock) do
62
+ OpenIDConnect::ResponseObject::IdToken.stub(
63
+ :decode, ->(_token, _jwks) { raise JSON::JWK::Set::KidNotFound }
64
+ ) do
65
+ OpenIDConnect::Client.stub(:new, openid_client_mock) do
66
+ retrieved_id_token = Kubeclient::OIDCAuthProvider.token(
67
+ 'client-id' => @client_id,
68
+ 'client-secret' => @client_secret,
69
+ 'id-token' => @id_token,
70
+ 'idp-issuer-url' => @idp_issuer_url,
71
+ 'refresh-token' => @refresh_token
72
+ )
73
+ assert_equal(@new_id_token, retrieved_id_token)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
60
79
  private
61
80
 
62
81
  def openid_client_mock
@@ -69,12 +69,31 @@ class TestPodLog < MiniTest::Test
69
69
  times: 1)
70
70
  end
71
71
 
72
+ def test_get_pod_limit_bytes
73
+ selected_bytes = open_test_file('pod_log.txt').read(10)
74
+
75
+ stub_request(:get, %r{/namespaces/default/pods/[a-z0-9-]+/log})
76
+ .to_return(body: selected_bytes,
77
+ status: 200)
78
+
79
+ client = Kubeclient::Client.new('http://localhost:8080/api/', 'v1')
80
+ retrieved_log = client.get_pod_log('redis-master-pod',
81
+ 'default',
82
+ limit_bytes: 10)
83
+
84
+ assert_equal(selected_bytes, retrieved_log)
85
+
86
+ assert_requested(:get,
87
+ 'http://localhost:8080/api/v1/namespaces/default/pods/redis-master-pod/log?limitBytes=10',
88
+ times: 1)
89
+ end
90
+
72
91
  def test_watch_pod_log
73
- expected_lines = open_test_file('pod_log.txt').read.split("\n")
92
+ file = open_test_file('pod_log.txt')
93
+ expected_lines = file.read.split("\n")
74
94
 
75
95
  stub_request(:get, %r{/namespaces/default/pods/[a-z0-9-]+/log\?.*follow})
76
- .to_return(body: open_test_file('pod_log.txt'),
77
- status: 200)
96
+ .to_return(body: file, status: 200)
78
97
 
79
98
  client = Kubeclient::Client.new('http://localhost:8080/api/', 'v1')
80
99
 
@@ -85,6 +104,21 @@ class TestPodLog < MiniTest::Test
85
104
  end
86
105
  end
87
106
 
107
+ def test_watch_pod_log_with_block
108
+ file = open_test_file('pod_log.txt')
109
+ first = file.readlines.first.chomp
110
+
111
+ stub_request(:get, %r{/namespaces/default/pods/[a-z0-9-]+/log\?.*follow})
112
+ .to_return(body: file, status: 200)
113
+
114
+ client = Kubeclient::Client.new('http://localhost:8080/api/', 'v1')
115
+
116
+ client.watch_pod_log('redis-master-pod', 'default') do |line|
117
+ assert_equal first, line
118
+ break
119
+ end
120
+ end
121
+
88
122
  def test_watch_pod_log_follow_redirect
89
123
  expected_lines = open_test_file('pod_log.txt').read.split("\n")
90
124
  redirect = 'http://localhost:1234/api/namespaces/default/pods/redis-master-pod/log'
@@ -274,6 +274,33 @@ class TestService < MiniTest::Test
274
274
  end
275
275
  end
276
276
 
277
+ def test_apply_service
278
+ service = Kubeclient::Resource.new
279
+ name = 'my_service'
280
+
281
+ service.metadata = {}
282
+ service.metadata.name = name
283
+ service.metadata.namespace = 'development'
284
+ service.metadata.annotations = {}
285
+ service.metadata.annotations['key'] = 'value'
286
+
287
+ stub_core_api_list
288
+ resource_name = "#{name}?fieldManager=myapp&force=true"
289
+ expected_url = "http://localhost:8080/api/v1/namespaces/development/services/#{resource_name}"
290
+ stub_request(:patch, expected_url)
291
+ .to_return(body: open_test_file('service_patch.json'), status: 200)
292
+
293
+ client = Kubeclient::Client.new('http://localhost:8080/api/', 'v1')
294
+ service = client.apply_service(service, field_manager: 'myapp')
295
+ assert_kind_of(RecursiveOpenStruct, service)
296
+
297
+ assert_requested(:patch, expected_url, times: 1) do |req|
298
+ data = JSON.parse(req.body)
299
+ req.headers['Content-Type'] == 'application/apply-patch+yaml' &&
300
+ data['metadata']['annotations']['key'] == 'value'
301
+ end
302
+ end
303
+
277
304
  def test_json_patch_service
278
305
  service = Kubeclient::Resource.new
279
306
  name = 'my-service'
@@ -27,6 +27,19 @@ class TestWatch < MiniTest::Test
27
27
  end
28
28
  end
29
29
 
30
+ def test_watch_pod_block
31
+ stub_core_api_list
32
+ stub_request(:get, %r{/watch/pods})
33
+ .to_return(body: open_test_file('watch_stream.json'),
34
+ status: 200)
35
+
36
+ client = Kubeclient::Client.new('http://localhost:8080/api/', 'v1')
37
+ yielded = []
38
+ client.watch_pods { |notice| yielded << notice.type }
39
+
40
+ assert_equal %w[ADDED MODIFIED DELETED], yielded
41
+ end
42
+
30
43
  def test_watch_pod_raw
31
44
  stub_core_api_list
32
45
 
metadata CHANGED
@@ -1,27 +1,27 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kubeclient
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0
4
+ version: 4.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alissa Bonas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-03 00:00:00.000000000 Z
11
+ date: 2020-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.6'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.6'
27
27
  - !ruby/object:Gem::Dependency
@@ -150,6 +150,20 @@ dependencies:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
152
  version: '1.1'
153
+ - !ruby/object:Gem::Dependency
154
+ name: jsonpath
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '1.0'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '1.0'
153
167
  - !ruby/object:Gem::Dependency
154
168
  name: rest-client
155
169
  requirement: !ruby/object:Gem::Requirement
@@ -170,34 +184,40 @@ dependencies:
170
184
  requirements:
171
185
  - - "~>"
172
186
  - !ruby/object:Gem::Version
173
- version: '1.0'
187
+ version: '1.1'
174
188
  - - ">="
175
189
  - !ruby/object:Gem::Version
176
- version: 1.0.4
190
+ version: 1.1.1
177
191
  type: :runtime
178
192
  prerelease: false
179
193
  version_requirements: !ruby/object:Gem::Requirement
180
194
  requirements:
181
195
  - - "~>"
182
196
  - !ruby/object:Gem::Version
183
- version: '1.0'
197
+ version: '1.1'
184
198
  - - ">="
185
199
  - !ruby/object:Gem::Version
186
- version: 1.0.4
200
+ version: 1.1.1
187
201
  - !ruby/object:Gem::Dependency
188
202
  name: http
189
203
  requirement: !ruby/object:Gem::Requirement
190
204
  requirements:
191
- - - "~>"
205
+ - - ">="
192
206
  - !ruby/object:Gem::Version
193
207
  version: '3.0'
208
+ - - "<"
209
+ - !ruby/object:Gem::Version
210
+ version: '5.0'
194
211
  type: :runtime
195
212
  prerelease: false
196
213
  version_requirements: !ruby/object:Gem::Requirement
197
214
  requirements:
198
- - - "~>"
215
+ - - ">="
199
216
  - !ruby/object:Gem::Version
200
217
  version: '3.0'
218
+ - - "<"
219
+ - !ruby/object:Gem::Version
220
+ version: '5.0'
201
221
  description: A client for Kubernetes REST api
202
222
  email:
203
223
  - abonas@redhat.com
@@ -216,10 +236,13 @@ files:
216
236
  - Rakefile
217
237
  - kubeclient.gemspec
218
238
  - lib/kubeclient.rb
239
+ - lib/kubeclient/aws_eks_credentials.rb
219
240
  - lib/kubeclient/common.rb
220
241
  - lib/kubeclient/config.rb
221
242
  - lib/kubeclient/entity_list.rb
222
243
  - lib/kubeclient/exec_credentials.rb
244
+ - lib/kubeclient/gcp_auth_provider.rb
245
+ - lib/kubeclient/gcp_command_credentials.rb
223
246
  - lib/kubeclient/google_application_default_credentials.rb
224
247
  - lib/kubeclient/http_error.rb
225
248
  - lib/kubeclient/missing_kind_compatibility.rb
@@ -236,6 +259,7 @@ files:
236
259
  - test/config/external-key.rsa
237
260
  - test/config/external.kubeconfig
238
261
  - test/config/gcpauth.kubeconfig
262
+ - test/config/gcpcmdauth.kubeconfig
239
263
  - test/config/nouser.kubeconfig
240
264
  - test/config/oidcauth.kubeconfig
241
265
  - test/config/timestamps.kubeconfig
@@ -304,6 +328,7 @@ files:
304
328
  - test/test_config.rb
305
329
  - test/test_endpoint.rb
306
330
  - test/test_exec_credentials.rb
331
+ - test/test_gcp_command_credentials.rb
307
332
  - test/test_google_application_default_credentials.rb
308
333
  - test/test_guestbook_go.rb
309
334
  - test/test_helper.rb
@@ -347,8 +372,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
347
372
  - !ruby/object:Gem::Version
348
373
  version: '0'
349
374
  requirements: []
350
- rubyforge_project:
351
- rubygems_version: 2.7.6
375
+ rubygems_version: 3.1.2
352
376
  signing_key:
353
377
  specification_version: 4
354
378
  summary: A client for Kubernetes REST api
@@ -361,6 +385,7 @@ test_files:
361
385
  - test/config/external-key.rsa
362
386
  - test/config/external.kubeconfig
363
387
  - test/config/gcpauth.kubeconfig
388
+ - test/config/gcpcmdauth.kubeconfig
364
389
  - test/config/nouser.kubeconfig
365
390
  - test/config/oidcauth.kubeconfig
366
391
  - test/config/timestamps.kubeconfig
@@ -429,6 +454,7 @@ test_files:
429
454
  - test/test_config.rb
430
455
  - test/test_endpoint.rb
431
456
  - test/test_exec_credentials.rb
457
+ - test/test_gcp_command_credentials.rb
432
458
  - test/test_google_application_default_credentials.rb
433
459
  - test/test_guestbook_go.rb
434
460
  - test/test_helper.rb