kubeclient 4.5.0 → 4.9.1

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: 5b11e0f567dcbaee703e9964d576144e5be7a44d49d9476cd8a37b51106076c8
4
- data.tar.gz: '0919953ac39325564d59f990daa8485ad1ee4fcbbfff8b6078a6812f3bbe549a'
3
+ metadata.gz: 1ea191875dc5c9e99d49152f6615caa50478bc8604a4de9c6fdda0d46da75ea5
4
+ data.tar.gz: 84214e5b1f2f3116aeb646828e0a5cf6456173493abf1fb384fe6132a723271d
5
5
  SHA512:
6
- metadata.gz: 2164fee7d4aaeb93baffdb1c3989c06fcdf0c39fd597fbd30c812494cabafb24a53d0e7ed00d9cad9cb3369cc04375daa9bdb63862e002c85792b2fb85f391f3
7
- data.tar.gz: 4012343cd31a0cdf0a307e89a32ad44dc73e2f847887f4b9f1791a4169c6c56144c0831683a7d142fa0f8469cae73d44c9744d6042a30043fa73c4ddb0e048df
6
+ metadata.gz: 549974ed0fbec82aa99df19db4829af4da21eec3ab037037c9a6880ab2b7f0bbf9a854ee9e1713a9eedccf6e7fafbc593301a8e67b745e54d88f9da158f6b596
7
+ data.tar.gz: 2f44720eca3c585e69c562b218f6e17c1ed0fb13dd5e26177fef6fa3ec610a6c0b1f056102ac3d3e3d9cf21801fe13d8560c509423838304cac0a40b66eedae8
@@ -11,13 +11,18 @@ rvm:
11
11
  - "2.3.0"
12
12
  - "2.4.0"
13
13
  - "2.5.0"
14
- - "2.6.3"
14
+ - "2.6.0"
15
+ - "2.7.0"
15
16
  env:
16
17
  - TASK=test
17
18
  matrix:
18
19
  exclude:
19
20
  - os: osx
20
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.
21
26
  include:
22
27
  - os: linux
23
28
  rvm: "2.5.0"
@@ -4,10 +4,48 @@ 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.5.02019-09-27
7
+ ## 4.9.12020-08-31
8
+ ### Fixed
9
+ - Now should work with apiserver deployed not at root of domain but a sub-path,
10
+ which is standard with Rancher.
11
+ Notably, `create_...` methods were sending bad apiVersion and getting 400 error.
12
+ (#457, hopefully fixes #318, #418 and https://gitlab.com/gitlab-org/gitlab/-/issues/22043)
13
+
14
+ ## 4.9.0 - 2020-08-03
15
+ ### Added
16
+ - Support for `user: exec` credential plugins using TLS client auth (#453)
17
+
18
+ ## 4.8.0 — 2020-07-03
8
19
 
9
20
  ### Added
21
+ - Support for server-side apply (#448).
10
22
 
23
+ ### Fixed
24
+ - Declared forgotten dependency on jsonpath, needed for `gcp` provider with `cmd-path` (#450).
25
+
26
+ ## 4.7.0 — 2020-06-14
27
+
28
+ ### Fixed
29
+ - Ruby 2.7 compatibility: bumped minimum recursive-open-struct to one that works on 2.7 (#439).
30
+ - Ruby 2.7 warnings (#433, #438).
31
+ - Improved watch documentation, including behavior planned to change in 5.0.0 (#436).
32
+
33
+ ### Added
34
+ - Google Application Default Credentials: Added `userinfo.email` to requested scopes, which is necessary for RBAC policies (#441).
35
+
36
+ ## 4.6.0 — 2019-12-30
37
+
38
+ ### Fixed
39
+ - AmazonEksCredentials was sometimes leaving base64 padding that IAM auth of the EKS cluster rejects. Now padding is always stripped. (#424, #423)
40
+
41
+ ### Added
42
+ - Allow calling `watch_foos` methods with a block, simpler to use and guarantees closing the connection. (#425)
43
+
44
+ - Support `limitBytes` query parameter for `get_pod_log`. (#426)
45
+
46
+ ## 4.5.0 — 2019-09-27
47
+
48
+ ### Added
11
49
  - Support `:resourceVersion` parameter in `get_foos` methods (similar to existing support in `watch_foos` methods). (#420)
12
50
 
13
51
  - Relax dependency on `http` gem to allow both 3.x and 4.x. (#413)
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
@@ -420,16 +420,48 @@ We try to support the last 3 minor versions, matching the [official support poli
420
420
  Kubernetes 1.2 and below have known issues and are unsupported.
421
421
  Kubernetes 1.3 presumed to still work although nobody is really testing on such old versions...
422
422
 
423
- ## Examples:
423
+ ## Supported actions & examples:
424
424
 
425
- #### Get all instances of a specific entity type
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
+ ```
454
+
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
426
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`
427
459
 
428
460
  ```ruby
429
461
  pods = client.get_pods
430
462
  ```
431
463
 
432
- Get all entities of a specific type in a namespace:<br>
464
+ Get all entities of a specific type in a namespace:
433
465
 
434
466
  ```ruby
435
467
  services = client.get_services(namespace: 'development')
@@ -447,13 +479,22 @@ You can specify multiple labels (that option will return entities which have bot
447
479
  pods = client.get_pods(label_selector: 'name=redis-master,app=redis')
448
480
  ```
449
481
 
450
- You can get entities at a specific version by specifying a parameter named `resource_version` (named `resourceVersion` in Kubernetes server):
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
+ ```
451
486
 
487
+ You can ask for entities at a specific version by specifying a parameter named `resource_version`:
452
488
  ```ruby
453
489
  pods = client.get_pods(resource_version: '0')
454
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.
455
494
 
456
- Get all entities of a specific type in chunks:
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
457
498
 
458
499
  ```ruby
459
500
  continue = nil
@@ -508,7 +549,87 @@ Other formats are:
508
549
  - `:parsed` for `JSON.parse`
509
550
  - `:parsed_symbolized` for `JSON.parse(..., symbolize_names: true)`
510
551
 
511
- #### 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)
512
633
 
513
634
  For example: `delete_pod "pod name"` , `delete_replication_controller "rc name"`, `delete_node "node name"`, `delete_secret "secret name"`
514
635
 
@@ -532,7 +653,7 @@ delete_options = Kubeclient::Resource.new(
532
653
  client.delete_deployment(deployment_name, namespace, delete_options: delete_options)
533
654
  ```
534
655
 
535
- #### Create an entity
656
+ ### Create an entity
536
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`
537
658
 
538
659
  Input parameter - object of type `Service`, `Pod`, `ReplicationController`.
@@ -558,7 +679,7 @@ service.metadata.labels.role = 'slave'
558
679
  client.create_service(service)
559
680
  ```
560
681
 
561
- #### Update an entity
682
+ ### Update an entity
562
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`
563
684
 
564
685
  Input parameter - object of type `Pod`, `Service`, `ReplicationController` etc.
@@ -569,7 +690,7 @@ The below example is for v1
569
690
  updated = client.update_service(service1)
570
691
  ```
571
692
 
572
- #### Patch an entity (by name)
693
+ ### Patch an entity (by name)
573
694
  For example: `patch_pod`, `patch_service`, `patch_secret`, `patch_resource_quota`, `patch_persistent_volume`
574
695
 
575
696
  Input parameters - name (string) specifying the entity name, patch (hash) to be applied to the resource, optional: namespace name (string)
@@ -584,41 +705,41 @@ patched = client.patch_pod("docker-registry", {metadata: {annotations: {key: 'va
584
705
 
585
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.
586
707
 
587
- #### Get all entities of all types : all_entities
588
- 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.
589
- This method is a convenience method instead of calling each entity's get method separately.
708
+ ### Apply an entity
590
709
 
591
- ```ruby
592
- client.all_entities
593
- ```
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
594
711
 
595
- #### Receive entity updates
596
- It is possible to receive live update notices watching the relevant entities:
712
+ For example: `apply_pod`
597
713
 
598
- ```ruby
599
- watcher = client.watch_pods
600
- watcher.each do |notice|
601
- # process notice data
602
- end
603
- ```
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.
604
715
 
605
- It is possible to interrupt the watcher from another thread with:
716
+ Example:
606
717
 
607
718
  ```ruby
608
- 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')
609
730
  ```
610
731
 
611
- #### Watch events for a particular object
612
- 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.
613
737
 
614
738
  ```ruby
615
- watcher = client.watch_events(namespace: 'development', field_selector: 'involvedObject.name=redis-master')
616
- watcher.each do |notice|
617
- # process notice date
618
- end
739
+ client.all_entities
619
740
  ```
620
741
 
621
- #### Get a proxy URL
742
+ ### Get a proxy URL
622
743
  You can get a complete URL for connecting a kubernetes entity via the proxy.
623
744
 
624
745
  ```ruby
@@ -633,7 +754,7 @@ client.proxy_url('pod', 'podname', 5001, 'ns')
633
754
  # => "https://localhost.localdomain:8443/api/v1/namespaces/ns/pods/podname:5001/proxy"
634
755
  ```
635
756
 
636
- #### Get the logs of a pod
757
+ ### Get the logs of a pod
637
758
  You can get the logs of a running pod, specifying the name of the pod and the
638
759
  namespace where the pod is running:
639
760
 
@@ -670,16 +791,21 @@ client.get_pod_log('pod-name', 'default', tail_lines: 10)
670
791
  # => "..."
671
792
  ```
672
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
+
673
800
  You can also watch the logs of a pod to get a stream of data:
674
801
 
675
802
  ```ruby
676
- watcher = client.watch_pod_log('pod-name', 'default', container: 'ruby')
677
- watcher.each do |line|
803
+ client.watch_pod_log('pod-name', 'default', container: 'ruby') do |line|
678
804
  puts line
679
805
  end
680
806
  ```
681
807
 
682
- #### Process a template
808
+ ### OpenShift: Process a template
683
809
  Returns a processed template containing a list of objects to create.
684
810
  Input parameter - template (hash)
685
811
  Besides its metadata, the template should include a list of objects to be processed and a list of parameters
@@ -20,7 +20,9 @@ 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
24
26
  # Won't work with uncommitted changes, you have to commit the changelog first.
25
27
  gem bump --version $RELEASE_VERSION
26
28
  git show # View version bump change.
@@ -30,9 +30,9 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency 'googleauth', '~> 0.5.1'
31
31
  spec.add_development_dependency('mocha', '~> 1.5')
32
32
  spec.add_development_dependency 'openid_connect', '~> 1.1'
33
- spec.add_development_dependency 'jsonpath', '~> 1.0'
34
33
 
34
+ spec.add_dependency 'jsonpath', '~> 1.0'
35
35
  spec.add_dependency 'rest-client', '~> 2.0'
36
- spec.add_dependency 'recursive-open-struct', '~> 1.0', '>= 1.0.4'
36
+ spec.add_dependency 'recursive-open-struct', '~> 1.1', '>= 1.1.1'
37
37
  spec.add_dependency 'http', '>= 3.0', '< 5.0'
38
38
  end
@@ -28,7 +28,7 @@ module Kubeclient
28
28
  uri,
29
29
  '/api',
30
30
  version,
31
- options
31
+ **options
32
32
  )
33
33
  end
34
34
  end
@@ -38,7 +38,7 @@ module Kubeclient
38
38
  'X-K8s-Aws-Id' => eks_cluster
39
39
  }
40
40
  )
41
- kube_token = 'k8s-aws-v1.' + Base64.urlsafe_encode64(presigned_url_string.to_s).chomp('==')
41
+ kube_token = 'k8s-aws-v1.' + Base64.urlsafe_encode64(presigned_url_string.to_s).sub(/=*$/, '') # rubocop:disable Metrics/LineLength
42
42
  kube_token
43
43
  end
44
44
  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,
@@ -194,10 +194,22 @@ module Kubeclient
194
194
  def handle_uri(uri, path)
195
195
  raise ArgumentError, 'Missing uri' unless uri
196
196
  @api_endpoint = (uri.is_a?(URI) ? uri : URI.parse(uri))
197
- @api_endpoint.path = path if @api_endpoint.path.empty?
198
- @api_endpoint.path = @api_endpoint.path.chop if @api_endpoint.path.end_with?('/')
199
- components = @api_endpoint.path.to_s.split('/') # ["", "api"] or ["", "apis", batch]
200
- @api_group = components.length > 2 ? components[2] + '/' : ''
197
+
198
+ # This regex will anchor at the last `/api`, `/oapi` or`/apis/:group`) part of the URL
199
+ # The whole path will be matched and if existing, the api_group will be extracted.
200
+ re = /^(?<path>.*\/o?api(?:s\/(?<apigroup>[^\/]+))?)$/mi
201
+ match = re.match(@api_endpoint.path.chomp('/'))
202
+
203
+ if match
204
+ # Since `re` captures 2 groups, match will always have 3 elements
205
+ # If thus we have a non-nil value in match 2, this is our api_group.
206
+ @api_group = match[:apigroup].nil? ? '' : match[:apigroup] + '/'
207
+ @api_endpoint.path = match[:path]
208
+ else
209
+ # This is a fallback, for when `/api` was not provided as part of the uri
210
+ @api_group = ''
211
+ @api_endpoint.path = @api_endpoint.path.chomp('/') + path
212
+ end
201
213
  end
202
214
 
203
215
  def build_namespace_prefix(namespace)
@@ -213,12 +225,12 @@ module Kubeclient
213
225
  end
214
226
 
215
227
  # watch all entities of a type e.g. watch_nodes, watch_pods, etc.
216
- define_singleton_method("watch_#{entity.method_names[1]}") do |options = {}|
228
+ define_singleton_method("watch_#{entity.method_names[1]}") do |options = {}, &block|
217
229
  # This method used to take resource_version as a param, so
218
230
  # this conversion is to keep backwards compatibility
219
231
  options = { resource_version: options } unless options.is_a?(Hash)
220
232
 
221
- watch_entities(entity.resource_name, options)
233
+ watch_entities(entity.resource_name, options, &block)
222
234
  end
223
235
 
224
236
  # get a single entity of a specific type by name
@@ -229,7 +241,7 @@ module Kubeclient
229
241
 
230
242
  define_singleton_method("delete_#{entity.method_names[0]}") \
231
243
  do |name, namespace = nil, opts = {}|
232
- delete_entity(entity.resource_name, name, namespace, opts)
244
+ delete_entity(entity.resource_name, name, namespace, **opts)
233
245
  end
234
246
 
235
247
  define_singleton_method("create_#{entity.method_names[0]}") do |entity_config|
@@ -254,6 +266,10 @@ module Kubeclient
254
266
  do |name, patch, namespace = nil|
255
267
  patch_entity(entity.resource_name, name, patch, 'merge-patch', namespace)
256
268
  end
269
+
270
+ define_singleton_method("apply_#{entity.method_names[0]}") do |*args|
271
+ apply_entity(entity.resource_name, *args)
272
+ end
257
273
  end
258
274
  end
259
275
  # rubocop:enable Metrics/BlockLength
@@ -295,7 +311,9 @@ module Kubeclient
295
311
  # :as (:raw|:ros) - defaults to :ros
296
312
  # :raw - return the raw response body as a string
297
313
  # :ros - return a collection of RecursiveOpenStruct objects
298
- def watch_entities(resource_name, options = {})
314
+ # Accepts an optional block, that will be called with each entity,
315
+ # otherwise returns a WatchStream
316
+ def watch_entities(resource_name, options = {}, &block)
299
317
  ns = build_namespace_prefix(options[:namespace])
300
318
 
301
319
  path = "watch/#{ns}#{resource_name}"
@@ -306,11 +324,13 @@ module Kubeclient
306
324
  WATCH_ARGUMENTS.each { |k, v| params[k] = options[v] if options[v] }
307
325
  uri.query = URI.encode_www_form(params) if params.any?
308
326
 
309
- Kubeclient::Common::WatchStream.new(
327
+ watcher = Kubeclient::Common::WatchStream.new(
310
328
  uri,
311
329
  http_options(uri),
312
330
  formatter: ->(value) { format_response(options[:as] || @as, value) }
313
331
  )
332
+
333
+ return_or_yield_to_watcher(watcher, &block)
314
334
  end
315
335
 
316
336
  # Accepts the following options:
@@ -407,6 +427,19 @@ module Kubeclient
407
427
  format_response(@as, response.body)
408
428
  end
409
429
 
430
+ def apply_entity(resource_name, resource, field_manager:, force: true)
431
+ name = "#{resource[:metadata][:name]}?fieldManager=#{field_manager}&force=#{force}"
432
+ ns_prefix = build_namespace_prefix(resource[:metadata][:namespace])
433
+ response = handle_exception do
434
+ rest_client[ns_prefix + resource_name + "/#{name}"]
435
+ .patch(
436
+ resource.to_json,
437
+ { 'Content-Type' => 'application/apply-patch+yaml' }.merge(@headers)
438
+ )
439
+ end
440
+ format_response(@as, response.body)
441
+ end
442
+
410
443
  def all_entities(options = {})
411
444
  discover unless @discovered
412
445
  @entities.values.each_with_object({}) do |entity, result_hash|
@@ -423,13 +456,14 @@ module Kubeclient
423
456
 
424
457
  def get_pod_log(pod_name, namespace,
425
458
  container: nil, previous: false,
426
- timestamps: false, since_time: nil, tail_lines: nil)
459
+ timestamps: false, since_time: nil, tail_lines: nil, limit_bytes: nil)
427
460
  params = {}
428
461
  params[:previous] = true if previous
429
462
  params[:container] = container if container
430
463
  params[:timestamps] = timestamps if timestamps
431
464
  params[:sinceTime] = format_datetime(since_time) if since_time
432
465
  params[:tailLines] = tail_lines if tail_lines
466
+ params[:limitBytes] = limit_bytes if limit_bytes
433
467
 
434
468
  ns = build_namespace_prefix(namespace)
435
469
  handle_exception do
@@ -438,7 +472,7 @@ module Kubeclient
438
472
  end
439
473
  end
440
474
 
441
- def watch_pod_log(pod_name, namespace, container: nil)
475
+ def watch_pod_log(pod_name, namespace, container: nil, &block)
442
476
  # Adding the "follow=true" query param tells the Kubernetes API to keep
443
477
  # the connection open and stream updates to the log.
444
478
  params = { follow: true }
@@ -450,7 +484,10 @@ module Kubeclient
450
484
  uri.path += "/#{@api_version}/#{ns}pods/#{pod_name}/log"
451
485
  uri.query = URI.encode_www_form(params)
452
486
 
453
- Kubeclient::Common::WatchStream.new(uri, http_options(uri), formatter: ->(value) { value })
487
+ watcher = Kubeclient::Common::WatchStream.new(
488
+ uri, http_options(uri), formatter: ->(value) { value }
489
+ )
490
+ return_or_yield_to_watcher(watcher, &block)
454
491
  end
455
492
 
456
493
  def proxy_url(kind, name, port, namespace = '')
@@ -587,6 +624,16 @@ module Kubeclient
587
624
  raise ArgumentError, msg unless File.readable?(@auth_options[:bearer_token_file])
588
625
  end
589
626
 
627
+ def return_or_yield_to_watcher(watcher, &block)
628
+ return watcher unless block_given?
629
+
630
+ begin
631
+ watcher.each(&block)
632
+ ensure
633
+ watcher.finish
634
+ end
635
+ end
636
+
590
637
  def http_options(uri)
591
638
  options = {
592
639
  basic_auth_user: @auth_options[:username],
@@ -41,6 +41,11 @@ module Kubeclient
41
41
  def context(context_name = nil)
42
42
  cluster, user, namespace = fetch_context(context_name || @kcfg['current-context'])
43
43
 
44
+ if user.key?('exec')
45
+ exec_opts = expand_command_option(user['exec'], 'command')
46
+ user['exec_result'] = ExecCredentials.run(exec_opts)
47
+ end
48
+
44
49
  ca_cert_data = fetch_cluster_ca_data(cluster)
45
50
  client_cert_data = fetch_user_cert_data(user)
46
51
  client_key_data = fetch_user_key_data(user)
@@ -134,6 +139,8 @@ module Kubeclient
134
139
  File.read(ext_file_path(user['client-certificate']))
135
140
  elsif user.key?('client-certificate-data')
136
141
  Base64.decode64(user['client-certificate-data'])
142
+ elsif user.key?('exec_result') && user['exec_result'].key?('clientCertificateData')
143
+ user['exec_result']['clientCertificateData']
137
144
  end
138
145
  end
139
146
 
@@ -142,6 +149,8 @@ module Kubeclient
142
149
  File.read(ext_file_path(user['client-key']))
143
150
  elsif user.key?('client-key-data')
144
151
  Base64.decode64(user['client-key-data'])
152
+ elsif user.key?('exec_result') && user['exec_result'].key?('clientKeyData')
153
+ user['exec_result']['clientKeyData']
145
154
  end
146
155
  end
147
156
 
@@ -149,9 +158,8 @@ module Kubeclient
149
158
  options = {}
150
159
  if user.key?('token')
151
160
  options[:bearer_token] = user['token']
152
- elsif user.key?('exec')
153
- exec_opts = expand_command_option(user['exec'], 'command')
154
- options[:bearer_token] = Kubeclient::ExecCredentials.token(exec_opts)
161
+ elsif user.key?('exec_result') && user['exec_result'].key?('token')
162
+ options[:bearer_token] = user['exec_result']['token']
155
163
  elsif user.key?('auth-provider')
156
164
  options[:bearer_token] = fetch_token_from_provider(user['auth-provider'])
157
165
  else
@@ -6,7 +6,7 @@ module Kubeclient
6
6
  # Inspired by https://github.com/kubernetes/client-go/blob/master/plugin/pkg/client/auth/exec/exec.go
7
7
  class ExecCredentials
8
8
  class << self
9
- def token(opts)
9
+ def run(opts)
10
10
  require 'open3'
11
11
  require 'json'
12
12
 
@@ -25,7 +25,7 @@ module Kubeclient
25
25
 
26
26
  creds = JSON.parse(out)
27
27
  validate_credentials(opts, creds)
28
- creds['status']['token']
28
+ creds['status']
29
29
  end
30
30
 
31
31
  private
@@ -34,6 +34,36 @@ module Kubeclient
34
34
  raise KeyError, 'exec command is required' unless opts['command']
35
35
  end
36
36
 
37
+ def validate_client_credentials_status(status)
38
+ has_client_cert_data = status.key?('clientCertificateData')
39
+ has_client_key_data = status.key?('clientKeyData')
40
+
41
+ if has_client_cert_data && !has_client_key_data
42
+ raise 'exec plugin didn\'t return client key data'
43
+ end
44
+
45
+ if !has_client_cert_data && has_client_key_data
46
+ raise 'exec plugin didn\'t return client certificate data'
47
+ end
48
+
49
+ has_client_cert_data && has_client_key_data
50
+ end
51
+
52
+ def validate_credentials_status(status)
53
+ raise 'exec plugin didn\'t return a status field' if status.nil?
54
+
55
+ has_client_credentials = validate_client_credentials_status(status)
56
+ has_token = status.key?('token')
57
+
58
+ if has_client_credentials && has_token
59
+ raise 'exec plugin returned both token and client data'
60
+ end
61
+
62
+ return if has_client_credentials || has_token
63
+
64
+ raise 'exec plugin didn\'t return a token or client data' unless has_token
65
+ end
66
+
37
67
  def validate_credentials(opts, creds)
38
68
  # out should have ExecCredential structure
39
69
  raise 'invalid credentials' if creds.nil?
@@ -45,8 +75,7 @@ module Kubeclient
45
75
  "plugin returned version #{creds['apiVersion']}"
46
76
  end
47
77
 
48
- raise 'exec plugin didn\'t return a status field' if creds['status'].nil?
49
- raise 'exec plugin didn\'t return a token' if creds['status']['token'].nil?
78
+ validate_credentials_status(creds['status'])
50
79
  end
51
80
 
52
81
  # Transform name/value pairs to hash
@@ -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
@@ -1,4 +1,4 @@
1
1
  # Kubernetes REST-API Client
2
2
  module Kubeclient
3
- VERSION = '4.5.0'.freeze
3
+ VERSION = '4.9.1'.freeze
4
4
  end
@@ -0,0 +1,160 @@
1
+ require_relative 'test_helper'
2
+
3
+ # URLHandling tests
4
+ class TestCommonUrlHandling < MiniTest::Test
5
+ def test_no_path_in_uri
6
+ client = Kubeclient::Client.new('http://localhost:8080', 'v1')
7
+ rest_client = client.rest_client
8
+ assert_equal('v1', client.instance_variable_get(:@api_version))
9
+ assert_equal('', client.instance_variable_get(:@api_group))
10
+ assert_equal('http://localhost:8080/api/v1', rest_client.url.to_s)
11
+ end
12
+
13
+ def test_with_api_path_in_uri
14
+ client = Kubeclient::Client.new('http://localhost:8080/api', 'v1')
15
+ rest_client = client.rest_client
16
+ assert_equal('v1', client.instance_variable_get(:@api_version))
17
+ assert_equal('', client.instance_variable_get(:@api_group))
18
+ assert_equal('http://localhost:8080/api/v1', rest_client.url.to_s)
19
+ end
20
+
21
+ def test_with_api_path_in_uri_other_version
22
+ client = Kubeclient::Client.new('http://localhost:8080/api', 'v2')
23
+ rest_client = client.rest_client
24
+ assert_equal('v2', client.instance_variable_get(:@api_version))
25
+ assert_equal('', client.instance_variable_get(:@api_group))
26
+ assert_equal('http://localhost:8080/api/v2', rest_client.url.to_s)
27
+ end
28
+
29
+ def test_with_api_group_path_in_uri
30
+ client = Kubeclient::Client.new('http://localhost:8080/apis/this_is_the_group', 'v1')
31
+ rest_client = client.rest_client
32
+ assert_equal('v1', client.instance_variable_get(:@api_version))
33
+ assert_equal('this_is_the_group/', client.instance_variable_get(:@api_group))
34
+ assert_equal('http://localhost:8080/apis/this_is_the_group/v1', rest_client.url.to_s)
35
+ end
36
+
37
+ def test_with_api_group_path_in_uri_other_version
38
+ client = Kubeclient::Client.new('http://localhost:8080/apis/this_is_the_group', 'v2')
39
+ rest_client = client.rest_client
40
+ assert_equal('v2', client.instance_variable_get(:@api_version))
41
+ assert_equal('this_is_the_group/', client.instance_variable_get(:@api_group))
42
+ assert_equal('http://localhost:8080/apis/this_is_the_group/v2', rest_client.url.to_s)
43
+ end
44
+
45
+ def test_with_api_path_in_uri_trailing_slash
46
+ client = Kubeclient::Client.new('http://localhost:8080/api/', 'v1')
47
+ rest_client = client.rest_client
48
+ assert_equal('v1', client.instance_variable_get(:@api_version))
49
+ assert_equal('', client.instance_variable_get(:@api_group))
50
+ assert_equal('http://localhost:8080/api/v1', rest_client.url.to_s)
51
+ end
52
+
53
+ def test_with_api_path_in_api
54
+ client = Kubeclient::Client.new('http://localhost:8080/api/but/I/want/a/hidden/k8s/api', 'v1')
55
+ rest_client = client.rest_client
56
+ assert_equal('v1', client.instance_variable_get(:@api_version))
57
+ assert_equal('', client.instance_variable_get(:@api_group))
58
+ assert_equal('http://localhost:8080/api/but/I/want/a/hidden/k8s/api/v1', rest_client.url.to_s)
59
+ end
60
+
61
+ def test_with_api_group_path_in_api
62
+ client = Kubeclient::Client.new(
63
+ 'http://localhost:8080/api/but/I/want/a/hidden/k8s/apis/this_is_the_group',
64
+ 'v1'
65
+ )
66
+ rest_client = client.rest_client
67
+ assert_equal('v1', client.instance_variable_get(:@api_version))
68
+ assert_equal('this_is_the_group/', client.instance_variable_get(:@api_group))
69
+ assert_equal(
70
+ 'http://localhost:8080/api/but/I/want/a/hidden/k8s/apis/this_is_the_group/v1',
71
+ rest_client.url.to_s
72
+ )
73
+ end
74
+
75
+ def test_rancher_with_api_path_in_uri
76
+ client = Kubeclient::Client.new('http://localhost:8080/k8s/clusters/c-somerancherID/api', 'v1')
77
+ rest_client = client.rest_client
78
+ assert_equal('v1', client.instance_variable_get(:@api_version))
79
+ assert_equal('', client.instance_variable_get(:@api_group))
80
+ assert_equal('http://localhost:8080/k8s/clusters/c-somerancherID/api/v1', rest_client.url.to_s)
81
+ end
82
+
83
+ def test_rancher_no_api_path_in_uri
84
+ client = Kubeclient::Client.new('http://localhost:8080/k8s/clusters/c-somerancherID', 'v1')
85
+ rest_client = client.rest_client
86
+ assert_equal('v1', client.instance_variable_get(:@api_version))
87
+ assert_equal('', client.instance_variable_get(:@api_group))
88
+ assert_equal('http://localhost:8080/k8s/clusters/c-somerancherID/api/v1', rest_client.url.to_s)
89
+ end
90
+
91
+ def test_rancher_no_api_path_in_uri_trailing_slash
92
+ client = Kubeclient::Client.new('http://localhost:8080/k8s/clusters/c-somerancherID/', 'v1')
93
+ rest_client = client.rest_client
94
+ assert_equal('v1', client.instance_variable_get(:@api_version))
95
+ assert_equal('', client.instance_variable_get(:@api_group))
96
+ assert_equal('http://localhost:8080/k8s/clusters/c-somerancherID/api/v1', rest_client.url.to_s)
97
+ end
98
+
99
+ def test_rancher_with_api_path_in_uri_trailing_slash
100
+ client = Kubeclient::Client.new('http://localhost:8080/k8s/clusters/c-somerancherID/api/', 'v1')
101
+ rest_client = client.rest_client
102
+ assert_equal('v1', client.instance_variable_get(:@api_version))
103
+ assert_equal('', client.instance_variable_get(:@api_group))
104
+ assert_equal('http://localhost:8080/k8s/clusters/c-somerancherID/api/v1', rest_client.url.to_s)
105
+ end
106
+
107
+ def test_rancher_with_api_group_in_uri_trailing_slash
108
+ client = Kubeclient::Client.new(
109
+ 'http://localhost:8080/k8s/clusters/c-somerancherID/apis/this_is_the_group',
110
+ 'v1'
111
+ )
112
+ rest_client = client.rest_client
113
+ assert_equal('v1', client.instance_variable_get(:@api_version))
114
+ assert_equal('this_is_the_group/', client.instance_variable_get(:@api_group))
115
+ assert_equal(
116
+ 'http://localhost:8080/k8s/clusters/c-somerancherID/apis/this_is_the_group/v1',
117
+ rest_client.url.to_s
118
+ )
119
+ end
120
+
121
+ def test_with_openshift_api_path_in_uri
122
+ client = Kubeclient::Client.new('http://localhost:8080/oapi', 'v1')
123
+ rest_client = client.rest_client
124
+ assert_equal('v1', client.instance_variable_get(:@api_version))
125
+ assert_equal('', client.instance_variable_get(:@api_group))
126
+ assert_equal('http://localhost:8080/oapi/v1', rest_client.url.to_s)
127
+ end
128
+
129
+ def test_arbitrary_path_with_openshift_api_path_in_uri
130
+ client = Kubeclient::Client.new('http://localhost:8080/foobarbaz/oapi', 'v1')
131
+ rest_client = client.rest_client
132
+ assert_equal('v1', client.instance_variable_get(:@api_version))
133
+ assert_equal('', client.instance_variable_get(:@api_group))
134
+ assert_equal('http://localhost:8080/foobarbaz/oapi/v1', rest_client.url.to_s)
135
+ end
136
+
137
+ def test_with_openshift_api_path_in_uri_trailing_slash
138
+ client = Kubeclient::Client.new('http://localhost:8080/oapi/', 'v1')
139
+ rest_client = client.rest_client
140
+ assert_equal('v1', client.instance_variable_get(:@api_version))
141
+ assert_equal('', client.instance_variable_get(:@api_group))
142
+ assert_equal('http://localhost:8080/oapi/v1', rest_client.url.to_s)
143
+ end
144
+
145
+ def test_with_arbitrary_path_in_uri
146
+ client = Kubeclient::Client.new('http://localhost:8080/foobarbaz', 'v1')
147
+ rest_client = client.rest_client
148
+ assert_equal('v1', client.instance_variable_get(:@api_version))
149
+ assert_equal('', client.instance_variable_get(:@api_group))
150
+ assert_equal('http://localhost:8080/foobarbaz/api/v1', rest_client.url.to_s)
151
+ end
152
+
153
+ def test_with_arbitrary_and_api_path_in_uri
154
+ client = Kubeclient::Client.new('http://localhost:8080/foobarbaz/api', 'v1')
155
+ rest_client = client.rest_client
156
+ assert_equal('v1', client.instance_variable_get(:@api_version))
157
+ assert_equal('', client.instance_variable_get(:@api_group))
158
+ assert_equal('http://localhost:8080/foobarbaz/api/v1', rest_client.url.to_s)
159
+ end
160
+ end
@@ -1,13 +1,13 @@
1
1
  require_relative 'test_helper'
2
2
  require 'open3'
3
3
 
4
- # Unit tests for the ExecCredentials token provider
4
+ # Unit tests for the ExecCredentials provider
5
5
  class ExecCredentialsTest < MiniTest::Test
6
6
  def test_exec_opts_missing
7
7
  expected_msg =
8
8
  'exec options are required'
9
9
  exception = assert_raises(ArgumentError) do
10
- Kubeclient::ExecCredentials.token(nil)
10
+ Kubeclient::ExecCredentials.run(nil)
11
11
  end
12
12
  assert_equal(expected_msg, exception.message)
13
13
  end
@@ -16,7 +16,7 @@ class ExecCredentialsTest < MiniTest::Test
16
16
  expected_msg =
17
17
  'exec command is required'
18
18
  exception = assert_raises(KeyError) do
19
- Kubeclient::ExecCredentials.token({})
19
+ Kubeclient::ExecCredentials.run({})
20
20
  end
21
21
  assert_equal(expected_msg, exception.message)
22
22
  end
@@ -33,34 +33,134 @@ class ExecCredentialsTest < MiniTest::Test
33
33
 
34
34
  Open3.stub(:capture3, [nil, err, st]) do
35
35
  exception = assert_raises(RuntimeError) do
36
- Kubeclient::ExecCredentials.token(opts)
36
+ Kubeclient::ExecCredentials.run(opts)
37
37
  end
38
38
  assert_equal(expected_msg, exception.message)
39
39
  end
40
40
  end
41
41
 
42
- def test_token
42
+ def test_run_with_token_credentials
43
43
  opts = { 'command' => 'dummy' }
44
44
 
45
+ credentials = {
46
+ 'token' => '0123456789ABCDEF0123456789ABCDEF'
47
+ }
48
+
49
+ creds = JSON.dump(
50
+ 'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
51
+ 'status' => credentials
52
+ )
53
+
54
+ st = Minitest::Mock.new
55
+ st.expect(:success?, true)
56
+
57
+ Open3.stub(:capture3, [creds, nil, st]) do
58
+ assert_equal(credentials, Kubeclient::ExecCredentials.run(opts))
59
+ end
60
+ end
61
+
62
+ def test_run_with_client_credentials
63
+ opts = { 'command' => 'dummy' }
64
+
65
+ credentials = {
66
+ 'clientCertificateData' => '0123456789ABCDEF0123456789ABCDEF',
67
+ 'clientKeyData' => '0123456789ABCDEF0123456789ABCDEF'
68
+ }
69
+
70
+ creds = JSON.dump(
71
+ 'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
72
+ 'status' => credentials
73
+ )
74
+
75
+ st = Minitest::Mock.new
76
+ st.expect(:success?, true)
77
+
78
+ Open3.stub(:capture3, [creds, nil, st]) do
79
+ assert_equal(credentials, Kubeclient::ExecCredentials.run(opts))
80
+ end
81
+ end
82
+
83
+ def test_run_with_missing_client_certificate_data
84
+ opts = { 'command' => 'dummy' }
85
+
86
+ credentials = {
87
+ 'clientKeyData' => '0123456789ABCDEF0123456789ABCDEF'
88
+ }
89
+
90
+ creds = JSON.dump(
91
+ 'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
92
+ 'status' => credentials
93
+ )
94
+
95
+ st = Minitest::Mock.new
96
+ st.expect(:success?, true)
97
+
98
+ expected_msg = 'exec plugin didn\'t return client certificate data'
99
+
100
+ Open3.stub(:capture3, [creds, nil, st]) do
101
+ exception = assert_raises(RuntimeError) do
102
+ Kubeclient::ExecCredentials.run(opts)
103
+ end
104
+ assert_equal(expected_msg, exception.message)
105
+ end
106
+ end
107
+
108
+ def test_run_with_missing_client_key_data
109
+ opts = { 'command' => 'dummy' }
110
+
111
+ credentials = {
112
+ 'clientCertificateData' => '0123456789ABCDEF0123456789ABCDEF'
113
+ }
114
+
45
115
  creds = JSON.dump(
46
- 'apiVersion': 'client.authentication.k8s.io/v1alpha1',
47
- 'status': {
48
- 'token': '0123456789ABCDEF0123456789ABCDEF'
49
- }
116
+ 'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
117
+ 'status' => credentials
50
118
  )
51
119
 
52
120
  st = Minitest::Mock.new
53
121
  st.expect(:success?, true)
54
122
 
123
+ expected_msg = 'exec plugin didn\'t return client key data'
124
+
55
125
  Open3.stub(:capture3, [creds, nil, st]) do
56
- assert_equal('0123456789ABCDEF0123456789ABCDEF', Kubeclient::ExecCredentials.token(opts))
126
+ exception = assert_raises(RuntimeError) do
127
+ Kubeclient::ExecCredentials.run(opts)
128
+ end
129
+ assert_equal(expected_msg, exception.message)
130
+ end
131
+ end
132
+
133
+ def test_run_with_client_data_and_token
134
+ opts = { 'command' => 'dummy' }
135
+
136
+ credentials = {
137
+ 'clientCertificateData' => '0123456789ABCDEF0123456789ABCDEF',
138
+ 'clientKeyData' => '0123456789ABCDEF0123456789ABCDEF',
139
+ 'token' => '0123456789ABCDEF0123456789ABCDEF'
140
+ }
141
+
142
+ creds = JSON.dump(
143
+ 'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
144
+ 'status' => credentials
145
+ )
146
+
147
+ st = Minitest::Mock.new
148
+ st.expect(:success?, true)
149
+
150
+ expected_msg = 'exec plugin returned both token and client data'
151
+
152
+ Open3.stub(:capture3, [creds, nil, st]) do
153
+ exception = assert_raises(RuntimeError) do
154
+ Kubeclient::ExecCredentials.run(opts)
155
+ end
156
+ assert_equal(expected_msg, exception.message)
57
157
  end
58
158
  end
59
159
 
60
160
  def test_status_missing
61
161
  opts = { 'command' => 'dummy' }
62
162
 
63
- creds = JSON.dump('apiVersion': 'client.authentication.k8s.io/v1alpha1')
163
+ creds = JSON.dump('apiVersion' => 'client.authentication.k8s.io/v1alpha1')
64
164
 
65
165
  st = Minitest::Mock.new
66
166
  st.expect(:success?, true)
@@ -69,28 +169,28 @@ class ExecCredentialsTest < MiniTest::Test
69
169
 
70
170
  Open3.stub(:capture3, [creds, nil, st]) do
71
171
  exception = assert_raises(RuntimeError) do
72
- Kubeclient::ExecCredentials.token(opts)
172
+ Kubeclient::ExecCredentials.run(opts)
73
173
  end
74
174
  assert_equal(expected_msg, exception.message)
75
175
  end
76
176
  end
77
177
 
78
- def test_token_missing
178
+ def test_credentials_missing
79
179
  opts = { 'command' => 'dummy' }
80
180
 
81
181
  creds = JSON.dump(
82
- 'apiVersion': 'client.authentication.k8s.io/v1alpha1',
83
- 'status': {}
182
+ 'apiVersion' => 'client.authentication.k8s.io/v1alpha1',
183
+ 'status' => {}
84
184
  )
85
185
 
86
186
  st = Minitest::Mock.new
87
187
  st.expect(:success?, true)
88
188
 
89
- expected_msg = 'exec plugin didn\'t return a token'
189
+ expected_msg = 'exec plugin didn\'t return a token or client data'
90
190
 
91
191
  Open3.stub(:capture3, [creds, nil, st]) do
92
192
  exception = assert_raises(RuntimeError) do
93
- Kubeclient::ExecCredentials.token(opts)
193
+ Kubeclient::ExecCredentials.run(opts)
94
194
  end
95
195
  assert_equal(expected_msg, exception.message)
96
196
  end
@@ -106,7 +206,7 @@ class ExecCredentialsTest < MiniTest::Test
106
206
  }
107
207
 
108
208
  creds = JSON.dump(
109
- 'apiVersion': api_version
209
+ 'apiVersion' => api_version
110
210
  )
111
211
 
112
212
  st = Minitest::Mock.new
@@ -117,7 +217,7 @@ class ExecCredentialsTest < MiniTest::Test
117
217
 
118
218
  Open3.stub(:capture3, [creds, nil, st]) do
119
219
  exception = assert_raises(RuntimeError) do
120
- Kubeclient::ExecCredentials.token(opts)
220
+ Kubeclient::ExecCredentials.run(opts)
121
221
  end
122
222
  assert_equal(expected_msg, exception.message)
123
223
  end
@@ -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,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kubeclient
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.5.0
4
+ version: 4.9.1
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-09-27 00:00:00.000000000 Z
11
+ date: 2020-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -157,7 +157,7 @@ dependencies:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
159
  version: '1.0'
160
- type: :development
160
+ type: :runtime
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
@@ -184,20 +184,20 @@ dependencies:
184
184
  requirements:
185
185
  - - "~>"
186
186
  - !ruby/object:Gem::Version
187
- version: '1.0'
187
+ version: '1.1'
188
188
  - - ">="
189
189
  - !ruby/object:Gem::Version
190
- version: 1.0.4
190
+ version: 1.1.1
191
191
  type: :runtime
192
192
  prerelease: false
193
193
  version_requirements: !ruby/object:Gem::Requirement
194
194
  requirements:
195
195
  - - "~>"
196
196
  - !ruby/object:Gem::Version
197
- version: '1.0'
197
+ version: '1.1'
198
198
  - - ">="
199
199
  - !ruby/object:Gem::Version
200
- version: 1.0.4
200
+ version: 1.1.1
201
201
  - !ruby/object:Gem::Dependency
202
202
  name: http
203
203
  requirement: !ruby/object:Gem::Requirement
@@ -324,6 +324,7 @@ files:
324
324
  - test/json/versions_list.json
325
325
  - test/json/watch_stream.json
326
326
  - test/test_common.rb
327
+ - test/test_common_url_handling.rb
327
328
  - test/test_component_status.rb
328
329
  - test/test_config.rb
329
330
  - test/test_endpoint.rb
@@ -372,8 +373,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
372
373
  - !ruby/object:Gem::Version
373
374
  version: '0'
374
375
  requirements: []
375
- rubyforge_project:
376
- rubygems_version: 2.7.6.2
376
+ rubygems_version: 3.1.2
377
377
  signing_key:
378
378
  specification_version: 4
379
379
  summary: A client for Kubernetes REST api
@@ -451,6 +451,7 @@ test_files:
451
451
  - test/json/versions_list.json
452
452
  - test/json/watch_stream.json
453
453
  - test/test_common.rb
454
+ - test/test_common_url_handling.rb
454
455
  - test/test_component_status.rb
455
456
  - test/test_config.rb
456
457
  - test/test_endpoint.rb