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 +4 -4
- data/.travis.yml +22 -6
- data/CHANGELOG.md +46 -2
- data/README.md +208 -37
- data/RELEASING.md +4 -1
- data/kubeclient.gemspec +4 -3
- data/lib/kubeclient.rb +3 -2
- data/lib/kubeclient/aws_eks_credentials.rb +46 -0
- data/lib/kubeclient/common.rb +49 -13
- data/lib/kubeclient/config.rb +19 -9
- data/lib/kubeclient/gcp_auth_provider.rb +19 -0
- data/lib/kubeclient/gcp_command_credentials.rb +31 -0
- data/lib/kubeclient/google_application_default_credentials.rb +6 -1
- data/lib/kubeclient/oidc_auth_provider.rb +11 -5
- data/lib/kubeclient/version.rb +1 -1
- data/test/config/gcpcmdauth.kubeconfig +26 -0
- data/test/test_config.rb +14 -0
- data/test/test_gcp_command_credentials.rb +27 -0
- data/test/test_kubeclient.rb +16 -0
- data/test/test_oidc_auth_provider.rb +19 -0
- data/test/test_pod_log.rb +37 -3
- data/test/test_service.rb +27 -0
- data/test/test_watch.rb +13 -0
- metadata +38 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48d448677cbf44234e974ccb8abd00002c729b059f8857120f11d7a017c63f96
|
4
|
+
data.tar.gz: 1db9a01ab5819636816562bd60b563db6e1aa20aa45f8e8b1283da35e1c77a37
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3ba57f5db5c09035f1ae3265203f5eb64858b7383c9cb73a9e2186befc4b7c120db3984634626895143c5409c9b943103d93a106274b4d61f57579d2e22ba93
|
7
|
+
data.tar.gz: 2154232e7359a4e3eae7191093338cce2f6daa44abf30f663bbe59631435927519f7e342dd103a485726375bdfde5d556c3a825b3dbd063d23f055e5eb16ad1e
|
data/.travis.yml
CHANGED
@@ -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
|
8
|
-
|
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
|
-
|
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
|
data/CHANGELOG.md
CHANGED
@@ -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
|
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_
|
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
|
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
|
-
##
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
551
|
-
It is possible to receive live update notices watching the relevant entities:
|
712
|
+
For example: `apply_pod`
|
552
713
|
|
553
|
-
|
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
|
-
|
716
|
+
Example:
|
561
717
|
|
562
718
|
```ruby
|
563
|
-
|
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
|
-
|
567
|
-
|
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
|
-
|
571
|
-
watcher.each do |notice|
|
572
|
-
# process notice date
|
573
|
-
end
|
739
|
+
client.all_entities
|
574
740
|
```
|
575
741
|
|
576
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/RELEASING.md
CHANGED
@@ -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
|
-
|
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
|
```
|
data/kubeclient.gemspec
CHANGED
@@ -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', '
|
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.
|
36
|
-
spec.add_dependency 'http', '
|
36
|
+
spec.add_dependency 'recursive-open-struct', '~> 1.1', '>= 1.1.1'
|
37
|
+
spec.add_dependency 'http', '>= 3.0', '< 5.0'
|
37
38
|
end
|
data/lib/kubeclient.rb
CHANGED
@@ -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
|
data/lib/kubeclient/common.rb
CHANGED
@@ -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'
|
41
|
-
'fieldSelector'
|
42
|
-
'
|
43
|
-
'
|
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
|
-
|
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(
|
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],
|
data/lib/kubeclient/config.rb
CHANGED
@@ -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']
|
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
|
-
|
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
|
-
|
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
|
-
|
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 >
|
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
|
data/lib/kubeclient/version.rb
CHANGED
@@ -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
|
data/test/test_config.rb
CHANGED
@@ -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
|
data/test/test_kubeclient.rb
CHANGED
@@ -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
|
data/test/test_pod_log.rb
CHANGED
@@ -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
|
-
|
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:
|
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'
|
data/test/test_service.rb
CHANGED
@@ -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'
|
data/test/test_watch.rb
CHANGED
@@ -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.
|
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:
|
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.
|
187
|
+
version: '1.1'
|
174
188
|
- - ">="
|
175
189
|
- !ruby/object:Gem::Version
|
176
|
-
version: 1.
|
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.
|
197
|
+
version: '1.1'
|
184
198
|
- - ">="
|
185
199
|
- !ruby/object:Gem::Version
|
186
|
-
version: 1.
|
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
|
-
|
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
|