Compare commits

...

98 Commits

Author SHA1 Message Date
Kubernetes Publisher
5e167e7c1e Fix Godeps.json to point to kubernetes-1.13.0-alpha.1 tags 2018-10-16 05:41:41 +00:00
Kubernetes Publisher
cbd9965a0e Merge pull request #69412 from tossmilestone/scheduler-factory-test
Refactor scheduler factory test

Kubernetes-commit: 060218a862ba69488aae7edc0313ccf1b448eab6
2018-10-15 21:40:59 +00:00
Kubernetes Publisher
7571b2924a Merge pull request #67412 from tanshanshan/little815
fix test log info

Kubernetes-commit: b9468c7084573bb87f2a8c3b2b6df41b8f136b04
2018-10-13 05:43:48 +00:00
Kubernetes Publisher
f157671357 Merge pull request #69627 from dims/updating-ghodss-yaml-to-latest-version-2
Updating ghodss/yaml and gopkg.in/yaml.v2 to latest version 2

Kubernetes-commit: 3348f9ae23d6502218c6600bcee8d05e00ce5ee3
2018-10-13 01:09:08 +00:00
Kubernetes Publisher
9b03088ac3 Merge pull request #69308 from p0lyn0mial/dynamic_informer
dynamic informer factory

Kubernetes-commit: effd009dabcd0738f5e2b0112b2d3b943c842c69
2018-10-11 10:50:49 +00:00
Kubernetes Publisher
6c1248a47f Merge pull request #69322 from jpbetz/etcd-client-3.3.9
Update etcd client to 3.3 for 1.13

Kubernetes-commit: a8c7a3fd5e707243af68b10a8a581c2c59248222
2018-10-11 06:53:57 +00:00
Kubernetes Publisher
79cc619401 Merge pull request #69232 from dims/add-missing-step-to-in-cluster-client-configuration-example
Add missing step to in-cluster-client-configuration example

Kubernetes-commit: 3a07fa620c6870bb63d8d877f42f417eb577be55
2018-10-10 20:49:57 +00:00
Davanum Srinivas
a1c4661c03 Updating ghodss/yaml and gopkg.in/yaml.v2 to latest version
Change-Id: I1f1a10b68a2d3e796724c6ac26f0ed3260153588

Kubernetes-commit: 6364af128b0ca50e66501519f333e696a26801d9
2018-10-10 10:14:20 -04:00
Kubernetes Publisher
dcf16a0f3b Merge pull request #46517 from goblain/cli-port-forward-listen
port-forward listen on address

Kubernetes-commit: 830e09cc967236bad8a0e9f8261acb487e2273ef
2018-10-09 21:04:35 +00:00
Kubernetes Publisher
f3fa17a207 sync: update godeps 2018-10-09 08:48:43 +00:00
k8s-ci-robot
7f0192792d Merge pull request #481 from nikhita/travis-go-1.11
travis: bump go version to 1.11.1
2018-10-09 01:06:20 -07:00
Kubernetes Publisher
2ccedc1ac8 Merge pull request #69537 from logicalhan/index-test
refactor index_test to compress the assertions and make consistent with store_test.go

Kubernetes-commit: 552453f6fdae70ab3c120dbc6772d9c602ea9fdf
2018-10-09 03:56:39 +00:00
Kubernetes Publisher
ba3e01d473 Merge pull request #69536 from awly/robust-cert-loading
Allow inverted key/cert order in combined PEM file

Kubernetes-commit: f883fd2ce6c9c41d2db7c752a6f193b5fa928205
2018-10-08 23:55:42 +00:00
Han Kang
9d900fc3f3 refactor index_test to compress the basic expected assertions
Kubernetes-commit: de0043b1d37ee27f1d75c22865667cedc54eec1b
2018-10-08 11:38:19 -07:00
Andrew Lytvynov
40fb7ab6be Allow inverted key/cert order in combined PEM file
certificate.FileStore only handles (cert, key) combined PEM files. This
PR allows (key, cert), which is what "openssl req -out foo.pem -keyout
foo.pem" generates.

Kubernetes-commit: 4b6a6a1cd5c8df83b3c51a03ecab975b82057489
2018-10-08 11:37:36 -07:00
Kubernetes Publisher
672cf70624 Merge pull request #68403 from wgliang/master.deprecate-Parallelize
Replace Parallelize with function ParallelizeUntil and formally depre…

Kubernetes-commit: c00f19bd1518aaebf468f25ff3005c21dacbe561
2018-10-06 18:58:06 +00:00
Nikhita Raghunath
4c5811d72b travis: bump go version to 1.11.1 2018-10-06 13:08:55 +05:30
Kubernetes Publisher
1f8420dc26 Merge pull request #69386 from cblecker/go-1.11
Update to go1.11.1

Kubernetes-commit: 0f17e9ade6e089eadb7c09052a4a030f155a0eb0
2018-10-06 04:33:16 +00:00
Christoph Blecker
7f1c01699a Update gofmt for go1.11
Kubernetes-commit: 97b2992dc191a357e2167eff5035ce26237a4799
2018-10-05 12:59:38 -07:00
k8s-ci-robot
9c81604894 Merge pull request #480 from nikhita/travis-check-branch
travis: run godep restore for non-master branches
2018-10-05 07:33:22 -07:00
Nikhita Raghunath
ae670e1f7c travis: run godep restore for non-master branches 2018-10-05 16:32:14 +05:30
Kubernetes Publisher
3e116d4527 Merge pull request #69329 from mrIncompetent/use-signer-interface
use signer interface for certificate creation

Kubernetes-commit: 0509eea6b53161d16d3944b0e0329cb0125384f9
2018-10-03 08:05:38 +00:00
Kubernetes Publisher
a0aefdd1eb Merge pull request #67356 from yliaog/master
Moved staging/src/k8s.io/client-go/tools/bootstrap to staging/src/k8s…

Kubernetes-commit: c179a9c9df4bfb6421258a5f31da9f595474ada4
2018-10-02 20:35:51 -07:00
He Xiaoxi
da56c29d61 Refactor scheduler factory test
Use `k8s.io/client-go/kubernetes/fake.Clientset` as the fake k8s client.

Signed-off-by: He Xiaoxi <xxhe@alauda.io>

Kubernetes-commit: a96a390d924d33ba7dda13761ff0eb1eb37a02b6
2018-10-02 23:46:17 +08:00
mrIncompetent
30575d7a9f use signer interface for certificate creation
Kubernetes-commit: ef04b17ad8097db22755af05e2889b4d73eaf99f
2018-10-02 14:49:45 +02:00
Kubernetes Publisher
aa75b7764b Merge pull request #69125 from caesarxuchao/fix-dynamic-empty-scheme
Populates the fake dynamic client scheme

Kubernetes-commit: 43ce5fbf41411babfacd1f5dda9e30fb19691f1e
2018-10-02 04:08:21 +00:00
Joe Betz
155d5ec829 Update etcd client to 3.3.9
Kubernetes-commit: 4263c752115c3796ee5715c7de4cbc2e237809d3
2018-10-01 16:53:57 -07:00
Radek Pieczonka
2a9c6c4210 port-forward listen on address
adds an address flag to kubectl port-forward to allow to bind to a different ip then localhost

Kubernetes-commit: 4643c6e95e0a0cf6561554fb3b9a1bc59bcead0c
2018-10-01 08:52:50 +02:00
Davanum Srinivas
61a24777f8 Add missing step to in-cluster-client-configuration example
If the permissions are not setup correctly the example fails.

Change-Id: I167ef68be66f8b56740236ae475c3b7fdcc0dfb5

Kubernetes-commit: d222c310aaa4ce8777094f4a37a920982aff794d
2018-09-28 14:46:01 -04:00
Kubernetes Publisher
78bf9020c4 Merge pull request #69035 from dmathieu/fake-eviction
Set api version and action name in fake evictions

Kubernetes-commit: 2e357e39c81673f916a81a0a4f485ed080043e25
2018-09-28 12:54:08 +00:00
k8s-ci-robot
c49eef9ec8 Merge pull request #466 from nikhita/release-9
Add client-go 9.0.0 changelog
2018-09-27 10:16:00 -07:00
Kubernetes Publisher
e2bc6cda8d Merge pull request #68866 from tanshanshan/error_fix_its
fix syntax error:'its'

Kubernetes-commit: 66b956e93b4d6adb44251fb925b8c41344f929fd
2018-09-27 07:27:37 +00:00
Chao Xu
97dc22a90a Populates the fake dynamic client scheme
Kubernetes-commit: db20e16b87b38ace132c375f9ae414a98b451d7b
2018-09-26 18:32:49 -07:00
Kubernetes Publisher
bb17ab6ef1 Merge pull request #68748 from p0lyn0mial/dynamic_lister_informer
adds dynamic lister

Kubernetes-commit: 23baf579c8cfc7d69fefac17ecd271321b15fc54
2018-09-26 23:34:42 +00:00
Kubernetes Publisher
44e7f0ea19 Merge pull request #66516 from tallclair/redirect
Add verification to apiserver redirect following

Kubernetes-commit: 109b67c291de3b9bda35c35e471b9064de6ff859
2018-09-26 23:33:41 +00:00
Kubernetes Publisher
4d3c94690f Merge pull request #68729 from TinySong/fix-client-go-typos
remove duplicated words in heap.go and interfaces.go

Kubernetes-commit: a641e1cba8c587aace2fd8bf012db3a15dd7a536
2018-09-26 07:35:54 +00:00
Kubernetes Publisher
a5d40177db Merge pull request #68245 from jingyih/remove_tagName_in_goDoc
*: Remove comment tags in GoDoc

Kubernetes-commit: a67689dfcab0ed547e1d060c414eae7c81629cc9
2018-09-25 15:32:50 +00:00
Kubernetes Publisher
087039522d Merge pull request #68238 from justinsb/update_reflect2_to_101
Update reflect2 to 1.0.1 (memory utilization fix)

Kubernetes-commit: a94ea824eb59e92188f166c302d7995ba9002667
2018-09-25 15:31:48 +00:00
Kubernetes Publisher
e39833ec3c Merge pull request #68215 from tanshanshan/fixtypo
fix error info

Kubernetes-commit: fe4ca268d76288d21de89de14388b1d4e2f20bbc
2018-09-25 15:31:00 +00:00
Damien Mathieu
b8d8ea0ef0 set api version and action name in fake evictions
Kubernetes-commit: 4e8a188dad443ccd5e9453361b785d98370c0b9a
2018-09-25 15:16:36 +02:00
Kubernetes Publisher
33124a8148 Merge pull request #67513 from novas0x2a/expose-port
export a method to expose which ports were forwarded

Kubernetes-commit: 4a627e9008a48fff8925131b97e372b2b15f6380
2018-09-25 11:34:40 +00:00
Nikhita Raghunath
c5518cda52 Update README to reflect 9.0.0 release 2018-09-22 13:23:30 +05:30
Nikhita Raghunath
3655b8b061 Add client-go 9.0.0 changelog 2018-09-22 13:23:22 +05:30
tanshanshan
0fa5515631 fix syntax error:'its'
Kubernetes-commit: 7dfe0dae64aa2f6cd579f9c88bdf84cccad94504
2018-09-20 09:45:46 +08:00
Kubernetes Publisher
93821fae80 Merge pull request #68790 from nikhita/client-go-install.md
Move INSTALL.md from k8s.io/client-go to staging

Kubernetes-commit: 259a59d93618f0266151a50018f3b3f3cbcfc0e4
2018-09-19 06:22:14 -07:00
k8s-ci-robot
ca74aedde1 Merge pull request #468 from nikhita/remove-install.md
Remove INSTALL.md
2018-09-19 05:04:59 -07:00
k8s-ci-robot
7f726ce02d Merge pull request #467 from nikhita/bump-go-version-travis
Bump go version to 1.10.2 for travis
2018-09-19 04:34:29 -07:00
Nikhita Raghunath
91ab78faba Remove INSTALL.md
This is going to move to staging.
2018-09-19 15:58:43 +05:30
Nikhita Raghunath
0152131f08 Bump go version to 1.10.2 for travis 2018-09-19 15:40:27 +05:30
Nikhita Raghunath
c8e76297e1 Move INSTALL.md from k8s.io/client-go to staging
Kubernetes-commit: 331ae66491a775c24887413f44549d83b797b61e
2018-09-18 20:55:22 +05:30
Kubernetes Publisher
780aa459ed Merge pull request #68530 from wenjiaswe/68115
Rewrite finalURLTemplate used only for metrics because of dynamic client change

Kubernetes-commit: 817d420d6807d0b3bd0a4baa20d709ab755ad602
2018-09-17 19:39:44 +00:00
p0lyn0mial
bfcfd82283 adds dynamic informer factory
Kubernetes-commit: 4d479aff37edfcdb17ac5a5f89b1e3f95d118f30
2018-09-17 19:07:00 +02:00
p0lyn0mial
cf575eb079 adds dynamic lister
Kubernetes-commit: 8b43a0005a5bd3c2e415d55efb6c9d77c1c655b1
2018-09-17 19:07:00 +02:00
song
bd418b4186 remove duplicated words in heap.go and interfaces.go
Kubernetes-commit: dd4a8f9cab80aaf280ed5ed95a6facc26c00d928
2018-09-17 18:00:19 +08:00
Wenjia Zhang
13b096589c Rewrite finalURLTemplate used only for metrics because of dynamic client change
Kubernetes-commit: 99248b8fe1fe1c28188657d811dc7baf8cd12982
2018-09-14 11:31:21 -07:00
Kubernetes Publisher
86dcffcdde Merge pull request #66314 from jlowdermilk/cmdtokensource-reset
gcp client auth plugin: persist default cache on unauthorized

Kubernetes-commit: c04fe8c27c9053a37face46abfebc45b9ac23dd7
2018-09-14 08:06:50 +00:00
Guoliang Wang
b5eba4c261 Replace Parallelize with function ParallelizeUntil and formally deprecate the Parallelize
Kubernetes-commit: c2622dd9d800280bcaeea9473340a4efb94946d2
2018-09-07 17:17:27 +08:00
Jingyi Hu
33fabb1e0f *: Remove comment tags in GoDoc
Adding blank line between comment tag and package name in doc.go. So
that the comment tags such as '+k8s:deepcopy-gen=package' do not show up
in GoDoc.

Kubernetes-commit: 61117761cd4a1b2e6ad9ff2d7eb915f3d2739dc6
2018-09-04 14:08:32 -07:00
Kubernetes Publisher
37c3c02ec9 Merge pull request #67555 from wgliang/opt/improve-performance
Automatic merge from submit-queue (batch tested with PRs 67555, 68196). If you want to cherry-pick this change to another branch, please follow the instructions here: https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md.

Not split nodes when searching for nodes but doing it all at once

**What this PR does / why we need it**:
Not split nodes when searching for nodes but doing it all at once.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #

**Special notes for your reviewer**:
@bsalamat

This is a follow up PR of #66733.

https://github.com/kubernetes/kubernetes/pull/66733#discussion_r205932531

**Release note**:

```release-note
Not split nodes when searching for nodes but doing it all at once.
```

Kubernetes-commit: a0b457d0e5ed54646fd01eac877efcea5be3216d
2018-09-04 19:08:50 +00:00
Justin Santa Barbara
3652287890 Update reflect2 to 1.0.1 (memory utilization fix)
Picking up https://github.com/modern-go/reflect2/pull/2 which lazy
initializes a map of all types which we don't use in k8s, saving
memory & initialization time.

Kubernetes-commit: 970e4da4c6636b835175dc79a7492d22dc11ba33
2018-09-04 13:13:00 -04:00
tanshanshan
9a73dd2136 fix error info
Kubernetes-commit: 19c9df95ad7812f3f5bab39211c82316e72f91b4
2018-09-04 17:14:12 +08:00
Kubernetes Publisher
f06dbfd735 Merge pull request #67359 from mikedanese/reloadtoken
Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions here: https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md.

client: periodically reload InClusterConfig token

/sig auth
/sig api-machinery

```release-note
NONE
```

Kubernetes-commit: 7b6647a418c660f2c87f183f706b297f1cb573ca
2018-09-02 07:32:50 +00:00
Kubernetes Publisher
ec724c24d1 Merge pull request #67910 from liztio/cert-renewal
Automatic merge from submit-queue (batch tested with PRs 64283, 67910, 67803, 68100). If you want to cherry-pick this change to another branch, please follow the instructions here: https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md.

Kubeadm Cert Renewal

**What this PR does / why we need it**:

adds explicit support for renewal of certificates via command

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes kubernetes/kubeadm#206

**Special notes for your reviewer**:
The targeted documentation is at kubernetes/website#9712

**Release note**:

```release-note
Adds the commands `kubeadm alpha phases renew <cert-name>`
```

Kubernetes-commit: 17dde46baebe0b67421132af7b99b42d89ea4cd0
2018-09-01 03:41:45 +00:00
liz
22e1ddcc48 Actually renew certificates (using on-disk CAs)
Kubernetes-commit: 76be5ca581818227062f7d0638cb772bb862bdb9
2018-08-29 17:57:02 -04:00
Kubernetes Publisher
87935b98dd Merge pull request #67545 from sttts/sttts-auth-optional-kubeconfig
Automatic merge from submit-queue (batch tested with PRs 66960, 67545). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

delegated authn/z: optionally opt-out of mandatory authn/authz kubeconfig

This adds `RemoteKubeConfigFileOptional` field to the delegated authn/z option structs. If set to true, the authn/z kubeconfig file flags are optional. If no kubeconfig is given, all token requests are considered to be anonymous and no client CA is looked up in the cluster.

Prerequisite for https://github.com/kubernetes/kubernetes/pull/64149 and https://github.com/kubernetes/kubernetes/pull/67069.

Kubernetes-commit: 1b3a2dd0830ca0e02d5b95d2ecc0161d0c93a0c7
2018-08-28 23:27:43 +00:00
Kubernetes Publisher
31ff53b616 Merge pull request #66717 from bboreham/remove-timeoutFromListOptions
Automatic merge from submit-queue (batch tested with PRs 67323, 66717, 67038). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Remove unused function timeoutFromListOptions()

**What this PR does / why we need it**:
It removes a function which is not exported and not called or referenced.

Re-opening #60215 after rebase.

**Which issue(s) this PR fixes**
No issue.

**Special notes for your reviewer**:
I guess it would be useful to know what this was supposed to be doing. I just happened upon it when trying to understand how watch timeouts were configured.

**Release note**:
```release-note
NONE
```

Kubernetes-commit: 1bfbf861e92410dbed2cb8debdc2f841d0fd7719
2018-08-28 03:30:01 +00:00
Kubernetes Publisher
855e8fadf4 Merge pull request #64097 from damemi/hpa-metrics-specificity
Automatic merge from submit-queue (batch tested with PRs 67894, 64097). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

HPA metrics specificity improvements

**What this PR does / why we need it**:
Improves available specificity for HPA metrics by adding metric selector fields for metrics of Pods and Objects.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Implements this KEP: https://github.com/kubernetes/community/pull/2055

**Special notes for your reviewer**:
Need to add/update tests?

**Release note**:

```release-note
Introduces autoscaling/v2beta2 and custom_metrics/v1beta2, which implement metric selectors for Object and Pods metrics, as well as allowing AverageValue targets on Objects, similar to External metrics.
```

/assign @DirectXMan12

Kubernetes-commit: fdb5707194d56cbbd0da11c5be3a2a5653e714c9
2018-08-27 23:25:16 +00:00
Kubernetes Publisher
030d971055 Merge pull request #67809 from Pingan2017/dafaultfitler
Automatic merge from submit-queue (batch tested with PRs 67362, 67256, 67809). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

del unused func DefaultEventFilterFunc

**What this PR does / why we need it**:

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #

**Special notes for your reviewer**:

**Release note**:

```release-note
NONE
```

Kubernetes-commit: da62c170f724277bae45f6efed6085eb34566bc5
2018-08-27 19:24:36 +00:00
Pingan2017
ada81a0d75 del unused func DefaultEventFilterFunc
Kubernetes-commit: 655271f89b665d87f5de6ba292ed551999635612
2018-08-24 17:26:17 +08:00
Kubernetes Publisher
79cb21f5b3 Merge pull request #66971 from tnozicka/informer-watcher
Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

#50102 Task 2: Add UntilWithSync

**What this PR does / why we need it**:
This is a split off from https://github.com/kubernetes/kubernetes/pull/50102 to go in smaller pieces.

Introduces UntilWithSync based on informer.

**Needs https://github.com/kubernetes/kubernetes/pull/66906 first**
/hold

**Release note**:
```release-note
NONE
```

/priority important-soon
/kind bug
(bug after the main PR which is this split from)

Kubernetes-commit: c4f355a2ad9692f5459541d4e4d94fcbc5f7d946
2018-08-23 15:28:47 +00:00
Dr. Stefan Schimanski
6007d7f0a9 authn/z: optionally opt-out of mandatory authn/authz kubeconfig
Kubernetes-commit: a671d65673590f0dfcf5c2b673e1518d11510bdb
2018-08-22 11:56:07 +02:00
Kubernetes Publisher
3a92319114 Merge pull request #67596 from nikhita/add-apimachinery-label-owners
Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Add sig/api-machinery label to apimachinery OWNERS files

Inspired by https://github.com/kubernetes/kubernetes/pull/67548. List of OWNERS files taken from https://github.com/kubernetes/community/blob/master/sig-api-machinery/README.md#subprojects.

**Release note**:

```release-note
NONE
```

Kubernetes-commit: 8f4ab6fe7635983443ebef7fde5b9c8861bef5bb
2018-08-20 23:26:47 +00:00
Nikhita Raghunath
8dd267ed24 Add sig/api-machinery label to apimachinery OWNERS files
Kubernetes-commit: 6e47ba1fded3dc9932bd62affb673d321089760f
2018-08-20 18:46:47 +05:30
Kubernetes Publisher
b53d9ecf53 Merge pull request #66023 from ibrasho/change-TRUNCATED-to-DATA+OMITTED
Automatic merge from submit-queue (batch tested with PRs 60790, 66023, 67549). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

change TRUNCATED to DATA+OMITTED in kubectl config view

**What this PR does / why we need it**:
Based on the discussion in #61573, this PR switches the replacement text for CA certificate data and client certificates and secrets printed using `kubectl config view`. Currently, `REDACTED` is used, which might give a false impression that the data is a secret (which is not true for the public certificates).

This PR changes `REDACTED` to `DATA+OMITTED`. The printed string is the base64 encoded string on the byte stream. Some trickery is involved to print a readable string (refer to [this comment](https://github.com/kubernetes/kubernetes/pull/66023/files#diff-aec000ca3f293c94dcd99b4a9d1c5c3cL86) for more info).

**Which issue(s) this PR fixes**:
Fixes #61573

**Special notes for your reviewer**:

**Release note**:
```release-note
Switched certificate data replacement from "REDACTED" to "DATA+OMITTED"
```

Kubernetes-commit: ae9beb4663329c8ca824638805b70eba0d295977
2018-08-18 05:30:00 +00:00
Kubernetes Publisher
5d107d4ebc Merge pull request #65779 from cblecker/mergo-update
Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

update github.com/imdario/mergo to v0.3.5

**What this PR does / why we need it**:
Updates github.com/imdario/mergo library to v0.3.5. We were pinned because of a functionality change in the dependency, however, a new function was introduced with similar functionality to the old.

There is apparently some Debian packaging issues (https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=878254) because of this. I'm still not clear what those are, unless they are forcing the library to update, as opposed to using our `vendor/`.

That said, this will allow for some of those vendor conflicts to resolve for anyone else who is using client-go, so that's at least worthwhile.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
fixes #27543, fixes https://github.com/kubernetes/client-go/issues/431

**Special notes for your reviewer**:

**Release note**:
```release-note
NONE
```

Kubernetes-commit: 6b4135267911b6c10ed536308d29d2a7c453eef6
2018-08-18 01:30:48 +00:00
Guoliang Wang
35223a0681 Not split nodes when searching for nodes but doing it all at once
Kubernetes-commit: 6c63dcfffebb9a8bcc5e1cee748ad16d7ed7e293
2018-08-18 09:05:58 +08:00
Kubernetes Publisher
d3f03ecc37 Merge pull request #66920 from wking/segments-segment-typo
Automatic merge from submit-queue (batch tested with PRs 66920, 67316, 67363, 67528, 66963). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

client-go/rest: Fix "segments segment" comment typo

The typo landed with `SubResource` in adb75e1f (kubernetes/kubernetes#29147).

```release-note
NONE
```

Kubernetes-commit: 49b295415d398a3a36b5abb7ba071d3b224087eb
2018-08-17 21:32:35 +00:00
Mike Lundy
e1ca545b2c export a method to expose which ports were forwarded
Without this change, the only method to discover what local port was
bound (if port 0 was requested) is to parse it out of the "out" stream,
which isn't the most reliable method.

Kubernetes-commit: a9084923f2bd499c287b675007cb7c6c093d60ad
2018-08-16 13:11:26 -07:00
Kubernetes Publisher
b95fa2e4f3 Merge pull request #67347 from juanvallejo/jvallejo/use-temp-file-config-tests
Automatic merge from submit-queue (batch tested with PRs 67347, 67307, 67358, 67364, 67385). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

use temp file for kubeconfig in test

Followup to https://github.com/kubernetes/kubernetes/pull/67093

Updates client_config_test to use a temporary file for kubeconfig.

**Release note**:
```release-note
NONE
```

cc @deads2k @soltysh

Kubernetes-commit: 46bfa1ab17afaffc584f560ee5c752ecbbad4f61
2018-08-16 13:35:57 +00:00
Kubernetes Publisher
744b11616f Merge pull request #66906 from tnozicka/rename-until
Automatic merge from submit-queue (batch tested with PRs 67071, 66906, 66722, 67276, 67039). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

#50102 Task 1: Move apimachinery/pkg/watch.Until into client-go/tools/watch.UntilWithoutRetry

**What this PR does / why we need it**:
This is a split off from https://github.com/kubernetes/kubernetes/pull/50102 to go in smaller pieces.

Moves `apimachinery/pkg/watch.Until` into `client-go/tools/watch.UntilWithoutRetry` and adds context so it is cancelable.

**Release note**:
```release-note
NONE
```

**Dev release note**:
```dev-release-note
`apimachinery/pkg/watch.Until` has been moved to `client-go/tools/watch.UntilWithoutRetry`.
While switching please consider using the new `client-go/tools/watch.UntilWithSync` or `client-go/tools/watch.Until`.
```

/cc @smarterclayton @kubernetes/sig-api-machinery-pr-reviews
/milestone v1.12
/priority important-soon
/kind bug
(bug after the main PR which is this split from)

Kubernetes-commit: b6f0aed056ab94fef0b6f54e1ca1d66a5fc228b3
2018-08-16 13:34:54 +00:00
tanshanshan
f3c7bb05b0 fix test log info
Kubernetes-commit: b0696b173b2cff3e561a7417b7adf48b93b5f44a
2018-08-15 10:19:03 +08:00
Yu Liao
b35fd0f035 Moved staging/src/k8s.io/client-go/tools/bootstrap to staging/src/k8s.io/cluster-bootstrap
Kubernetes-commit: fc21115c3fb14e3eec4d57c1b813111fb2efd5c3
2018-08-13 17:22:57 -07:00
Mike Danese
eb0f6510ef reload token file for InClusterConfig every 5 minutes
Kubernetes-commit: 287f6a564fb8c264f281056011f4a66f197b18f4
2018-08-13 16:47:17 -07:00
juanvallejo
69308bcf06 use temp file for kubeconfig in test
Kubernetes-commit: 03af5089afb5d31707e7d91e55dfd6b157e6f8e1
2018-08-13 14:29:34 -04:00
Christoph Blecker
43b95360bd add more tests to clientcmd
Kubernetes-commit: e9cf2b0f798f1fac07f52b5d193419e938e3a7cd
2018-08-11 22:16:26 -07:00
Tomas Nozicka
f314ed00c1 Update Bazel
Kubernetes-commit: dc2cfd5d208bfdb105d055471b778f0dd87ba99d
2018-08-03 16:46:26 +02:00
Tomas Nozicka
4528feba43 Add UntilWithSync (informer based)
Kubernetes-commit: 866cc1acab6c1c30a7550b2de7160c8052be884d
2018-08-03 16:45:41 +02:00
Tomas Nozicka
6dac86f710 Deprecate ListWatchUntil, fix it and call places
Kubernetes-commit: e434f3189e81e473aa89e3589ca81545d1341cf8
2018-08-03 16:08:28 +02:00
Tomas Nozicka
82acb64076 Move ListWatchUntil to its kin
Kubernetes-commit: 07b8373ab393547239ed4fe227758a1715480f22
2018-08-03 15:22:42 +02:00
W. Trevor King
045dbe7ac1 client-go/rest: Fix "segments segment" comment typo
The typo landed with SubResource in adb75e1f (generated staging area,
2016-08-06, kubernetes/kubernetes#29147).

Kubernetes-commit: e42aab6430cbc9d14891e9aaada9ec8becf62273
2018-08-02 08:30:44 -07:00
Ibrahim AshShohail
a83d2a2682 change TRUNCATED to DATA+OMITTED in kubectl config view
Signed-off-by: Ibrahim AshShohail <me@ibrasho.com>

Kubernetes-commit: 23996b25c93c0fc576f5d4a547f91e1b7c4b68a3
2018-08-01 03:23:00 +03:00
Tim Allclair
a6d1c60475 Only allow apiserver to follow redriects to the same host
Kubernetes-commit: 4ee9f007cbc88cca5fa3e8576ff951a52a248e3c
2018-07-23 13:16:25 -07:00
Jeff Lowdermilk
b345654764 gcp client auth plugin: persist default cache on unauthorized
The default cache for a cachedTokenSource is not always empty. In the
case of commandTokenSource, it contains calling details for the
external command that is used to generate refresh tokens. Persisting
a completely empty cache will thus break ability for the plugin to
obtain refresh tokens. This changes the roundtripper to persist
the default cache instead of assuming an empty map.

Kubernetes-commit: 73e5e43711043c6a80d41b1be080b3b2d3dfbacb
2018-07-17 14:06:11 -07:00
Christoph Blecker
442fc9f1c4 Improve TestMergoSemantics test
Kubernetes-commit: 194c471c9ccbb1cf57c66ac081828a693e7ce687
2018-07-04 13:52:44 -07:00
Christoph Blecker
44530d33a7 Move from mergo.Merge to mergo.MergeWithOverwrite
Kubernetes-commit: a1f6d24962f2b9e6002bcc721e1b48d1008d6cbf
2018-07-04 12:52:01 -07:00
Christoph Blecker
e88f376842 Update github.com/imdario/mergo to v0.3.5
Kubernetes-commit: 12b2e2c2b53ab987e956673bc778e040af22e304
2018-07-03 11:15:43 -07:00
Mike Dame
4b6317c5d4 Generate files and modifications for autoscaling/v2beta2 and custom_metrics/v1beta2
Kubernetes-commit: 77d7f9cfa2b489b75b47fa1e1de5660139f83034
2018-06-28 14:35:50 -04:00
Bryan Boreham
17d4c50f8a Remove unused function timeoutFromListOptions()
Kubernetes-commit: 2663fee405521b2cd5538583533f5d812bc3d996
2018-02-22 15:15:57 +00:00
118 changed files with 4930 additions and 1393 deletions

View File

@@ -3,6 +3,8 @@ language: go
go_import_path: k8s.io/client-go
go:
- 1.8.1
- 1.11.1
script: go build ./...
script:
- if [ "$TRAVIS_BRANCH" != "master" ]; then godep restore; fi
- go build ./...

View File

@@ -5,6 +5,187 @@ https://github.com/kubernetes/test-infra/issues/5843.
Changes in `k8s.io/api` and `k8s.io/apimachinery` are mentioned here
because `k8s.io/client-go` depends on them.
# v9.0.0
**Breaking Changes:**
* client-go now supports additional non-alpha-numeric characters in UserInfo
"extra" data keys. It should be updated in order to properly support extra
data containing "/" characters or other characters disallowed in HTTP headers.
Old clients sending keys which were `%`-escaped by the user will have their
values unescaped by new API servers.
([#65799](https://github.com/kubernetes/kubernetes/pull/65799))
* `apimachinery/pkg/watch.Until` has been moved to
`client-go/tools/watch.UntilWithoutRetry`. While switching please consider
using the new `client-go/tools/watch.UntilWithSync` or `client-go/tools/watch.Until`.
([#66906](https://github.com/kubernetes/kubernetes/pull/66906))
* [k8s.io/apimachinery] `Unstructured` metadata accessors now respect omitempty semantics
i.e. a field having zero value will now be removed from the unstructured metadata map.
([#67635](https://github.com/kubernetes/kubernetes/pull/67635))
* [k8s.io/apimachinery] The `ObjectConvertor` interface is now changed such that
`ConvertFieldLabel` func takes GroupVersionKind as an argument instead of just
version and kind. ([#65780](https://github.com/kubernetes/kubernetes/pull/65780))
* [k8s.io/apimachinery] componentconfig `ClientConnectionConfiguration` is
moved to `k8s.io/apimachinery/pkg/apis/config`.
([#66058](https://github.com/kubernetes/kubernetes/pull/66058))
* [k8s.io/apimachinery] Renamed ` KubeConfigFile` to `Kubeconfig` in
`ClientConnectionConfiguration`.
([#67149](https://github.com/kubernetes/kubernetes/pull/67149))
* [k8s.io/apimachinery] JSON patch no longer supports `int`.
([#63522](https://github.com/kubernetes/kubernetes/pull/63522))
**New Features:**
* Add ability to cancel leader election.
This also proves useful in integration tests where the whole app is started and
stopped in each test. ([#57932](https://github.com/kubernetes/kubernetes/pull/57932))
* An example showing how to use fake clients in tests is added.
([#65291](https://github.com/kubernetes/kubernetes/pull/65291))
* [k8s.io/apimachinery] Create and Update now support `CreateOptions` and `UpdateOptions`.
([#65105](https://github.com/kubernetes/kubernetes/pull/65105))
**Bug fixes and Improvements:**
* Decrease the amount of time it takes to modify kubeconfig
files with large amounts of contexts.
([#67093](https://github.com/kubernetes/kubernetes/pull/67093))
* The leader election client now renews timeout.
([#65094](https://github.com/kubernetes/kubernetes/pull/65094))
* Switched certificate data replacement from `REDACTED` to `DATA+OMITTED`.
([#66023](https://github.com/kubernetes/kubernetes/pull/66023))
* Fix listing in the fake dynamic client.
([#66078](https://github.com/kubernetes/kubernetes/pull/66078))
* Fix discovery so that plural names are no longer ignored if a singular name is not specified.
([#66249](https://github.com/kubernetes/kubernetes/pull/66249))
* Fix kubelet startup failure when using `ExecPlugin` in kubeconfig.
([#66395](https://github.com/kubernetes/kubernetes/pull/66395))
* Fix panic in the fake `SubjectAccessReview` client when object is nil.
([#66837](https://github.com/kubernetes/kubernetes/pull/66837))
* Periodically reload `InClusterConfig` token.
([#67359](https://github.com/kubernetes/kubernetes/pull/67359))
* [k8s.io/apimachinery] Report parsing error in json serializer.
([#63668](https://github.com/kubernetes/kubernetes/pull/63668))
* [k8s.io/apimachinery] The `metav1.ObjectMeta` accessor does not deepcopy
owner references anymore. In general, the accessor interface does not enforce
deepcopy nor does it forbid it (e.g. for `unstructured.Unstructured`).
([#64915](https://github.com/kubernetes/kubernetes/pull/64915))
* [k8s.io/apimachinery] Utility functions `SetTransportDefaults` and `DialerFor`
once again respect custom Dial functions set on transports.
([#65547](https://github.com/kubernetes/kubernetes/pull/65547))
* [k8s.io/apimachinery] Speed-up conversion function invocation by avoiding
`reflect.Call`. Action required: regenerated conversion with conversion-gen.
([#65771](https://github.com/kubernetes/kubernetes/pull/65771))
* [k8s.io/apimachinery] Establish "406 Not Acceptable" response for
unmarshable protobuf serialization error.
([#67041](https://github.com/kubernetes/kubernetes/pull/67041))
* [k8s.io/apimachinery] Immediately close the other side of the connection by
exiting once one side closes when proxying.
([#67288](https://github.com/kubernetes/kubernetes/pull/67288))
## API changes
**Breaking Changes:**
* Volume dynamic provisioning scheduling has been promoted to beta.
ACTION REQUIRED: The DynamicProvisioningScheduling alpha feature gate has been removed.
The VolumeScheduling beta feature gate is still required for this feature.
([#67432](https://github.com/kubernetes/kubernetes/pull/67432))
* The CSI file system type is no longer defaulted to ext4.
All the production drivers listed under https://kubernetes-csi.github.io/docs/Drivers.html
were inspected and should not be impacted after this change.
If you are using a driver not in that list,
please test the drivers on an updated test cluster first.
([#65499](https://github.com/kubernetes/kubernetes/pull/65499))
**New Features:**
* Support annotations for remote admission webhooks.
([#58679](https://github.com/kubernetes/kubernetes/pull/58679))
* Support both directory and block device for local volume
plugin `FileSystem` `VolumeMode`.
([#63011](https://github.com/kubernetes/kubernetes/pull/63011))
* Introduce `autoscaling/v2beta2` and `custom_metrics/v1beta2`,
which implement metric selectors for Object and Pods metrics,
as well as allowing AverageValue targets on Objects, similar to External metrics.
([#64097](https://github.com/kubernetes/kubernetes/pull/64097))
* Add `Lease` API in the `coordination.k8s.io` API group.
([#64246](https://github.com/kubernetes/kubernetes/pull/64246))
* `ProcMount` added to `SecurityContext` and `AllowedProcMounts` added to `PodSecurityPolicy`
to allow paths in the container's `/proc` to not be masked.
([#64283](https://github.com/kubernetes/kubernetes/pull/64283))
* Add the `AuditAnnotations` field to `ImageReviewStatus` to allow the
`ImageReview` backend to return annotations to be added to the created pod.
([#64597](https://github.com/kubernetes/kubernetes/pull/64597))
* SCTP is now supported as additional protocol (alpha) alongside TCP and UDP in
Pod, Service, Endpoint, and NetworkPolicy.
([#64973](https://github.com/kubernetes/kubernetes/pull/64973))
* The `PodShareProcessNamespace` feature to configure PID namespace sharing
within a pod has been promoted to beta.
([#66507](https://github.com/kubernetes/kubernetes/pull/66507))
* Add `TTLSecondsAfterFinished` to `JobSpec` for cleaning up Jobs after they finish.
([#66840](https://github.com/kubernetes/kubernetes/pull/66840))
* Add `DataSource` and `TypedLocalObjectReference` fields to support
restoring a volume from a volume snapshot data source.
([#67087](https://github.com/kubernetes/kubernetes/pull/67087))
* `RuntimeClass` is a new API resource for defining different classes of runtimes
that may be used to run containers in the cluster.
Pods can select a `RunitmeClass` to use via the `RuntimeClassName` field.
This feature is in alpha, and the `RuntimeClass` feature gate must be enabled
in order to use it. ([#67737](https://github.com/kubernetes/kubernetes/pull/67737))
* To address the possibility dry-run requests overwhelming admission webhooks
that rely on side effects and a reconciliation mechanism, a new field is being
added to `admissionregistration.k8s.io/v1beta1.ValidatingWebhookConfiguration`
and `admissionregistration.k8s.io/v1beta1.MutatingWebhookConfiguration` so that
webhooks can explicitly register as having dry-run support.
If a dry-run request is made on a resource that triggers a non dry-run supporting
webhook, the request will be completely rejected, with "400: Bad Request".
Additionally, a new field is being added to the
`admission.k8s.io/v1beta1.AdmissionReview` API object, exposing to webhooks
whether or not the request being reviewed is a dry-run.
([#66936](https://github.com/kubernetes/kubernetes/pull/66936))
**Bug fixes and Improvements:**
* The `DisruptedPods` field in `PodDisruptionBudgetStatus` is now optional.
([#63757](https://github.com/kubernetes/kubernetes/pull/63757))
* `extensions/v1beta1` Deployment's `ProgressDeadlineSeconds` now defaults to `MaxInt32`.
([#66581](https://github.com/kubernetes/kubernetes/pull/66581))
# v8.0.0
**Breaking Changes:**

182
Godeps/Godeps.json generated
View File

@@ -1,6 +1,6 @@
{
"ImportPath": "k8s.io/client-go",
"GoVersion": "go1.10",
"GoVersion": "go1.11",
"GodepVersion": "v80",
"Packages": [
"./..."
@@ -52,15 +52,15 @@
},
{
"ImportPath": "github.com/ghodss/yaml",
"Rev": "73d445a93680fa1a78ae23a5839bad48f32ba1ee"
"Rev": "c7ce16629ff4cd059ed96ed06419dd3856fd3577"
},
{
"ImportPath": "github.com/gogo/protobuf/proto",
"Rev": "c0656edd0d9eab7c66d1eb0c568f9039345796f7"
"Rev": "342cbe0a04158f6dcb03ca0079991a51a4248c02"
},
{
"ImportPath": "github.com/gogo/protobuf/sortkeys",
"Rev": "c0656edd0d9eab7c66d1eb0c568f9039345796f7"
"Rev": "342cbe0a04158f6dcb03ca0079991a51a4248c02"
},
{
"ImportPath": "github.com/golang/glog",
@@ -156,7 +156,7 @@
},
{
"ImportPath": "github.com/imdario/mergo",
"Rev": "6633656539c1639d9d78127b7d47c622b5d7b6dc"
"Rev": "9316a62528ac99aaecb4e47eadd6dc8aa6533d58"
},
{
"ImportPath": "github.com/json-iterator/go",
@@ -168,7 +168,7 @@
},
{
"ImportPath": "github.com/modern-go/reflect2",
"Rev": "05fbef0ca5da472bbf96c9322b84a53edc03c9fd"
"Rev": "94122c33edd36123c84d5368cfb2b69df93a0ec8"
},
{
"ImportPath": "github.com/peterbourgon/diskv",
@@ -268,335 +268,339 @@
},
{
"ImportPath": "gopkg.in/yaml.v2",
"Rev": "670d4cfef0544295bc27a114dbac37980d83185a"
"Rev": "5420a8b6744d3b0345ab293f6fcba19c978f1183"
},
{
"ImportPath": "k8s.io/api/admissionregistration/v1alpha1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/admissionregistration/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/apps/v1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/apps/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/apps/v1beta2",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/authentication/v1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/authentication/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/authorization/v1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/authorization/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/autoscaling/v1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/autoscaling/v2beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/autoscaling/v2beta2",
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/batch/v1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/batch/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/batch/v2alpha1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/certificates/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/coordination/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/core/v1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/events/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/extensions/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/imagepolicy/v1alpha1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/networking/v1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/policy/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/rbac/v1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/rbac/v1alpha1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/rbac/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/scheduling/v1alpha1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/scheduling/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/settings/v1alpha1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/storage/v1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/storage/v1alpha1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/api/storage/v1beta1",
"Rev": "91bfdbcf0c2cab32ec1236cee4c300793abea68a"
"Rev": "c7463263e3f1f232e11cfe37e5017cb5104786c2"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/apitesting",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/apitesting/fuzzer",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/apitesting/roundtrip",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/equality",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/errors",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/meta",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/resource",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/fuzzer",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/internalversion",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1beta1",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/conversion",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/conversion/queryparams",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/fields",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/labels",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/runtime",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/runtime/schema",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer/json",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer/protobuf",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer/recognizer",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer/streaming",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/runtime/serializer/versioning",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/selection",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/types",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/cache",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/clock",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/diff",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/errors",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/framer",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/httpstream",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/httpstream/spdy",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/intstr",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/json",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/mergepatch",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/naming",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/net",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/remotecommand",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/runtime",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/sets",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/strategicpatch",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/validation",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/validation/field",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/wait",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/yaml",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/version",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/watch",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/third_party/forked/golang/json",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/third_party/forked/golang/netutil",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/apimachinery/third_party/forked/golang/reflect",
"Rev": "720795f37ef04a6d63ba53b0b43233b3f851c149"
"Rev": "60666be32c5de527b69dabe8e4400b4f0aa897de"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",

View File

@@ -63,7 +63,7 @@ To install `client-go` and place its dependencies in your `$GOPATH`:
```sh
go get k8s.io/client-go/...
cd $GOPATH/src/k8s.io/client-go
git checkout v6.0.0
git checkout v9.0.0 # replace v9.0.0 with the required version
# cd 1.5 # only necessary with 1.5 and 1.4 clients.
godep restore ./...
```
@@ -99,7 +99,7 @@ your project:
package: ( your project's import path ) # e.g. github.com/foo/bar
import:
- package: k8s.io/client-go
version: v6.0.0
version: v9.0.0 # replace v9.0.0 with the required version
```
Second, add a Go file that imports `client-go` somewhere in your project,
@@ -132,7 +132,7 @@ requests can override the version manually in `glide.yaml`. For example:
package: ( your project's import path ) # e.g. github.com/foo/bar
import:
- package: k8s.io/client-go
version: v6.0.0
version: v9.0.0 # replace v9.0.0 with the required version
# Use a newer version of go-spew even though client-go wants an old one.
- package: github.com/davecgh/go-spew
version: v1.1.0

2
OWNERS
View File

@@ -13,3 +13,5 @@ reviewers:
- soltysh
- sttts
- yliaog
labels:
- sig/api-machinery

View File

@@ -2,7 +2,7 @@
Go clients for talking to a [kubernetes](http://kubernetes.io/) cluster.
We currently recommend using the v8.0.0 tag. See [INSTALL.md](/INSTALL.md) for
We currently recommend using the v9.0.0 tag. See [INSTALL.md](/INSTALL.md) for
detailed installation instructions. `go get k8s.io/client-go/...` works, but
will build `master`, which doesn't handle the dependencies well.
@@ -91,17 +91,16 @@ We will backport bugfixes--but not new features--into older versions of
#### Compatibility matrix
| | Kubernetes 1.5 | Kubernetes 1.6 | Kubernetes 1.7 | Kubernetes 1.8 | Kubernetes 1.9 | Kubernetes 1.10 | Kubernetes 1.11 |
|---------------------|----------------|----------------|----------------|----------------|----------------|-----------------|-----------------|
| client-go 1.5 | - | - | - | - | - | - | - |
| client-go 2.0 | ✓ | +- | +- | +- | +- | +- | +- |
| client-go 3.0 | +- | ✓ | - | +- | +- | +- | +- |
| client-go 4.0 | +- | +- | ✓ | +- | +- | +- | +- |
| client-go 5.0 | +- | +- | +- | ✓ | +- | +- | +- |
| client-go 6.0 | +- | +- | +- | +- | | +- | +- |
| client-go 7.0 | +- | +- | +- | +- | +- | | +- |
| client-go 8.0 | +- | +- | +- | +- | +- | +- | |
| client-go HEAD | +- | +- | +- | +- | +- | +- | +- |
| | Kubernetes 1.6 | Kubernetes 1.7 | Kubernetes 1.8 | Kubernetes 1.9 | Kubernetes 1.10 | Kubernetes 1.11 | Kubernetes 1.12 |
|---------------------|----------------|----------------|----------------|----------------|-----------------|-----------------|-----------------|
| client-go 3.0 | | - | +- | +- | +- | +- | +- |
| client-go 4.0 | +- | ✓ | +- | +- | +- | +- | +- |
| client-go 5.0 | +- | +- | ✓ | +- | +- | +- | +- |
| client-go 6.0 | +- | +- | +- | ✓ | +- | +- | +- |
| client-go 7.0 | +- | +- | +- | +- | ✓ | +- | +- |
| client-go 8.0 | +- | +- | +- | +- | +- | | +- |
| client-go 9.0 | +- | +- | +- | +- | +- | +- | |
| client-go HEAD | +- | +- | +- | +- | +- | +- | +- |
Key:
@@ -128,9 +127,10 @@ between client-go versions.
| client-go 3.0 | Kubernetes main repo, 1.6 branch | = - |
| client-go 4.0 | Kubernetes main repo, 1.7 branch | = - |
| client-go 5.0 | Kubernetes main repo, 1.8 branch | = - |
| client-go 6.0 | Kubernetes main repo, 1.9 branch | |
| client-go 6.0 | Kubernetes main repo, 1.9 branch | = - |
| client-go 7.0 | Kubernetes main repo, 1.10 branch | ✓ |
| client-go 8.0 | Kubernetes main repo, 1.11 branch | ✓ |
| client-go 9.0 | Kubernetes main repo, 1.12 branch | ✓ |
| client-go HEAD | Kubernetes main repo, master branch | ✓ |
Key:

View File

@@ -0,0 +1,155 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamicinformer
import (
"sync"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamiclister"
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
)
// NewDynamicSharedInformerFactory constructs a new instance of dynamicSharedInformerFactory for all namespaces.
func NewDynamicSharedInformerFactory(client dynamic.Interface, defaultResync time.Duration) DynamicSharedInformerFactory {
return NewFilteredDynamicSharedInformerFactory(client, defaultResync, metav1.NamespaceAll, nil)
}
// NewFilteredDynamicSharedInformerFactory constructs a new instance of dynamicSharedInformerFactory.
// Listers obtained via this factory will be subject to the same filters as specified here.
func NewFilteredDynamicSharedInformerFactory(client dynamic.Interface, defaultResync time.Duration, namespace string, tweakListOptions TweakListOptionsFunc) DynamicSharedInformerFactory {
return &dynamicSharedInformerFactory{
client: client,
defaultResync: defaultResync,
namespace: metav1.NamespaceAll,
informers: map[schema.GroupVersionResource]informers.GenericInformer{},
startedInformers: make(map[schema.GroupVersionResource]bool),
}
}
type dynamicSharedInformerFactory struct {
client dynamic.Interface
defaultResync time.Duration
namespace string
lock sync.Mutex
informers map[schema.GroupVersionResource]informers.GenericInformer
// startedInformers is used for tracking which informers have been started.
// This allows Start() to be called multiple times safely.
startedInformers map[schema.GroupVersionResource]bool
}
var _ DynamicSharedInformerFactory = &dynamicSharedInformerFactory{}
func (f *dynamicSharedInformerFactory) ForResource(gvr schema.GroupVersionResource) informers.GenericInformer {
f.lock.Lock()
defer f.lock.Unlock()
key := gvr
informer, exists := f.informers[key]
if exists {
return informer
}
informer = NewFilteredDynamicInformer(f.client, gvr, f.namespace, f.defaultResync, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, nil)
f.informers[key] = informer
return informer
}
// Start initializes all requested informers.
func (f *dynamicSharedInformerFactory) Start(stopCh <-chan struct{}) {
f.lock.Lock()
defer f.lock.Unlock()
for informerType, informer := range f.informers {
if !f.startedInformers[informerType] {
go informer.Informer().Run(stopCh)
f.startedInformers[informerType] = true
}
}
}
// WaitForCacheSync waits for all started informers' cache were synced.
func (f *dynamicSharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool {
informers := func() map[schema.GroupVersionResource]cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informers := map[schema.GroupVersionResource]cache.SharedIndexInformer{}
for informerType, informer := range f.informers {
if f.startedInformers[informerType] {
informers[informerType] = informer.Informer()
}
}
return informers
}()
res := map[schema.GroupVersionResource]bool{}
for informType, informer := range informers {
res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
}
return res
}
// NewFilteredDynamicInformer constructs a new informer for a dynamic type.
func NewFilteredDynamicInformer(client dynamic.Interface, gvr schema.GroupVersionResource, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions TweakListOptionsFunc) informers.GenericInformer {
return &dynamicInformer{
gvr: gvr,
informer: cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.Resource(gvr).Namespace(namespace).List(options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.Resource(gvr).Namespace(namespace).Watch(options)
},
},
&unstructured.Unstructured{},
resyncPeriod,
indexers,
),
}
}
type dynamicInformer struct {
informer cache.SharedIndexInformer
gvr schema.GroupVersionResource
}
var _ informers.GenericInformer = &dynamicInformer{}
func (d *dynamicInformer) Informer() cache.SharedIndexInformer {
return d.informer
}
func (d *dynamicInformer) Lister() cache.GenericLister {
return dynamiclister.NewRuntimeObjectShim(dynamiclister.New(d.informer.GetIndexer(), d.gvr))
}

View File

@@ -0,0 +1,160 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamicinformer_test
import (
"context"
"testing"
"time"
"k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/diff"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/tools/cache"
)
func TestDynamicSharedInformerFactory(t *testing.T) {
scenarios := []struct {
name string
existingObj *unstructured.Unstructured
gvr schema.GroupVersionResource
ns string
trigger func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeDynamicClient, testObject *unstructured.Unstructured) *unstructured.Unstructured
handler func(rcvCh chan<- *unstructured.Unstructured) *cache.ResourceEventHandlerFuncs
}{
// scenario 1
{
name: "scenario 1: test if adding an object triggers AddFunc",
ns: "ns-foo",
gvr: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeDynamicClient, _ *unstructured.Unstructured) *unstructured.Unstructured {
testObject := newUnstructured("extensions/v1beta1", "Deployment", "ns-foo", "name-foo")
createdObj, err := fakeClient.Resource(gvr).Namespace(ns).Create(testObject, metav1.CreateOptions{})
if err != nil {
t.Error(err)
}
return createdObj
},
handler: func(rcvCh chan<- *unstructured.Unstructured) *cache.ResourceEventHandlerFuncs {
return &cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
rcvCh <- obj.(*unstructured.Unstructured)
},
}
},
},
// scenario 2
{
name: "scenario 2: tests if updating an object triggers UpdateFunc",
ns: "ns-foo",
gvr: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
existingObj: newUnstructured("extensions/v1beta1", "Deployment", "ns-foo", "name-foo"),
trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeDynamicClient, testObject *unstructured.Unstructured) *unstructured.Unstructured {
testObject.Object["spec"] = "updatedName"
updatedObj, err := fakeClient.Resource(gvr).Namespace(ns).Update(testObject, metav1.UpdateOptions{})
if err != nil {
t.Error(err)
}
return updatedObj
},
handler: func(rcvCh chan<- *unstructured.Unstructured) *cache.ResourceEventHandlerFuncs {
return &cache.ResourceEventHandlerFuncs{
UpdateFunc: func(old, updated interface{}) {
rcvCh <- updated.(*unstructured.Unstructured)
},
}
},
},
// scenario 3
{
name: "scenario 3: test if deleting an object triggers DeleteFunc",
ns: "ns-foo",
gvr: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
existingObj: newUnstructured("extensions/v1beta1", "Deployment", "ns-foo", "name-foo"),
trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeDynamicClient, testObject *unstructured.Unstructured) *unstructured.Unstructured {
err := fakeClient.Resource(gvr).Namespace(ns).Delete(testObject.GetName(), &metav1.DeleteOptions{})
if err != nil {
t.Error(err)
}
return testObject
},
handler: func(rcvCh chan<- *unstructured.Unstructured) *cache.ResourceEventHandlerFuncs {
return &cache.ResourceEventHandlerFuncs{
DeleteFunc: func(obj interface{}) {
rcvCh <- obj.(*unstructured.Unstructured)
},
}
},
},
}
for _, ts := range scenarios {
t.Run(ts.name, func(t *testing.T) {
// test data
timeout := time.Duration(3 * time.Second)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
scheme := runtime.NewScheme()
informerReciveObjectCh := make(chan *unstructured.Unstructured, 1)
objs := []runtime.Object{}
if ts.existingObj != nil {
objs = append(objs, ts.existingObj)
}
fakeClient := fake.NewSimpleDynamicClient(scheme, objs...)
target := dynamicinformer.NewDynamicSharedInformerFactory(fakeClient, 0)
// act
informerListerForGvr := target.ForResource(ts.gvr)
informerListerForGvr.Informer().AddEventHandler(ts.handler(informerReciveObjectCh))
target.Start(ctx.Done())
if synced := target.WaitForCacheSync(ctx.Done()); !synced[ts.gvr] {
t.Errorf("informer for %s hasn't synced", ts.gvr)
}
testObject := ts.trigger(ts.gvr, ts.ns, fakeClient, ts.existingObj)
select {
case objFromInformer := <-informerReciveObjectCh:
if !equality.Semantic.DeepEqual(testObject, objFromInformer) {
t.Fatalf("%v", diff.ObjectDiff(testObject, objFromInformer))
}
case <-ctx.Done():
t.Errorf("tested informer haven't received an object, waited %v", timeout)
}
})
}
}
func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": apiVersion,
"kind": kind,
"metadata": map[string]interface{}{
"namespace": namespace,
"name": name,
},
"spec": name,
},
}
}

View File

@@ -0,0 +1,34 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamicinformer
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/informers"
)
// DynamicSharedInformerFactory provides access to a shared informer and lister for dynamic client
type DynamicSharedInformerFactory interface {
Start(stopCh <-chan struct{})
ForResource(gvr schema.GroupVersionResource) informers.GenericInformer
WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool
}
// TweakListOptionsFunc defines the signature of a helper function
// that wants to provide more listing options to API
type TweakListOptionsFunc func(*metav1.ListOptions)

View File

@@ -0,0 +1,40 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiclister
import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
)
// Lister helps list resources.
type Lister interface {
// List lists all resources in the indexer.
List(selector labels.Selector) (ret []*unstructured.Unstructured, err error)
// Get retrieves a resource from the indexer with the given name
Get(name string) (*unstructured.Unstructured, error)
// Namespace returns an object that can list and get resources in a given namespace.
Namespace(namespace string) NamespaceLister
}
// NamespaceLister helps list and get resources.
type NamespaceLister interface {
// List lists all resources in the indexer for a given namespace.
List(selector labels.Selector) (ret []*unstructured.Unstructured, err error)
// Get retrieves a resource from the indexer for a given namespace and name.
Get(name string) (*unstructured.Unstructured, error)
}

View File

@@ -0,0 +1,91 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiclister
import (
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/tools/cache"
)
var _ Lister = &dynamicLister{}
var _ NamespaceLister = &dynamicNamespaceLister{}
// dynamicLister implements the Lister interface.
type dynamicLister struct {
indexer cache.Indexer
gvr schema.GroupVersionResource
}
// New returns a new Lister.
func New(indexer cache.Indexer, gvr schema.GroupVersionResource) Lister {
return &dynamicLister{indexer: indexer, gvr: gvr}
}
// List lists all resources in the indexer.
func (l *dynamicLister) List(selector labels.Selector) (ret []*unstructured.Unstructured, err error) {
err = cache.ListAll(l.indexer, selector, func(m interface{}) {
ret = append(ret, m.(*unstructured.Unstructured))
})
return ret, err
}
// Get retrieves a resource from the indexer with the given name
func (l *dynamicLister) Get(name string) (*unstructured.Unstructured, error) {
obj, exists, err := l.indexer.GetByKey(name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(l.gvr.GroupResource(), name)
}
return obj.(*unstructured.Unstructured), nil
}
// Namespace returns an object that can list and get resources from a given namespace.
func (l *dynamicLister) Namespace(namespace string) NamespaceLister {
return &dynamicNamespaceLister{indexer: l.indexer, namespace: namespace, gvr: l.gvr}
}
// dynamicNamespaceLister implements the NamespaceLister interface.
type dynamicNamespaceLister struct {
indexer cache.Indexer
namespace string
gvr schema.GroupVersionResource
}
// List lists all resources in the indexer for a given namespace.
func (l *dynamicNamespaceLister) List(selector labels.Selector) (ret []*unstructured.Unstructured, err error) {
err = cache.ListAllByNamespace(l.indexer, l.namespace, selector, func(m interface{}) {
ret = append(ret, m.(*unstructured.Unstructured))
})
return ret, err
}
// Get retrieves a resource from the indexer for a given namespace and name.
func (l *dynamicNamespaceLister) Get(name string) (*unstructured.Unstructured, error) {
obj, exists, err := l.indexer.GetByKey(l.namespace + "/" + name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(l.gvr.GroupResource(), name)
}
return obj.(*unstructured.Unstructured), nil
}

View File

@@ -0,0 +1,257 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiclister_test
import (
"reflect"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/diff"
"k8s.io/client-go/dynamic/dynamiclister"
"k8s.io/client-go/tools/cache"
)
func TestNamespaceGetMethod(t *testing.T) {
tests := []struct {
name string
existingObjects []runtime.Object
namespaceToSync string
gvrToSync schema.GroupVersionResource
objectToGet string
expectedObject *unstructured.Unstructured
expectError bool
}{
{
name: "scenario 1: gets name-foo1 resource from the indexer from ns-foo namespace",
existingObjects: []runtime.Object{
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1"),
newUnstructured("group/version", "TheKind", "ns-bar", "name-bar"),
},
namespaceToSync: "ns-foo",
gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"},
objectToGet: "name-foo1",
expectedObject: newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1"),
},
{
name: "scenario 2: gets name-foo-non-existing resource from the indexer from ns-foo namespace",
existingObjects: []runtime.Object{
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1"),
newUnstructured("group/version", "TheKind", "ns-bar", "name-bar"),
},
namespaceToSync: "ns-foo",
gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"},
objectToGet: "name-foo-non-existing",
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// test data
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
for _, obj := range test.existingObjects {
err := indexer.Add(obj)
if err != nil {
t.Fatal(err)
}
}
// act
target := dynamiclister.New(indexer, test.gvrToSync).Namespace(test.namespaceToSync)
actualObject, err := target.Get(test.objectToGet)
// validate
if test.expectError {
if err == nil {
t.Fatal("expected to get an error but non was returned")
}
return
}
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(test.expectedObject, actualObject) {
t.Fatalf("unexpected object has been returned expected = %v actual = %v, diff = %v", test.expectedObject, actualObject, diff.ObjectDiff(test.expectedObject, actualObject))
}
})
}
}
func TestNamespaceListMethod(t *testing.T) {
// test data
objs := []runtime.Object{
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1"),
newUnstructured("group/version", "TheKind", "ns-bar", "name-bar"),
}
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
for _, obj := range objs {
err := indexer.Add(obj)
if err != nil {
t.Fatal(err)
}
}
expectedOutput := []*unstructured.Unstructured{
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1"),
}
namespaceToList := "ns-foo"
// act
target := dynamiclister.New(indexer, schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"}).Namespace(namespaceToList)
actualOutput, err := target.List(labels.Everything())
// validate
if err != nil {
t.Fatal(err)
}
assertListOrDie(expectedOutput, actualOutput, t)
}
func TestListerGetMethod(t *testing.T) {
tests := []struct {
name string
existingObjects []runtime.Object
namespaceToSync string
gvrToSync schema.GroupVersionResource
objectToGet string
expectedObject *unstructured.Unstructured
expectError bool
}{
{
name: "scenario 1: gets name-foo1 resource from the indexer",
existingObjects: []runtime.Object{
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
newUnstructured("group/version", "TheKind", "", "name-foo1"),
newUnstructured("group/version", "TheKind", "ns-bar", "name-bar"),
},
namespaceToSync: "",
gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"},
objectToGet: "name-foo1",
expectedObject: newUnstructured("group/version", "TheKind", "", "name-foo1"),
},
{
name: "scenario 2: doesn't get name-foo resource from the indexer from ns-foo namespace",
existingObjects: []runtime.Object{
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo1"),
newUnstructured("group/version", "TheKind", "ns-bar", "name-bar"),
},
namespaceToSync: "ns-foo",
gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"},
objectToGet: "name-foo",
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// test data
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
for _, obj := range test.existingObjects {
err := indexer.Add(obj)
if err != nil {
t.Fatal(err)
}
}
// act
target := dynamiclister.New(indexer, test.gvrToSync)
actualObject, err := target.Get(test.objectToGet)
// validate
if test.expectError {
if err == nil {
t.Fatal("expected to get an error but non was returned")
}
return
}
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(test.expectedObject, actualObject) {
t.Fatalf("unexpected object has been returned expected = %v actual = %v, diff = %v", test.expectedObject, actualObject, diff.ObjectDiff(test.expectedObject, actualObject))
}
})
}
}
func TestListerListMethod(t *testing.T) {
// test data
objs := []runtime.Object{
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
}
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
for _, obj := range objs {
err := indexer.Add(obj)
if err != nil {
t.Fatal(err)
}
}
expectedOutput := []*unstructured.Unstructured{
newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
}
// act
target := dynamiclister.New(indexer, schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"})
actualOutput, err := target.List(labels.Everything())
// validate
if err != nil {
t.Fatal(err)
}
assertListOrDie(expectedOutput, actualOutput, t)
}
func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": apiVersion,
"kind": kind,
"metadata": map[string]interface{}{
"namespace": namespace,
"name": name,
},
},
}
}
func assertListOrDie(expected, actual []*unstructured.Unstructured, t *testing.T) {
if len(actual) != len(expected) {
t.Fatalf("unexpected number of items returned, expected = %d, actual = %d", len(expected), len(actual))
}
for _, expectedObject := range expected {
found := false
for _, actualObject := range actual {
if actualObject.GetName() == expectedObject.GetName() {
if !reflect.DeepEqual(expectedObject, actualObject) {
t.Fatalf("unexpected object has been returned expected = %v actual = %v, diff = %v", expectedObject, actualObject, diff.ObjectDiff(expectedObject, actualObject))
}
found = true
}
}
if !found {
t.Fatalf("the resource with the name = %s was not found in the returned output", expectedObject.GetName())
}
}
}

View File

@@ -0,0 +1,87 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamiclister
import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
)
var _ cache.GenericLister = &dynamicListerShim{}
var _ cache.GenericNamespaceLister = &dynamicNamespaceListerShim{}
// dynamicListerShim implements the cache.GenericLister interface.
type dynamicListerShim struct {
lister Lister
}
// NewRuntimeObjectShim returns a new shim for Lister.
// It wraps Lister so that it implements cache.GenericLister interface
func NewRuntimeObjectShim(lister Lister) cache.GenericLister {
return &dynamicListerShim{lister: lister}
}
// List will return all objects across namespaces
func (s *dynamicListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) {
objs, err := s.lister.List(selector)
if err != nil {
return nil, err
}
ret = make([]runtime.Object, len(objs))
for index, obj := range objs {
ret[index] = obj
}
return ret, err
}
// Get will attempt to retrieve assuming that name==key
func (s *dynamicListerShim) Get(name string) (runtime.Object, error) {
return s.lister.Get(name)
}
func (s *dynamicListerShim) ByNamespace(namespace string) cache.GenericNamespaceLister {
return &dynamicNamespaceListerShim{
namespaceLister: s.lister.Namespace(namespace),
}
}
// dynamicNamespaceListerShim implements the NamespaceLister interface.
// It wraps NamespaceLister so that it implements cache.GenericNamespaceLister interface
type dynamicNamespaceListerShim struct {
namespaceLister NamespaceLister
}
// List will return all objects in this namespace
func (ns *dynamicNamespaceListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) {
objs, err := ns.namespaceLister.List(selector)
if err != nil {
return nil, err
}
ret = make([]runtime.Object, len(objs))
for index, obj := range objs {
ret[index] = obj
}
return ret, err
}
// Get will attempt to retrieve by namespace and name
func (ns *dynamicNamespaceListerShim) Get(name string) (runtime.Object, error) {
return ns.namespaceLister.Get(name)
}

View File

@@ -45,7 +45,7 @@ func NewSimpleDynamicClient(scheme *runtime.Scheme, objects ...runtime.Object) *
}
}
cs := &FakeDynamicClient{}
cs := &FakeDynamicClient{scheme: scheme}
cs.AddReactor("*", "*", testing.ObjectReaction(o))
cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
gvr := action.GetResource()

View File

@@ -25,7 +25,13 @@ build the image on Minikube:
docker build -t in-cluster .
If you are not using Minikube, you should build this image and push it to a registry
that your Kubernetes cluster can pull from.
that your Kubernetes cluster can pull from. If you have RBAC enabled, use the following
snippet to create role binding which will grant the default service account view
permissions.
```
kubectl create clusterrolebinding default-view --clusterrole=view --serviceaccount=default:default
```
Then, run the image in a Pod with a single instance Deployment:

View File

@@ -21,6 +21,7 @@ package autoscaling
import (
v1 "k8s.io/client-go/informers/autoscaling/v1"
v2beta1 "k8s.io/client-go/informers/autoscaling/v2beta1"
v2beta2 "k8s.io/client-go/informers/autoscaling/v2beta2"
internalinterfaces "k8s.io/client-go/informers/internalinterfaces"
)
@@ -30,6 +31,8 @@ type Interface interface {
V1() v1.Interface
// V2beta1 provides access to shared informers for resources in V2beta1.
V2beta1() v2beta1.Interface
// V2beta2 provides access to shared informers for resources in V2beta2.
V2beta2() v2beta2.Interface
}
type group struct {
@@ -52,3 +55,8 @@ func (g *group) V1() v1.Interface {
func (g *group) V2beta1() v2beta1.Interface {
return v2beta1.New(g.factory, g.namespace, g.tweakListOptions)
}
// V2beta2 returns a new v2beta2.Interface.
func (g *group) V2beta2() v2beta2.Interface {
return v2beta2.New(g.factory, g.namespace, g.tweakListOptions)
}

View File

@@ -0,0 +1,89 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by informer-gen. DO NOT EDIT.
package v2beta2
import (
time "time"
autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
watch "k8s.io/apimachinery/pkg/watch"
internalinterfaces "k8s.io/client-go/informers/internalinterfaces"
kubernetes "k8s.io/client-go/kubernetes"
v2beta2 "k8s.io/client-go/listers/autoscaling/v2beta2"
cache "k8s.io/client-go/tools/cache"
)
// HorizontalPodAutoscalerInformer provides access to a shared informer and lister for
// HorizontalPodAutoscalers.
type HorizontalPodAutoscalerInformer interface {
Informer() cache.SharedIndexInformer
Lister() v2beta2.HorizontalPodAutoscalerLister
}
type horizontalPodAutoscalerInformer struct {
factory internalinterfaces.SharedInformerFactory
tweakListOptions internalinterfaces.TweakListOptionsFunc
namespace string
}
// NewHorizontalPodAutoscalerInformer constructs a new informer for HorizontalPodAutoscaler type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewHorizontalPodAutoscalerInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredHorizontalPodAutoscalerInformer(client, namespace, resyncPeriod, indexers, nil)
}
// NewFilteredHorizontalPodAutoscalerInformer constructs a new informer for HorizontalPodAutoscaler type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredHorizontalPodAutoscalerInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.AutoscalingV2beta2().HorizontalPodAutoscalers(namespace).List(options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.AutoscalingV2beta2().HorizontalPodAutoscalers(namespace).Watch(options)
},
},
&autoscalingv2beta2.HorizontalPodAutoscaler{},
resyncPeriod,
indexers,
)
}
func (f *horizontalPodAutoscalerInformer) defaultInformer(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredHorizontalPodAutoscalerInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}
func (f *horizontalPodAutoscalerInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&autoscalingv2beta2.HorizontalPodAutoscaler{}, f.defaultInformer)
}
func (f *horizontalPodAutoscalerInformer) Lister() v2beta2.HorizontalPodAutoscalerLister {
return v2beta2.NewHorizontalPodAutoscalerLister(f.Informer().GetIndexer())
}

View File

@@ -0,0 +1,45 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by informer-gen. DO NOT EDIT.
package v2beta2
import (
internalinterfaces "k8s.io/client-go/informers/internalinterfaces"
)
// Interface provides access to all the informers in this group version.
type Interface interface {
// HorizontalPodAutoscalers returns a HorizontalPodAutoscalerInformer.
HorizontalPodAutoscalers() HorizontalPodAutoscalerInformer
}
type version struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// HorizontalPodAutoscalers returns a HorizontalPodAutoscalerInformer.
func (v *version) HorizontalPodAutoscalers() HorizontalPodAutoscalerInformer {
return &horizontalPodAutoscalerInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
}

View File

@@ -28,6 +28,7 @@ import (
v1beta2 "k8s.io/api/apps/v1beta2"
autoscalingv1 "k8s.io/api/autoscaling/v1"
v2beta1 "k8s.io/api/autoscaling/v2beta1"
v2beta2 "k8s.io/api/autoscaling/v2beta2"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
v2alpha1 "k8s.io/api/batch/v2alpha1"
@@ -127,6 +128,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
case v2beta1.SchemeGroupVersion.WithResource("horizontalpodautoscalers"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Autoscaling().V2beta1().HorizontalPodAutoscalers().Informer()}, nil
// Group=autoscaling, Version=v2beta2
case v2beta2.SchemeGroupVersion.WithResource("horizontalpodautoscalers"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Autoscaling().V2beta2().HorizontalPodAutoscalers().Informer()}, nil
// Group=batch, Version=v1
case batchv1.SchemeGroupVersion.WithResource("jobs"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Batch().V1().Jobs().Informer()}, nil

View File

@@ -31,6 +31,7 @@ import (
authorizationv1beta1 "k8s.io/client-go/kubernetes/typed/authorization/v1beta1"
autoscalingv1 "k8s.io/client-go/kubernetes/typed/autoscaling/v1"
autoscalingv2beta1 "k8s.io/client-go/kubernetes/typed/autoscaling/v2beta1"
autoscalingv2beta2 "k8s.io/client-go/kubernetes/typed/autoscaling/v2beta2"
batchv1 "k8s.io/client-go/kubernetes/typed/batch/v1"
batchv1beta1 "k8s.io/client-go/kubernetes/typed/batch/v1beta1"
batchv2alpha1 "k8s.io/client-go/kubernetes/typed/batch/v2alpha1"
@@ -77,6 +78,7 @@ type Interface interface {
// Deprecated: please explicitly pick a version if possible.
Autoscaling() autoscalingv1.AutoscalingV1Interface
AutoscalingV2beta1() autoscalingv2beta1.AutoscalingV2beta1Interface
AutoscalingV2beta2() autoscalingv2beta2.AutoscalingV2beta2Interface
BatchV1() batchv1.BatchV1Interface
// Deprecated: please explicitly pick a version if possible.
Batch() batchv1.BatchV1Interface
@@ -137,6 +139,7 @@ type Clientset struct {
authorizationV1beta1 *authorizationv1beta1.AuthorizationV1beta1Client
autoscalingV1 *autoscalingv1.AutoscalingV1Client
autoscalingV2beta1 *autoscalingv2beta1.AutoscalingV2beta1Client
autoscalingV2beta2 *autoscalingv2beta2.AutoscalingV2beta2Client
batchV1 *batchv1.BatchV1Client
batchV1beta1 *batchv1beta1.BatchV1beta1Client
batchV2alpha1 *batchv2alpha1.BatchV2alpha1Client
@@ -243,6 +246,11 @@ func (c *Clientset) AutoscalingV2beta1() autoscalingv2beta1.AutoscalingV2beta1In
return c.autoscalingV2beta1
}
// AutoscalingV2beta2 retrieves the AutoscalingV2beta2Client
func (c *Clientset) AutoscalingV2beta2() autoscalingv2beta2.AutoscalingV2beta2Interface {
return c.autoscalingV2beta2
}
// BatchV1 retrieves the BatchV1Client
func (c *Clientset) BatchV1() batchv1.BatchV1Interface {
return c.batchV1
@@ -470,6 +478,10 @@ func NewForConfig(c *rest.Config) (*Clientset, error) {
if err != nil {
return nil, err
}
cs.autoscalingV2beta2, err = autoscalingv2beta2.NewForConfig(&configShallowCopy)
if err != nil {
return nil, err
}
cs.batchV1, err = batchv1.NewForConfig(&configShallowCopy)
if err != nil {
return nil, err
@@ -569,6 +581,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset {
cs.authorizationV1beta1 = authorizationv1beta1.NewForConfigOrDie(c)
cs.autoscalingV1 = autoscalingv1.NewForConfigOrDie(c)
cs.autoscalingV2beta1 = autoscalingv2beta1.NewForConfigOrDie(c)
cs.autoscalingV2beta2 = autoscalingv2beta2.NewForConfigOrDie(c)
cs.batchV1 = batchv1.NewForConfigOrDie(c)
cs.batchV1beta1 = batchv1beta1.NewForConfigOrDie(c)
cs.batchV2alpha1 = batchv2alpha1.NewForConfigOrDie(c)
@@ -607,6 +620,7 @@ func New(c rest.Interface) *Clientset {
cs.authorizationV1beta1 = authorizationv1beta1.New(c)
cs.autoscalingV1 = autoscalingv1.New(c)
cs.autoscalingV2beta1 = autoscalingv2beta1.New(c)
cs.autoscalingV2beta2 = autoscalingv2beta2.New(c)
cs.batchV1 = batchv1.New(c)
cs.batchV1beta1 = batchv1beta1.New(c)
cs.batchV2alpha1 = batchv2alpha1.New(c)

View File

@@ -46,6 +46,8 @@ import (
fakeautoscalingv1 "k8s.io/client-go/kubernetes/typed/autoscaling/v1/fake"
autoscalingv2beta1 "k8s.io/client-go/kubernetes/typed/autoscaling/v2beta1"
fakeautoscalingv2beta1 "k8s.io/client-go/kubernetes/typed/autoscaling/v2beta1/fake"
autoscalingv2beta2 "k8s.io/client-go/kubernetes/typed/autoscaling/v2beta2"
fakeautoscalingv2beta2 "k8s.io/client-go/kubernetes/typed/autoscaling/v2beta2/fake"
batchv1 "k8s.io/client-go/kubernetes/typed/batch/v1"
fakebatchv1 "k8s.io/client-go/kubernetes/typed/batch/v1/fake"
batchv1beta1 "k8s.io/client-go/kubernetes/typed/batch/v1beta1"
@@ -209,6 +211,11 @@ func (c *Clientset) AutoscalingV2beta1() autoscalingv2beta1.AutoscalingV2beta1In
return &fakeautoscalingv2beta1.FakeAutoscalingV2beta1{Fake: &c.Fake}
}
// AutoscalingV2beta2 retrieves the AutoscalingV2beta2Client
func (c *Clientset) AutoscalingV2beta2() autoscalingv2beta2.AutoscalingV2beta2Interface {
return &fakeautoscalingv2beta2.FakeAutoscalingV2beta2{Fake: &c.Fake}
}
// BatchV1 retrieves the BatchV1Client
func (c *Clientset) BatchV1() batchv1.BatchV1Interface {
return &fakebatchv1.FakeBatchV1{Fake: &c.Fake}

View File

@@ -30,6 +30,7 @@ import (
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
autoscalingv1 "k8s.io/api/autoscaling/v1"
autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1"
autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
batchv2alpha1 "k8s.io/api/batch/v2alpha1"
@@ -71,6 +72,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{
authorizationv1beta1.AddToScheme,
autoscalingv1.AddToScheme,
autoscalingv2beta1.AddToScheme,
autoscalingv2beta2.AddToScheme,
batchv1.AddToScheme,
batchv1beta1.AddToScheme,
batchv2alpha1.AddToScheme,

View File

@@ -30,6 +30,7 @@ import (
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
autoscalingv1 "k8s.io/api/autoscaling/v1"
autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1"
autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
batchv2alpha1 "k8s.io/api/batch/v2alpha1"
@@ -71,6 +72,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{
authorizationv1beta1.AddToScheme,
autoscalingv1.AddToScheme,
autoscalingv2beta1.AddToScheme,
autoscalingv2beta2.AddToScheme,
batchv1.AddToScheme,
batchv1beta1.AddToScheme,
batchv2alpha1.AddToScheme,

View File

@@ -0,0 +1,90 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package v2beta2
import (
v2beta2 "k8s.io/api/autoscaling/v2beta2"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes/scheme"
rest "k8s.io/client-go/rest"
)
type AutoscalingV2beta2Interface interface {
RESTClient() rest.Interface
HorizontalPodAutoscalersGetter
}
// AutoscalingV2beta2Client is used to interact with features provided by the autoscaling group.
type AutoscalingV2beta2Client struct {
restClient rest.Interface
}
func (c *AutoscalingV2beta2Client) HorizontalPodAutoscalers(namespace string) HorizontalPodAutoscalerInterface {
return newHorizontalPodAutoscalers(c, namespace)
}
// NewForConfig creates a new AutoscalingV2beta2Client for the given config.
func NewForConfig(c *rest.Config) (*AutoscalingV2beta2Client, error) {
config := *c
if err := setConfigDefaults(&config); err != nil {
return nil, err
}
client, err := rest.RESTClientFor(&config)
if err != nil {
return nil, err
}
return &AutoscalingV2beta2Client{client}, nil
}
// NewForConfigOrDie creates a new AutoscalingV2beta2Client for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *AutoscalingV2beta2Client {
client, err := NewForConfig(c)
if err != nil {
panic(err)
}
return client
}
// New creates a new AutoscalingV2beta2Client for the given RESTClient.
func New(c rest.Interface) *AutoscalingV2beta2Client {
return &AutoscalingV2beta2Client{c}
}
func setConfigDefaults(config *rest.Config) error {
gv := v2beta2.SchemeGroupVersion
config.GroupVersion = &gv
config.APIPath = "/apis"
config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs}
if config.UserAgent == "" {
config.UserAgent = rest.DefaultKubernetesUserAgent()
}
return nil
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *AutoscalingV2beta2Client) RESTClient() rest.Interface {
if c == nil {
return nil
}
return c.restClient
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2017 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package api (k8s.io/client-go/tools/bootstrap/token/api) contains constants and types needed for
// bootstrap tokens as maintained by the BootstrapSigner and TokenCleaner
// controllers (in k8s.io/kubernetes/pkg/controller/bootstrap)
package api // import "k8s.io/client-go/tools/bootstrap/token/api"
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated typed clients.
package v2beta2

View File

@@ -0,0 +1,20 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
// Package fake has the automatically generated clients.
package fake

View File

@@ -0,0 +1,40 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v2beta2 "k8s.io/client-go/kubernetes/typed/autoscaling/v2beta2"
rest "k8s.io/client-go/rest"
testing "k8s.io/client-go/testing"
)
type FakeAutoscalingV2beta2 struct {
*testing.Fake
}
func (c *FakeAutoscalingV2beta2) HorizontalPodAutoscalers(namespace string) v2beta2.HorizontalPodAutoscalerInterface {
return &FakeHorizontalPodAutoscalers{c, namespace}
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *FakeAutoscalingV2beta2) RESTClient() rest.Interface {
var ret *rest.RESTClient
return ret
}

View File

@@ -0,0 +1,140 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v2beta2 "k8s.io/api/autoscaling/v2beta2"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
schema "k8s.io/apimachinery/pkg/runtime/schema"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
testing "k8s.io/client-go/testing"
)
// FakeHorizontalPodAutoscalers implements HorizontalPodAutoscalerInterface
type FakeHorizontalPodAutoscalers struct {
Fake *FakeAutoscalingV2beta2
ns string
}
var horizontalpodautoscalersResource = schema.GroupVersionResource{Group: "autoscaling", Version: "v2beta2", Resource: "horizontalpodautoscalers"}
var horizontalpodautoscalersKind = schema.GroupVersionKind{Group: "autoscaling", Version: "v2beta2", Kind: "HorizontalPodAutoscaler"}
// Get takes name of the horizontalPodAutoscaler, and returns the corresponding horizontalPodAutoscaler object, and an error if there is any.
func (c *FakeHorizontalPodAutoscalers) Get(name string, options v1.GetOptions) (result *v2beta2.HorizontalPodAutoscaler, err error) {
obj, err := c.Fake.
Invokes(testing.NewGetAction(horizontalpodautoscalersResource, c.ns, name), &v2beta2.HorizontalPodAutoscaler{})
if obj == nil {
return nil, err
}
return obj.(*v2beta2.HorizontalPodAutoscaler), err
}
// List takes label and field selectors, and returns the list of HorizontalPodAutoscalers that match those selectors.
func (c *FakeHorizontalPodAutoscalers) List(opts v1.ListOptions) (result *v2beta2.HorizontalPodAutoscalerList, err error) {
obj, err := c.Fake.
Invokes(testing.NewListAction(horizontalpodautoscalersResource, horizontalpodautoscalersKind, c.ns, opts), &v2beta2.HorizontalPodAutoscalerList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &v2beta2.HorizontalPodAutoscalerList{ListMeta: obj.(*v2beta2.HorizontalPodAutoscalerList).ListMeta}
for _, item := range obj.(*v2beta2.HorizontalPodAutoscalerList).Items {
if label.Matches(labels.Set(item.Labels)) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Watch returns a watch.Interface that watches the requested horizontalPodAutoscalers.
func (c *FakeHorizontalPodAutoscalers) Watch(opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewWatchAction(horizontalpodautoscalersResource, c.ns, opts))
}
// Create takes the representation of a horizontalPodAutoscaler and creates it. Returns the server's representation of the horizontalPodAutoscaler, and an error, if there is any.
func (c *FakeHorizontalPodAutoscalers) Create(horizontalPodAutoscaler *v2beta2.HorizontalPodAutoscaler) (result *v2beta2.HorizontalPodAutoscaler, err error) {
obj, err := c.Fake.
Invokes(testing.NewCreateAction(horizontalpodautoscalersResource, c.ns, horizontalPodAutoscaler), &v2beta2.HorizontalPodAutoscaler{})
if obj == nil {
return nil, err
}
return obj.(*v2beta2.HorizontalPodAutoscaler), err
}
// Update takes the representation of a horizontalPodAutoscaler and updates it. Returns the server's representation of the horizontalPodAutoscaler, and an error, if there is any.
func (c *FakeHorizontalPodAutoscalers) Update(horizontalPodAutoscaler *v2beta2.HorizontalPodAutoscaler) (result *v2beta2.HorizontalPodAutoscaler, err error) {
obj, err := c.Fake.
Invokes(testing.NewUpdateAction(horizontalpodautoscalersResource, c.ns, horizontalPodAutoscaler), &v2beta2.HorizontalPodAutoscaler{})
if obj == nil {
return nil, err
}
return obj.(*v2beta2.HorizontalPodAutoscaler), err
}
// UpdateStatus was generated because the type contains a Status member.
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
func (c *FakeHorizontalPodAutoscalers) UpdateStatus(horizontalPodAutoscaler *v2beta2.HorizontalPodAutoscaler) (*v2beta2.HorizontalPodAutoscaler, error) {
obj, err := c.Fake.
Invokes(testing.NewUpdateSubresourceAction(horizontalpodautoscalersResource, "status", c.ns, horizontalPodAutoscaler), &v2beta2.HorizontalPodAutoscaler{})
if obj == nil {
return nil, err
}
return obj.(*v2beta2.HorizontalPodAutoscaler), err
}
// Delete takes name of the horizontalPodAutoscaler and deletes it. Returns an error if one occurs.
func (c *FakeHorizontalPodAutoscalers) Delete(name string, options *v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewDeleteAction(horizontalpodautoscalersResource, c.ns, name), &v2beta2.HorizontalPodAutoscaler{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakeHorizontalPodAutoscalers) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
action := testing.NewDeleteCollectionAction(horizontalpodautoscalersResource, c.ns, listOptions)
_, err := c.Fake.Invokes(action, &v2beta2.HorizontalPodAutoscalerList{})
return err
}
// Patch applies the patch and returns the patched horizontalPodAutoscaler.
func (c *FakeHorizontalPodAutoscalers) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v2beta2.HorizontalPodAutoscaler, err error) {
obj, err := c.Fake.
Invokes(testing.NewPatchSubresourceAction(horizontalpodautoscalersResource, c.ns, name, data, subresources...), &v2beta2.HorizontalPodAutoscaler{})
if obj == nil {
return nil, err
}
return obj.(*v2beta2.HorizontalPodAutoscaler), err
}

View File

@@ -0,0 +1,21 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package v2beta2
type HorizontalPodAutoscalerExpansion interface{}

View File

@@ -0,0 +1,174 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package v2beta2
import (
v2beta2 "k8s.io/api/autoscaling/v2beta2"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
scheme "k8s.io/client-go/kubernetes/scheme"
rest "k8s.io/client-go/rest"
)
// HorizontalPodAutoscalersGetter has a method to return a HorizontalPodAutoscalerInterface.
// A group's client should implement this interface.
type HorizontalPodAutoscalersGetter interface {
HorizontalPodAutoscalers(namespace string) HorizontalPodAutoscalerInterface
}
// HorizontalPodAutoscalerInterface has methods to work with HorizontalPodAutoscaler resources.
type HorizontalPodAutoscalerInterface interface {
Create(*v2beta2.HorizontalPodAutoscaler) (*v2beta2.HorizontalPodAutoscaler, error)
Update(*v2beta2.HorizontalPodAutoscaler) (*v2beta2.HorizontalPodAutoscaler, error)
UpdateStatus(*v2beta2.HorizontalPodAutoscaler) (*v2beta2.HorizontalPodAutoscaler, error)
Delete(name string, options *v1.DeleteOptions) error
DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
Get(name string, options v1.GetOptions) (*v2beta2.HorizontalPodAutoscaler, error)
List(opts v1.ListOptions) (*v2beta2.HorizontalPodAutoscalerList, error)
Watch(opts v1.ListOptions) (watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v2beta2.HorizontalPodAutoscaler, err error)
HorizontalPodAutoscalerExpansion
}
// horizontalPodAutoscalers implements HorizontalPodAutoscalerInterface
type horizontalPodAutoscalers struct {
client rest.Interface
ns string
}
// newHorizontalPodAutoscalers returns a HorizontalPodAutoscalers
func newHorizontalPodAutoscalers(c *AutoscalingV2beta2Client, namespace string) *horizontalPodAutoscalers {
return &horizontalPodAutoscalers{
client: c.RESTClient(),
ns: namespace,
}
}
// Get takes name of the horizontalPodAutoscaler, and returns the corresponding horizontalPodAutoscaler object, and an error if there is any.
func (c *horizontalPodAutoscalers) Get(name string, options v1.GetOptions) (result *v2beta2.HorizontalPodAutoscaler, err error) {
result = &v2beta2.HorizontalPodAutoscaler{}
err = c.client.Get().
Namespace(c.ns).
Resource("horizontalpodautoscalers").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do().
Into(result)
return
}
// List takes label and field selectors, and returns the list of HorizontalPodAutoscalers that match those selectors.
func (c *horizontalPodAutoscalers) List(opts v1.ListOptions) (result *v2beta2.HorizontalPodAutoscalerList, err error) {
result = &v2beta2.HorizontalPodAutoscalerList{}
err = c.client.Get().
Namespace(c.ns).
Resource("horizontalpodautoscalers").
VersionedParams(&opts, scheme.ParameterCodec).
Do().
Into(result)
return
}
// Watch returns a watch.Interface that watches the requested horizontalPodAutoscalers.
func (c *horizontalPodAutoscalers) Watch(opts v1.ListOptions) (watch.Interface, error) {
opts.Watch = true
return c.client.Get().
Namespace(c.ns).
Resource("horizontalpodautoscalers").
VersionedParams(&opts, scheme.ParameterCodec).
Watch()
}
// Create takes the representation of a horizontalPodAutoscaler and creates it. Returns the server's representation of the horizontalPodAutoscaler, and an error, if there is any.
func (c *horizontalPodAutoscalers) Create(horizontalPodAutoscaler *v2beta2.HorizontalPodAutoscaler) (result *v2beta2.HorizontalPodAutoscaler, err error) {
result = &v2beta2.HorizontalPodAutoscaler{}
err = c.client.Post().
Namespace(c.ns).
Resource("horizontalpodautoscalers").
Body(horizontalPodAutoscaler).
Do().
Into(result)
return
}
// Update takes the representation of a horizontalPodAutoscaler and updates it. Returns the server's representation of the horizontalPodAutoscaler, and an error, if there is any.
func (c *horizontalPodAutoscalers) Update(horizontalPodAutoscaler *v2beta2.HorizontalPodAutoscaler) (result *v2beta2.HorizontalPodAutoscaler, err error) {
result = &v2beta2.HorizontalPodAutoscaler{}
err = c.client.Put().
Namespace(c.ns).
Resource("horizontalpodautoscalers").
Name(horizontalPodAutoscaler.Name).
Body(horizontalPodAutoscaler).
Do().
Into(result)
return
}
// UpdateStatus was generated because the type contains a Status member.
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
func (c *horizontalPodAutoscalers) UpdateStatus(horizontalPodAutoscaler *v2beta2.HorizontalPodAutoscaler) (result *v2beta2.HorizontalPodAutoscaler, err error) {
result = &v2beta2.HorizontalPodAutoscaler{}
err = c.client.Put().
Namespace(c.ns).
Resource("horizontalpodautoscalers").
Name(horizontalPodAutoscaler.Name).
SubResource("status").
Body(horizontalPodAutoscaler).
Do().
Into(result)
return
}
// Delete takes name of the horizontalPodAutoscaler and deletes it. Returns an error if one occurs.
func (c *horizontalPodAutoscalers) Delete(name string, options *v1.DeleteOptions) error {
return c.client.Delete().
Namespace(c.ns).
Resource("horizontalpodautoscalers").
Name(name).
Body(options).
Do().
Error()
}
// DeleteCollection deletes a collection of objects.
func (c *horizontalPodAutoscalers) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
return c.client.Delete().
Namespace(c.ns).
Resource("horizontalpodautoscalers").
VersionedParams(&listOptions, scheme.ParameterCodec).
Body(options).
Do().
Error()
}
// Patch applies the patch and returns the patched horizontalPodAutoscaler.
func (c *horizontalPodAutoscalers) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v2beta2.HorizontalPodAutoscaler, err error) {
result = &v2beta2.HorizontalPodAutoscaler{}
err = c.client.Patch(pt).
Namespace(c.ns).
Resource("horizontalpodautoscalers").
SubResource(subresources...).
Name(name).
Body(data).
Do().
Into(result)
return
}

View File

@@ -26,20 +26,31 @@ import (
func (c *FakePods) Bind(binding *v1.Binding) error {
action := core.CreateActionImpl{}
action.Verb = "create"
action.Namespace = binding.Namespace
action.Resource = podsResource
action.Subresource = "bindings"
action.Subresource = "binding"
action.Object = binding
_, err := c.Fake.Invokes(action, binding)
return err
}
func (c *FakePods) GetBinding(name string) (result *v1.Binding, err error) {
obj, err := c.Fake.
Invokes(core.NewGetSubresourceAction(podsResource, c.ns, "binding", name), &v1.Binding{})
if obj == nil {
return nil, err
}
return obj.(*v1.Binding), err
}
func (c *FakePods) GetLogs(name string, opts *v1.PodLogOptions) *restclient.Request {
action := core.GenericActionImpl{}
action.Verb = "get"
action.Namespace = c.ns
action.Resource = podsResource
action.Subresource = "logs"
action.Subresource = "log"
action.Value = opts
_, _ = c.Fake.Invokes(action, &v1.Pod{})

View File

@@ -26,8 +26,9 @@ func (c *FakeEvictions) Evict(eviction *policy.Eviction) error {
action := core.GetActionImpl{}
action.Verb = "post"
action.Namespace = c.ns
action.Resource = schema.GroupVersionResource{Group: "", Version: "", Resource: "pods"}
action.Resource = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
action.Subresource = "eviction"
action.Name = eviction.Name
_, err := c.Fake.Invokes(action, eviction)
return err
}

View File

@@ -0,0 +1,27 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by lister-gen. DO NOT EDIT.
package v2beta2
// HorizontalPodAutoscalerListerExpansion allows custom methods to be added to
// HorizontalPodAutoscalerLister.
type HorizontalPodAutoscalerListerExpansion interface{}
// HorizontalPodAutoscalerNamespaceListerExpansion allows custom methods to be added to
// HorizontalPodAutoscalerNamespaceLister.
type HorizontalPodAutoscalerNamespaceListerExpansion interface{}

View File

@@ -0,0 +1,94 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by lister-gen. DO NOT EDIT.
package v2beta2
import (
v2beta2 "k8s.io/api/autoscaling/v2beta2"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
)
// HorizontalPodAutoscalerLister helps list HorizontalPodAutoscalers.
type HorizontalPodAutoscalerLister interface {
// List lists all HorizontalPodAutoscalers in the indexer.
List(selector labels.Selector) (ret []*v2beta2.HorizontalPodAutoscaler, err error)
// HorizontalPodAutoscalers returns an object that can list and get HorizontalPodAutoscalers.
HorizontalPodAutoscalers(namespace string) HorizontalPodAutoscalerNamespaceLister
HorizontalPodAutoscalerListerExpansion
}
// horizontalPodAutoscalerLister implements the HorizontalPodAutoscalerLister interface.
type horizontalPodAutoscalerLister struct {
indexer cache.Indexer
}
// NewHorizontalPodAutoscalerLister returns a new HorizontalPodAutoscalerLister.
func NewHorizontalPodAutoscalerLister(indexer cache.Indexer) HorizontalPodAutoscalerLister {
return &horizontalPodAutoscalerLister{indexer: indexer}
}
// List lists all HorizontalPodAutoscalers in the indexer.
func (s *horizontalPodAutoscalerLister) List(selector labels.Selector) (ret []*v2beta2.HorizontalPodAutoscaler, err error) {
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
ret = append(ret, m.(*v2beta2.HorizontalPodAutoscaler))
})
return ret, err
}
// HorizontalPodAutoscalers returns an object that can list and get HorizontalPodAutoscalers.
func (s *horizontalPodAutoscalerLister) HorizontalPodAutoscalers(namespace string) HorizontalPodAutoscalerNamespaceLister {
return horizontalPodAutoscalerNamespaceLister{indexer: s.indexer, namespace: namespace}
}
// HorizontalPodAutoscalerNamespaceLister helps list and get HorizontalPodAutoscalers.
type HorizontalPodAutoscalerNamespaceLister interface {
// List lists all HorizontalPodAutoscalers in the indexer for a given namespace.
List(selector labels.Selector) (ret []*v2beta2.HorizontalPodAutoscaler, err error)
// Get retrieves the HorizontalPodAutoscaler from the indexer for a given namespace and name.
Get(name string) (*v2beta2.HorizontalPodAutoscaler, error)
HorizontalPodAutoscalerNamespaceListerExpansion
}
// horizontalPodAutoscalerNamespaceLister implements the HorizontalPodAutoscalerNamespaceLister
// interface.
type horizontalPodAutoscalerNamespaceLister struct {
indexer cache.Indexer
namespace string
}
// List lists all HorizontalPodAutoscalers in the indexer for a given namespace.
func (s horizontalPodAutoscalerNamespaceLister) List(selector labels.Selector) (ret []*v2beta2.HorizontalPodAutoscaler, err error) {
err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
ret = append(ret, m.(*v2beta2.HorizontalPodAutoscaler))
})
return ret, err
}
// Get retrieves the HorizontalPodAutoscaler from the indexer for a given namespace and name.
func (s horizontalPodAutoscalerNamespaceLister) Get(name string) (*v2beta2.HorizontalPodAutoscaler, error) {
obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(v2beta2.Resource("horizontalpodautoscaler"), name)
}
return obj.(*v2beta2.HorizontalPodAutoscaler), nil
}

View File

@@ -16,4 +16,5 @@ limitations under the License.
// +k8s:deepcopy-gen=package
// +groupName=client.authentication.k8s.io
package clientauthentication // import "k8s.io/client-go/pkg/apis/clientauthentication"

View File

@@ -20,4 +20,5 @@ limitations under the License.
// +k8s:defaulter-gen=TypeMeta
// +groupName=client.authentication.k8s.io
package v1alpha1 // import "k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1"

View File

@@ -20,4 +20,5 @@ limitations under the License.
// +k8s:defaulter-gen=TypeMeta
// +groupName=client.authentication.k8s.io
package v1beta1 // import "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"

View File

@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// +k8s:openapi-gen=true
// Package version supplies version information collected at build time to
// kubernetes components.
// +k8s:openapi-gen=true
package version // import "k8s.io/client-go/pkg/version"

View File

@@ -174,7 +174,13 @@ func parseScopes(gcpConfig map[string]string) []string {
}
func (g *gcpAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
return &conditionalTransport{&oauth2.Transport{Source: g.tokenSource, Base: rt}, g.persister}
var resetCache map[string]string
if cts, ok := g.tokenSource.(*cachedTokenSource); ok {
resetCache = cts.baseCache()
} else {
resetCache = make(map[string]string)
}
return &conditionalTransport{&oauth2.Transport{Source: g.tokenSource, Base: rt}, g.persister, resetCache}
}
func (g *gcpAuthProvider) Login() error { return nil }
@@ -247,6 +253,19 @@ func (t *cachedTokenSource) update(tok *oauth2.Token) map[string]string {
return ret
}
// baseCache is the base configuration value for this TokenSource, without any cached ephemeral tokens.
func (t *cachedTokenSource) baseCache() map[string]string {
t.lk.Lock()
defer t.lk.Unlock()
ret := map[string]string{}
for k, v := range t.cache {
ret[k] = v
}
delete(ret, "access-token")
delete(ret, "expiry")
return ret
}
type commandTokenSource struct {
cmd string
args []string
@@ -337,6 +356,7 @@ func parseJSONPath(input interface{}, name, template string) (string, error) {
type conditionalTransport struct {
oauthTransport *oauth2.Transport
persister restclient.AuthProviderConfigPersister
resetCache map[string]string
}
var _ net.RoundTripperWrapper = &conditionalTransport{}
@@ -354,8 +374,7 @@ func (t *conditionalTransport) RoundTrip(req *http.Request) (*http.Response, err
if res.StatusCode == 401 {
glog.V(4).Infof("The credentials that were supplied are invalid for the target cluster")
emptyCache := make(map[string]string)
t.persister.Persist(emptyCache)
t.persister.Persist(t.resetCache)
}
return res, nil

View File

@@ -442,37 +442,61 @@ func (t *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return t.res, nil
}
func TestClearingCredentials(t *testing.T) {
func Test_cmdTokenSource_roundTrip(t *testing.T) {
accessToken := "fakeToken"
fakeExpiry := time.Now().Add(time.Hour)
cache := map[string]string{
"access-token": "fakeToken",
"expiry": fakeExpiry.String(),
fakeExpiryStr := fakeExpiry.Format(time.RFC3339Nano)
fs := &fakeTokenSource{
token: &oauth2.Token{
AccessToken: accessToken,
Expiry: fakeExpiry,
},
}
cts := cachedTokenSource{
source: nil,
accessToken: cache["access-token"],
expiry: fakeExpiry,
persister: nil,
cache: nil,
cmdCache := map[string]string{
"cmd-path": "/path/to/tokensource/cmd",
"cmd-args": "--output=json",
}
cmdCacheUpdated := map[string]string{
"cmd-path": "/path/to/tokensource/cmd",
"cmd-args": "--output=json",
"access-token": accessToken,
"expiry": fakeExpiryStr,
}
simpleCacheUpdated := map[string]string{
"access-token": accessToken,
"expiry": fakeExpiryStr,
}
tests := []struct {
name string
res http.Response
cache map[string]string
name string
res http.Response
baseCache, expectedCache map[string]string
}{
{
"Unauthorized",
http.Response{StatusCode: 401},
make(map[string]string),
make(map[string]string),
},
{
"Unauthorized, nonempty defaultCache",
http.Response{StatusCode: 401},
cmdCache,
cmdCache,
},
{
"Authorized",
http.Response{StatusCode: 200},
cache,
make(map[string]string),
simpleCacheUpdated,
},
{
"Authorized, nonempty defaultCache",
http.Response{StatusCode: 200},
cmdCache,
cmdCacheUpdated,
},
}
@@ -480,17 +504,23 @@ func TestClearingCredentials(t *testing.T) {
req := http.Request{Header: http.Header{}}
for _, tc := range tests {
authProvider := gcpAuthProvider{&cts, persister}
cts, err := newCachedTokenSource(accessToken, fakeExpiry.String(), persister, fs, tc.baseCache)
if err != nil {
t.Fatalf("unexpected error from newCachedTokenSource: %v", err)
}
authProvider := gcpAuthProvider{cts, persister}
fakeTransport := MockTransport{&tc.res}
transport := (authProvider.WrapTransport(&fakeTransport))
persister.Persist(cache)
// call Token to persist/update cache
if _, err := cts.Token(); err != nil {
t.Fatalf("unexpected error from cachedTokenSource.Token(): %v", err)
}
transport.RoundTrip(&req)
if got := persister.read(); !reflect.DeepEqual(got, tc.cache) {
t.Errorf("got cache %v, want %v", got, tc.cache)
if got := persister.read(); !reflect.DeepEqual(got, tc.expectedCache) {
t.Errorf("got cache %v, want %v", got, tc.expectedCache)
}
}

View File

@@ -18,6 +18,7 @@ package rest
import (
"context"
"errors"
"fmt"
"io/ioutil"
"net"
@@ -29,7 +30,6 @@ import (
"time"
"github.com/golang/glog"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -44,6 +44,8 @@ const (
DefaultBurst int = 10
)
var ErrNotInCluster = errors.New("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined")
// Config holds the common attributes that can be passed to a Kubernetes client on
// initialization.
type Config struct {
@@ -308,20 +310,26 @@ func DefaultKubernetesUserAgent() string {
// InClusterConfig returns a config object which uses the service account
// kubernetes gives to pods. It's intended for clients that expect to be
// running inside a pod running on kubernetes. It will return an error if
// called from a process not running in a kubernetes environment.
// running inside a pod running on kubernetes. It will return ErrNotInCluster
// if called from a process not running in a kubernetes environment.
func InClusterConfig() (*Config, error) {
const (
tokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
rootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
)
host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
if len(host) == 0 || len(port) == 0 {
return nil, fmt.Errorf("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined")
return nil, ErrNotInCluster
}
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
ts := newCachedPathTokenSource(tokenFile)
if _, err := ts.Token(); err != nil {
return nil, err
}
tlsClientConfig := TLSClientConfig{}
rootCAFile := "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
if _, err := certutil.NewPool(rootCAFile); err != nil {
glog.Errorf("Expected to load root CA config from %s, but got err: %v", rootCAFile, err)
} else {
@@ -331,8 +339,8 @@ func InClusterConfig() (*Config, error) {
return &Config{
// TODO: switch to using cluster DNS.
Host: "https://" + net.JoinHostPort(host, port),
BearerToken: string(token),
TLSClientConfig: tlsClientConfig,
WrapTransport: TokenSourceWrapTransport(ts),
}, nil
}

View File

@@ -198,7 +198,7 @@ func (r *Request) Throttle(limiter flowcontrol.RateLimiter) *Request {
return r
}
// SubResource sets a sub-resource path which can be multiple segments segment after the resource
// SubResource sets a sub-resource path which can be multiple segments after the resource
// name but before the suffix.
func (r *Request) SubResource(subresources ...string) *Request {
if r.err != nil {
@@ -455,17 +455,9 @@ func (r *Request) URL() *url.URL {
// finalURLTemplate is similar to URL(), but will make all specific parameter values equal
// - instead of name or namespace, "{name}" and "{namespace}" will be used, and all query
// parameters will be reset. This creates a copy of the request so as not to change the
// underlying object. This means some useful request info (like the types of field
// selectors in use) will be lost.
// TODO: preserve field selector keys
// parameters will be reset. This creates a copy of the url so as not to change the
// underlying object.
func (r Request) finalURLTemplate() url.URL {
if len(r.resourceName) != 0 {
r.resourceName = "{name}"
}
if r.namespaceSet && len(r.namespace) != 0 {
r.namespace = "{namespace}"
}
newParams := url.Values{}
v := []string{"{value}"}
for k := range r.params {
@@ -473,6 +465,59 @@ func (r Request) finalURLTemplate() url.URL {
}
r.params = newParams
url := r.URL()
segments := strings.Split(r.URL().Path, "/")
groupIndex := 0
index := 0
if r.URL() != nil && r.baseURL != nil && strings.Contains(r.URL().Path, r.baseURL.Path) {
groupIndex += len(strings.Split(r.baseURL.Path, "/"))
}
if groupIndex >= len(segments) {
return *url
}
const CoreGroupPrefix = "api"
const NamedGroupPrefix = "apis"
isCoreGroup := segments[groupIndex] == CoreGroupPrefix
isNamedGroup := segments[groupIndex] == NamedGroupPrefix
if isCoreGroup {
// checking the case of core group with /api/v1/... format
index = groupIndex + 2
} else if isNamedGroup {
// checking the case of named group with /apis/apps/v1/... format
index = groupIndex + 3
} else {
// this should not happen that the only two possibilities are /api... and /apis..., just want to put an
// outlet here in case more API groups are added in future if ever possible:
// https://kubernetes.io/docs/concepts/overview/kubernetes-api/#api-groups
// if a wrong API groups name is encountered, return the {prefix} for url.Path
url.Path = "/{prefix}"
url.RawQuery = ""
return *url
}
//switch segLength := len(segments) - index; segLength {
switch {
// case len(segments) - index == 1:
// resource (with no name) do nothing
case len(segments)-index == 2:
// /$RESOURCE/$NAME: replace $NAME with {name}
segments[index+1] = "{name}"
case len(segments)-index == 3:
if segments[index+2] == "finalize" || segments[index+2] == "status" {
// /$RESOURCE/$NAME/$SUBRESOURCE: replace $NAME with {name}
segments[index+1] = "{name}"
} else {
// /namespace/$NAMESPACE/$RESOURCE: replace $NAMESPACE with {namespace}
segments[index+1] = "{namespace}"
}
case len(segments)-index >= 4:
segments[index+1] = "{namespace}"
// /namespace/$NAMESPACE/$RESOURCE/$NAME: replace $NAMESPACE with {namespace}, $NAME with {name}
if segments[index+3] != "finalize" && segments[index+3] != "status" {
// /$RESOURCE/$NAME/$SUBRESOURCE: replace $NAME with {name}
segments[index+3] = "{name}"
}
}
url.Path = path.Join(segments...)
return *url
}

View File

@@ -340,21 +340,169 @@ func TestResultIntoWithNoBodyReturnsErr(t *testing.T) {
}
func TestURLTemplate(t *testing.T) {
uri, _ := url.Parse("http://localhost")
r := NewRequest(nil, "POST", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0)
r.Prefix("pre1").Resource("r1").Namespace("ns").Name("nm").Param("p0", "v0")
full := r.URL()
if full.String() != "http://localhost/pre1/namespaces/ns/r1/nm?p0=v0" {
t.Errorf("unexpected initial URL: %s", full)
uri, _ := url.Parse("http://localhost/some/base/url/path")
testCases := []struct {
Request *Request
ExpectedFullURL string
ExpectedFinalURL string
}{
{
// non dynamic client
Request: NewRequest(nil, "POST", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("api", "v1").Resource("r1").Namespace("ns").Name("nm").Param("p0", "v0"),
ExpectedFullURL: "http://localhost/some/base/url/path/api/v1/namespaces/ns/r1/nm?p0=v0",
ExpectedFinalURL: "http://localhost/some/base/url/path/api/v1/namespaces/%7Bnamespace%7D/r1/%7Bname%7D?p0=%7Bvalue%7D",
},
{
// non dynamic client with wrong api group
Request: NewRequest(nil, "POST", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("pre1", "v1").Resource("r1").Namespace("ns").Name("nm").Param("p0", "v0"),
ExpectedFullURL: "http://localhost/some/base/url/path/pre1/v1/namespaces/ns/r1/nm?p0=v0",
ExpectedFinalURL: "http://localhost/%7Bprefix%7D",
},
{
// dynamic client with core group + namespace + resourceResource (with name)
// /api/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE/%NAME
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/api/v1/namespaces/ns/r1/name1"),
ExpectedFullURL: "http://localhost/some/base/url/path/api/v1/namespaces/ns/r1/name1",
ExpectedFinalURL: "http://localhost/some/base/url/path/api/v1/namespaces/%7Bnamespace%7D/r1/%7Bname%7D",
},
{
// dynamic client with named group + namespace + resourceResource (with name)
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE/%NAME
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/g1/v1/namespaces/ns/r1/name1"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/g1/v1/namespaces/ns/r1/name1",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/g1/v1/namespaces/%7Bnamespace%7D/r1/%7Bname%7D",
},
{
// dynamic client with core group + namespace + resourceResource (with NO name)
// /api/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/api/v1/namespaces/ns/r1"),
ExpectedFullURL: "http://localhost/some/base/url/path/api/v1/namespaces/ns/r1",
ExpectedFinalURL: "http://localhost/some/base/url/path/api/v1/namespaces/%7Bnamespace%7D/r1",
},
{
// dynamic client with named group + namespace + resourceResource (with NO name)
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/g1/v1/namespaces/ns/r1"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/g1/v1/namespaces/ns/r1",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/g1/v1/namespaces/%7Bnamespace%7D/r1",
},
{
// dynamic client with core group + resourceResource (with name)
// /api/$RESOURCEVERSION/$RESOURCE/%NAME
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/api/v1/r1/name1"),
ExpectedFullURL: "http://localhost/some/base/url/path/api/v1/r1/name1",
ExpectedFinalURL: "http://localhost/some/base/url/path/api/v1/r1/%7Bname%7D",
},
{
// dynamic client with named group + resourceResource (with name)
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/$RESOURCE/%NAME
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/g1/v1/r1/name1"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/g1/v1/r1/name1",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/g1/v1/r1/%7Bname%7D",
},
{
// dynamic client with named group + namespace + resourceResource (with name) + subresource
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE/%NAME/$SUBRESOURCE
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/namespaces/namespaces/namespaces/namespaces/namespaces/namespaces/finalize"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/namespaces/namespaces/namespaces/finalize",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/%7Bnamespace%7D/namespaces/%7Bname%7D/finalize",
},
{
// dynamic client with named group + namespace + resourceResource (with name)
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE/%NAME
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/namespaces/namespaces/namespaces/namespaces/namespaces/namespaces"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/namespaces/namespaces/namespaces",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/%7Bnamespace%7D/namespaces/%7Bname%7D",
},
{
// dynamic client with named group + namespace + resourceResource (with NO name) + subresource
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE/%SUBRESOURCE
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/namespaces/namespaces/namespaces/namespaces/namespaces/finalize"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/namespaces/namespaces/finalize",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/%7Bnamespace%7D/namespaces/finalize",
},
{
// dynamic client with named group + namespace + resourceResource (with NO name) + subresource
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE/%SUBRESOURCE
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/namespaces/namespaces/namespaces/namespaces/namespaces/status"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/namespaces/namespaces/status",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/%7Bnamespace%7D/namespaces/status",
},
{
// dynamic client with named group + namespace + resourceResource (with no name)
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE/%NAME
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/namespaces/namespaces/namespaces/namespaces/namespaces"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/namespaces/namespaces",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/%7Bnamespace%7D/namespaces",
},
{
// dynamic client with named group + resourceResource (with name) + subresource
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE/%NAME
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/namespaces/namespaces/namespaces/namespaces/finalize"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/namespaces/finalize",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/%7Bname%7D/finalize",
},
{
// dynamic client with named group + resourceResource (with name) + subresource
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE/%NAME
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/namespaces/namespaces/namespaces/namespaces/status"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/namespaces/status",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/%7Bname%7D/status",
},
{
// dynamic client with named group + resourceResource (with name)
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/$RESOURCE/%NAME
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/namespaces/namespaces/namespaces/namespaces"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/namespaces",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces/%7Bname%7D",
},
{
// dynamic client with named group + resourceResource (with no name)
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/$RESOURCE/%NAME
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/apis/namespaces/namespaces/namespaces"),
ExpectedFullURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces",
ExpectedFinalURL: "http://localhost/some/base/url/path/apis/namespaces/namespaces/namespaces",
},
{
// dynamic client with wrong api group + namespace + resourceResource (with name) + subresource
// /apis/$NAMEDGROUPNAME/$RESOURCEVERSION/namespaces/$NAMESPACE/$RESOURCE/%NAME/$SUBRESOURCE
Request: NewRequest(nil, "DELETE", uri, "", ContentConfig{GroupVersion: &schema.GroupVersion{Group: "test"}}, Serializers{}, nil, nil, 0).
Prefix("/pre1/namespaces/namespaces/namespaces/namespaces/namespaces/namespaces/finalize"),
ExpectedFullURL: "http://localhost/some/base/url/path/pre1/namespaces/namespaces/namespaces/namespaces/namespaces/namespaces/finalize",
ExpectedFinalURL: "http://localhost/%7Bprefix%7D",
},
}
actualURL := r.finalURLTemplate()
actual := actualURL.String()
expected := "http://localhost/pre1/namespaces/%7Bnamespace%7D/r1/%7Bname%7D?p0=%7Bvalue%7D"
if actual != expected {
t.Errorf("unexpected URL template: %s %s", actual, expected)
}
if r.URL().String() != full.String() {
t.Errorf("creating URL template changed request: %s -> %s", full.String(), r.URL().String())
for i, testCase := range testCases {
r := testCase.Request
full := r.URL()
if full.String() != testCase.ExpectedFullURL {
t.Errorf("%d: unexpected initial URL: %s %s", i, full, testCase.ExpectedFullURL)
}
actualURL := r.finalURLTemplate()
actual := actualURL.String()
if actual != testCase.ExpectedFinalURL {
t.Errorf("%d: unexpected URL template: %s %s", i, actual, testCase.ExpectedFinalURL)
}
if r.URL().String() != full.String() {
t.Errorf("%d, creating URL template changed request: %s -> %s", i, full.String(), r.URL().String())
}
}
}

138
rest/token_source.go Normal file
View File

@@ -0,0 +1,138 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rest
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"sync"
"time"
"github.com/golang/glog"
"golang.org/x/oauth2"
)
// TokenSourceWrapTransport returns a WrapTransport that injects bearer tokens
// authentication from an oauth2.TokenSource.
func TokenSourceWrapTransport(ts oauth2.TokenSource) func(http.RoundTripper) http.RoundTripper {
return func(rt http.RoundTripper) http.RoundTripper {
return &tokenSourceTransport{
base: rt,
ort: &oauth2.Transport{
Source: ts,
Base: rt,
},
}
}
}
func newCachedPathTokenSource(path string) oauth2.TokenSource {
return &cachingTokenSource{
now: time.Now,
leeway: 1 * time.Minute,
base: &fileTokenSource{
path: path,
// This period was picked because it is half of the minimum validity
// duration for a token provisioned by they TokenRequest API. This is
// unsophisticated and should induce rotation at a frequency that should
// work with the token volume source.
period: 5 * time.Minute,
},
}
}
type tokenSourceTransport struct {
base http.RoundTripper
ort http.RoundTripper
}
func (tst *tokenSourceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// This is to allow --token to override other bearer token providers.
if req.Header.Get("Authorization") != "" {
return tst.base.RoundTrip(req)
}
return tst.ort.RoundTrip(req)
}
type fileTokenSource struct {
path string
period time.Duration
}
var _ = oauth2.TokenSource(&fileTokenSource{})
func (ts *fileTokenSource) Token() (*oauth2.Token, error) {
tokb, err := ioutil.ReadFile(ts.path)
if err != nil {
return nil, fmt.Errorf("failed to read token file %q: %v", ts.path, err)
}
tok := strings.TrimSpace(string(tokb))
if len(tok) == 0 {
return nil, fmt.Errorf("read empty token from file %q", ts.path)
}
return &oauth2.Token{
AccessToken: tok,
Expiry: time.Now().Add(ts.period),
}, nil
}
type cachingTokenSource struct {
base oauth2.TokenSource
leeway time.Duration
sync.RWMutex
tok *oauth2.Token
// for testing
now func() time.Time
}
var _ = oauth2.TokenSource(&cachingTokenSource{})
func (ts *cachingTokenSource) Token() (*oauth2.Token, error) {
now := ts.now()
// fast path
ts.RLock()
tok := ts.tok
ts.RUnlock()
if tok != nil && tok.Expiry.Add(-1*ts.leeway).After(now) {
return tok, nil
}
// slow path
ts.Lock()
defer ts.Unlock()
if tok := ts.tok; tok != nil && tok.Expiry.Add(-1*ts.leeway).After(now) {
return tok, nil
}
tok, err := ts.base.Token()
if err != nil {
if ts.tok == nil {
return nil, err
}
glog.Errorf("Unable to rotate token: %v", err)
return ts.tok, nil
}
ts.tok = tok
return tok, nil
}

156
rest/token_source_test.go Normal file
View File

@@ -0,0 +1,156 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rest
import (
"fmt"
"reflect"
"sync"
"testing"
"time"
"golang.org/x/oauth2"
)
type testTokenSource struct {
calls int
tok *oauth2.Token
err error
}
func (ts *testTokenSource) Token() (*oauth2.Token, error) {
ts.calls++
return ts.tok, ts.err
}
func TestCachingTokenSource(t *testing.T) {
start := time.Now()
tokA := &oauth2.Token{
AccessToken: "a",
Expiry: start.Add(10 * time.Minute),
}
tokB := &oauth2.Token{
AccessToken: "b",
Expiry: start.Add(20 * time.Minute),
}
tests := []struct {
name string
tok *oauth2.Token
tsTok *oauth2.Token
tsErr error
wait time.Duration
wantTok *oauth2.Token
wantErr bool
wantTSCalls int
}{
{
name: "valid token returned from cache",
tok: tokA,
wantTok: tokA,
},
{
name: "valid token returned from cache 1 minute before scheduled refresh",
tok: tokA,
wait: 8 * time.Minute,
wantTok: tokA,
},
{
name: "new token created when cache is empty",
tsTok: tokA,
wantTok: tokA,
wantTSCalls: 1,
},
{
name: "new token created 1 minute after scheduled refresh",
tok: tokA,
tsTok: tokB,
wait: 10 * time.Minute,
wantTok: tokB,
wantTSCalls: 1,
},
{
name: "error on create token returns error",
tsErr: fmt.Errorf("error"),
wantErr: true,
wantTSCalls: 1,
},
}
for _, c := range tests {
t.Run(c.name, func(t *testing.T) {
tts := &testTokenSource{
tok: c.tsTok,
err: c.tsErr,
}
ts := &cachingTokenSource{
base: tts,
tok: c.tok,
leeway: 1 * time.Minute,
now: func() time.Time { return start.Add(c.wait) },
}
gotTok, gotErr := ts.Token()
if got, want := gotTok, c.wantTok; !reflect.DeepEqual(got, want) {
t.Errorf("unexpected token:\n\tgot:\t%#v\n\twant:\t%#v", got, want)
}
if got, want := tts.calls, c.wantTSCalls; got != want {
t.Errorf("unexpected number of Token() calls: got %d, want %d", got, want)
}
if gotErr == nil && c.wantErr {
t.Errorf("wanted error but got none")
}
if gotErr != nil && !c.wantErr {
t.Errorf("unexpected error: %v", gotErr)
}
})
}
}
func TestCachingTokenSourceRace(t *testing.T) {
for i := 0; i < 100; i++ {
tts := &testTokenSource{
tok: &oauth2.Token{
AccessToken: "a",
Expiry: time.Now().Add(1000 * time.Hour),
},
}
ts := &cachingTokenSource{
now: time.Now,
base: tts,
leeway: 1 * time.Minute,
}
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
if _, err := ts.Token(); err != nil {
t.Fatalf("err: %v", err)
}
}()
}
wg.Wait()
if tts.calls != 1 {
t.Errorf("expected one call to Token() but saw: %d", tts.calls)
}
}
}

View File

@@ -34,6 +34,6 @@ type ScaleInterface interface {
// Get fetches the scale of the given scalable resource.
Get(resource schema.GroupResource, name string) (*autoscalingapi.Scale, error)
// Update updates the scale of the the given scalable resource.
// Update updates the scale of the given scalable resource.
Update(resource schema.GroupResource, scale *autoscalingapi.Scale) (*autoscalingapi.Scale, error)
}

View File

@@ -1,5 +0,0 @@
approvers:
- jbeda
- luxas
reviewers:
- mattmoyer

View File

@@ -1,112 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"k8s.io/api/core/v1"
)
const (
// BootstrapTokenSecretPrefix is the prefix for bootstrap token names.
// Bootstrap tokens secrets must be named in the form
// `bootstrap-token-<token-id>`. This is the prefix to be used before the
// token ID.
BootstrapTokenSecretPrefix = "bootstrap-token-"
// SecretTypeBootstrapToken is used during the automated bootstrap process (first
// implemented by kubeadm). It stores tokens that are used to sign well known
// ConfigMaps. They may also eventually be used for authentication.
SecretTypeBootstrapToken v1.SecretType = "bootstrap.kubernetes.io/token"
// BootstrapTokenIDKey is the id of this token. This can be transmitted in the
// clear and encoded in the name of the secret. It must be a random 6 character
// string that matches the regexp `^([a-z0-9]{6})$`. Required.
BootstrapTokenIDKey = "token-id"
// BootstrapTokenSecretKey is the actual secret. It must be a random 16 character
// string that matches the regexp `^([a-z0-9]{16})$`. Required.
BootstrapTokenSecretKey = "token-secret"
// BootstrapTokenExpirationKey is when this token should be expired and no
// longer used. A controller will delete this resource after this time. This
// is an absolute UTC time using RFC3339. If this cannot be parsed, the token
// should be considered invalid. Optional.
BootstrapTokenExpirationKey = "expiration"
// BootstrapTokenDescriptionKey is a description in human-readable format that
// describes what the bootstrap token is used for. Optional.
BootstrapTokenDescriptionKey = "description"
// BootstrapTokenExtraGroupsKey is a comma-separated list of group names.
// The bootstrap token will authenticate as these groups in addition to the
// "system:bootstrappers" group.
BootstrapTokenExtraGroupsKey = "auth-extra-groups"
// BootstrapTokenUsagePrefix is the prefix for the other usage constants that specifies different
// functions of a bootstrap token
BootstrapTokenUsagePrefix = "usage-bootstrap-"
// BootstrapTokenUsageSigningKey signals that this token should be used to
// sign configs as part of the bootstrap process. Value must be "true". Any
// other value is assumed to be false. Optional.
BootstrapTokenUsageSigningKey = "usage-bootstrap-signing"
// BootstrapTokenUsageAuthentication signals that this token should be used
// as a bearer token to authenticate against the Kubernetes API. The bearer
// token takes the form "<token-id>.<token-secret>" and authenticates as the
// user "system:bootstrap:<token-id>" in the "system:bootstrappers" group
// as well as any groups specified using BootstrapTokenExtraGroupsKey.
// Value must be "true". Any other value is assumed to be false. Optional.
BootstrapTokenUsageAuthentication = "usage-bootstrap-authentication"
// ConfigMapClusterInfo defines the name for the ConfigMap where the information how to connect and trust the cluster exist
ConfigMapClusterInfo = "cluster-info"
// KubeConfigKey defines at which key in the Data object of the ConfigMap the KubeConfig object is stored
KubeConfigKey = "kubeconfig"
// JWSSignatureKeyPrefix defines what key prefix the JWS-signed tokens have
JWSSignatureKeyPrefix = "jws-kubeconfig-"
// BootstrapUserPrefix is the username prefix bootstrapping bearer tokens
// authenticate as. The full username given is "system:bootstrap:<token-id>".
BootstrapUserPrefix = "system:bootstrap:"
// BootstrapDefaultGroup is the default group for bootstrapping bearer
// tokens (in addition to any groups from BootstrapTokenExtraGroupsKey).
BootstrapDefaultGroup = "system:bootstrappers"
// BootstrapGroupPattern is the valid regex pattern that all groups
// assigned to a bootstrap token by BootstrapTokenExtraGroupsKey must match.
// See also util.ValidateBootstrapGroupName()
BootstrapGroupPattern = `\Asystem:bootstrappers:[a-z0-9:-]{0,255}[a-z0-9]\z`
// BootstrapTokenPattern defines the {id}.{secret} regular expression pattern
BootstrapTokenPattern = `\A([a-z0-9]{6})\.([a-z0-9]{16})\z`
// BootstrapTokenIDPattern defines token's id regular expression pattern
BootstrapTokenIDPattern = `\A([a-z0-9]{6})\z`
// BootstrapTokenIDBytes defines the number of bytes used for the Bootstrap Token's ID field
BootstrapTokenIDBytes = 6
// BootstrapTokenSecretBytes defines the number of bytes used the Bootstrap Token's Secret field
BootstrapTokenSecretBytes = 16
)
// KnownTokenUsages specifies the known functions a token will get.
var KnownTokenUsages = []string{"signing", "authentication"}

View File

@@ -1,133 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"bufio"
"crypto/rand"
"fmt"
"regexp"
"strings"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/tools/bootstrap/token/api"
)
// validBootstrapTokenChars defines the characters a bootstrap token can consist of
const validBootstrapTokenChars = "0123456789abcdefghijklmnopqrstuvwxyz"
var (
// BootstrapTokenRegexp is a compiled regular expression of TokenRegexpString
BootstrapTokenRegexp = regexp.MustCompile(api.BootstrapTokenPattern)
// BootstrapTokenIDRegexp is a compiled regular expression of TokenIDRegexpString
BootstrapTokenIDRegexp = regexp.MustCompile(api.BootstrapTokenIDPattern)
// BootstrapGroupRegexp is a compiled regular expression of BootstrapGroupPattern
BootstrapGroupRegexp = regexp.MustCompile(api.BootstrapGroupPattern)
)
// GenerateBootstrapToken generates a new, random Bootstrap Token.
func GenerateBootstrapToken() (string, error) {
tokenID, err := randBytes(api.BootstrapTokenIDBytes)
if err != nil {
return "", err
}
tokenSecret, err := randBytes(api.BootstrapTokenSecretBytes)
if err != nil {
return "", err
}
return TokenFromIDAndSecret(tokenID, tokenSecret), nil
}
// randBytes returns a random string consisting of the characters in
// validBootstrapTokenChars, with the length customized by the parameter
func randBytes(length int) (string, error) {
// len("0123456789abcdefghijklmnopqrstuvwxyz") = 36 which doesn't evenly divide
// the possible values of a byte: 256 mod 36 = 4. Discard any random bytes we
// read that are >= 252 so the bytes we evenly divide the character set.
const maxByteValue = 252
var (
b byte
err error
token = make([]byte, length)
)
reader := bufio.NewReaderSize(rand.Reader, length*2)
for i := range token {
for {
if b, err = reader.ReadByte(); err != nil {
return "", err
}
if b < maxByteValue {
break
}
}
token[i] = validBootstrapTokenChars[int(b)%len(validBootstrapTokenChars)]
}
return string(token), nil
}
// TokenFromIDAndSecret returns the full token which is of the form "{id}.{secret}"
func TokenFromIDAndSecret(id, secret string) string {
return fmt.Sprintf("%s.%s", id, secret)
}
// IsValidBootstrapToken returns whether the given string is valid as a Bootstrap Token and
// in other words satisfies the BootstrapTokenRegexp
func IsValidBootstrapToken(token string) bool {
return BootstrapTokenRegexp.MatchString(token)
}
// IsValidBootstrapTokenID returns whether the given string is valid as a Bootstrap Token ID and
// in other words satisfies the BootstrapTokenIDRegexp
func IsValidBootstrapTokenID(tokenID string) bool {
return BootstrapTokenIDRegexp.MatchString(tokenID)
}
// BootstrapTokenSecretName returns the expected name for the Secret storing the
// Bootstrap Token in the Kubernetes API.
func BootstrapTokenSecretName(tokenID string) string {
return fmt.Sprintf("%s%s", api.BootstrapTokenSecretPrefix, tokenID)
}
// ValidateBootstrapGroupName checks if the provided group name is a valid
// bootstrap group name. Returns nil if valid or a validation error if invalid.
func ValidateBootstrapGroupName(name string) error {
if BootstrapGroupRegexp.Match([]byte(name)) {
return nil
}
return fmt.Errorf("bootstrap group %q is invalid (must match %s)", name, api.BootstrapGroupPattern)
}
// ValidateUsages validates that the passed in string are valid usage strings for bootstrap tokens.
func ValidateUsages(usages []string) error {
validUsages := sets.NewString(api.KnownTokenUsages...)
invalidUsages := sets.NewString()
for _, usage := range usages {
if !validUsages.Has(usage) {
invalidUsages.Insert(usage)
}
}
if len(invalidUsages) > 0 {
return fmt.Errorf("invalid bootstrap token usage string: %s, valid usage options: %s", strings.Join(invalidUsages.List(), ","), strings.Join(api.KnownTokenUsages, ","))
}
return nil
}

View File

@@ -1,213 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"strings"
"testing"
)
func TestGenerateBootstrapToken(t *testing.T) {
token, err := GenerateBootstrapToken()
if err != nil {
t.Fatalf("GenerateBootstrapToken returned an unexpected error: %+v", err)
}
if !IsValidBootstrapToken(token) {
t.Errorf("GenerateBootstrapToken didn't generate a valid token: %q", token)
}
}
func TestRandBytes(t *testing.T) {
var randTest = []int{
0,
1,
2,
3,
100,
}
for _, rt := range randTest {
actual, err := randBytes(rt)
if err != nil {
t.Errorf("failed randBytes: %v", err)
}
if len(actual) != rt {
t.Errorf("failed randBytes:\n\texpected: %d\n\t actual: %d\n", rt, len(actual))
}
}
}
func TestTokenFromIDAndSecret(t *testing.T) {
var tests = []struct {
id string
secret string
expected string
}{
{"foo", "bar", "foo.bar"}, // should use default
{"abcdef", "abcdef0123456789", "abcdef.abcdef0123456789"},
{"h", "b", "h.b"},
}
for _, rt := range tests {
actual := TokenFromIDAndSecret(rt.id, rt.secret)
if actual != rt.expected {
t.Errorf(
"failed TokenFromIDAndSecret:\n\texpected: %s\n\t actual: %s",
rt.expected,
actual,
)
}
}
}
func TestIsValidBootstrapToken(t *testing.T) {
var tests = []struct {
token string
expected bool
}{
{token: "", expected: false},
{token: ".", expected: false},
{token: "1234567890123456789012", expected: false}, // invalid parcel size
{token: "12345.1234567890123456", expected: false}, // invalid parcel size
{token: ".1234567890123456", expected: false}, // invalid parcel size
{token: "123456.", expected: false}, // invalid parcel size
{token: "123456:1234567890.123456", expected: false}, // invalid separation
{token: "abcdef:1234567890123456", expected: false}, // invalid separation
{token: "Abcdef.1234567890123456", expected: false}, // invalid token id
{token: "123456.AABBCCDDEEFFGGHH", expected: false}, // invalid token secret
{token: "123456.AABBCCD-EEFFGGHH", expected: false}, // invalid character
{token: "abc*ef.1234567890123456", expected: false}, // invalid character
{token: "abcdef.1234567890123456", expected: true},
{token: "123456.aabbccddeeffgghh", expected: true},
{token: "ABCDEF.abcdef0123456789", expected: false},
{token: "abcdef.abcdef0123456789", expected: true},
{token: "123456.1234560123456789", expected: true},
}
for _, rt := range tests {
actual := IsValidBootstrapToken(rt.token)
if actual != rt.expected {
t.Errorf(
"failed IsValidBootstrapToken for the token %q\n\texpected: %t\n\t actual: %t",
rt.token,
rt.expected,
actual,
)
}
}
}
func TestIsValidBootstrapTokenID(t *testing.T) {
var tests = []struct {
tokenID string
expected bool
}{
{tokenID: "", expected: false},
{tokenID: "1234567890123456789012", expected: false},
{tokenID: "12345", expected: false},
{tokenID: "Abcdef", expected: false},
{tokenID: "ABCDEF", expected: false},
{tokenID: "abcdef.", expected: false},
{tokenID: "abcdef", expected: true},
{tokenID: "123456", expected: true},
}
for _, rt := range tests {
actual := IsValidBootstrapTokenID(rt.tokenID)
if actual != rt.expected {
t.Errorf(
"failed IsValidBootstrapTokenID for the token %q\n\texpected: %t\n\t actual: %t",
rt.tokenID,
rt.expected,
actual,
)
}
}
}
func TestBootstrapTokenSecretName(t *testing.T) {
var tests = []struct {
tokenID string
expected string
}{
{"foo", "bootstrap-token-foo"},
{"bar", "bootstrap-token-bar"},
{"", "bootstrap-token-"},
{"abcdef", "bootstrap-token-abcdef"},
}
for _, rt := range tests {
actual := BootstrapTokenSecretName(rt.tokenID)
if actual != rt.expected {
t.Errorf(
"failed BootstrapTokenSecretName:\n\texpected: %s\n\t actual: %s",
rt.expected,
actual,
)
}
}
}
func TestValidateBootstrapGroupName(t *testing.T) {
tests := []struct {
name string
input string
valid bool
}{
{"valid", "system:bootstrappers:foo", true},
{"valid nested", "system:bootstrappers:foo:bar:baz", true},
{"valid with dashes and number", "system:bootstrappers:foo-bar-42", true},
{"invalid uppercase", "system:bootstrappers:Foo", false},
{"missing prefix", "foo", false},
{"prefix with no body", "system:bootstrappers:", false},
{"invalid spaces", "system:bootstrappers: ", false},
{"invalid asterisk", "system:bootstrappers:*", false},
{"trailing colon", "system:bootstrappers:foo:", false},
{"trailing dash", "system:bootstrappers:foo-", false},
{"script tags", "system:bootstrappers:<script> alert(\"scary?!\") </script>", false},
{"too long", "system:bootstrappers:" + strings.Repeat("x", 300), false},
}
for _, test := range tests {
err := ValidateBootstrapGroupName(test.input)
if err != nil && test.valid {
t.Errorf("test %q: ValidateBootstrapGroupName(%q) returned unexpected error: %v", test.name, test.input, err)
}
if err == nil && !test.valid {
t.Errorf("test %q: ValidateBootstrapGroupName(%q) was supposed to return an error but didn't", test.name, test.input)
}
}
}
func TestValidateUsages(t *testing.T) {
tests := []struct {
name string
input []string
valid bool
}{
{"valid of signing", []string{"signing"}, true},
{"valid of authentication", []string{"authentication"}, true},
{"all valid", []string{"authentication", "signing"}, true},
{"single invalid", []string{"authentication", "foo"}, false},
{"all invalid", []string{"foo", "bar"}, false},
}
for _, test := range tests {
err := ValidateUsages(test.input)
if err != nil && test.valid {
t.Errorf("test %q: ValidateUsages(%v) returned unexpected error: %v", test.name, test.input, err)
}
if err == nil && !test.valid {
t.Errorf("test %q: ValidateUsages(%v) was supposed to return an error but didn't", test.name, test.input)
}
}
}

2
tools/cache/heap.go vendored
View File

@@ -204,7 +204,7 @@ func (h *Heap) AddIfNotPresent(obj interface{}) error {
return nil
}
// addIfNotPresentLocked assumes the lock is already held and adds the the provided
// addIfNotPresentLocked assumes the lock is already held and adds the provided
// item to the queue if it does not already exist.
func (h *Heap) addIfNotPresentLocked(key string, obj interface{}) {
if _, exists := h.data.items[key]; exists {

View File

@@ -17,6 +17,7 @@ limitations under the License.
package cache
import (
"k8s.io/apimachinery/pkg/util/sets"
"strings"
"testing"
@@ -70,60 +71,31 @@ func TestMultiIndexKeys(t *testing.T) {
index.Add(pod2)
index.Add(pod3)
erniePods, err := index.ByIndex("byUser", "ernie")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(erniePods) != 2 {
t.Errorf("Expected 2 pods but got %v", len(erniePods))
}
for _, erniePod := range erniePods {
if erniePod.(*v1.Pod).Name != "one" && erniePod.(*v1.Pod).Name != "tre" {
t.Errorf("Expected only 'one' or 'tre' but got %s", erniePod.(*v1.Pod).Name)
}
}
bertPods, err := index.ByIndex("byUser", "bert")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(bertPods) != 2 {
t.Errorf("Expected 2 pods but got %v", len(bertPods))
}
for _, bertPod := range bertPods {
if bertPod.(*v1.Pod).Name != "one" && bertPod.(*v1.Pod).Name != "two" {
t.Errorf("Expected only 'one' or 'two' but got %s", bertPod.(*v1.Pod).Name)
}
}
oscarPods, err := index.ByIndex("byUser", "oscar")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(oscarPods) != 1 {
t.Errorf("Expected 1 pods but got %v", len(erniePods))
}
for _, oscarPod := range oscarPods {
if oscarPod.(*v1.Pod).Name != "two" {
t.Errorf("Expected only 'two' but got %s", oscarPod.(*v1.Pod).Name)
}
}
ernieAndBertKeys, err := index.Index("byUser", pod1)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(ernieAndBertKeys) != 3 {
t.Errorf("Expected 3 pods but got %v", len(ernieAndBertKeys))
}
for _, ernieAndBertKey := range ernieAndBertKeys {
if ernieAndBertKey.(*v1.Pod).Name != "one" && ernieAndBertKey.(*v1.Pod).Name != "two" && ernieAndBertKey.(*v1.Pod).Name != "tre" {
t.Errorf("Expected only 'one', 'two' or 'tre' but got %s", ernieAndBertKey.(*v1.Pod).Name)
expected := map[string]sets.String{}
expected["ernie"] = sets.NewString("one", "tre")
expected["bert"] = sets.NewString("one", "two")
expected["elmo"] = sets.NewString("tre")
expected["oscar"] = sets.NewString("two")
expected["elmo"] = sets.NewString() // let's just make sure we don't get anything back in this case
{
for k, v := range expected {
found := sets.String{}
indexResults, err := index.ByIndex("byUser", k)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
for _, item := range indexResults {
found.Insert(item.(*v1.Pod).Name)
}
items := v.List()
if !found.HasAll(items...) {
t.Errorf("missing items, index %s, expected %v but found %v", k, items, found.List())
}
}
}
index.Delete(pod3)
erniePods, err = index.ByIndex("byUser", "ernie")
erniePods, err := index.ByIndex("byUser", "ernie")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
@@ -147,7 +119,7 @@ func TestMultiIndexKeys(t *testing.T) {
copyOfPod2 := pod2.DeepCopy()
copyOfPod2.Annotations["users"] = "oscar"
index.Update(copyOfPod2)
bertPods, err = index.ByIndex("byUser", "bert")
bertPods, err := index.ByIndex("byUser", "bert")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
@@ -159,5 +131,4 @@ func TestMultiIndexKeys(t *testing.T) {
t.Errorf("Expected only 'one' but got %s", bertPod.(*v1.Pod).Name)
}
}
}

View File

@@ -18,17 +18,13 @@ package cache
import (
"context"
"time"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/pager"
watchtools "k8s.io/client-go/tools/watch"
)
// ListerWatcher is any object that knows how to perform an initial list and start a watch on a resource.
@@ -94,13 +90,6 @@ func NewFilteredListWatchFromClient(c Getter, resource string, namespace string,
return &ListWatch{ListFunc: listFunc, WatchFunc: watchFunc}
}
func timeoutFromListOptions(options metav1.ListOptions) time.Duration {
if options.TimeoutSeconds != nil {
return time.Duration(*options.TimeoutSeconds) * time.Second
}
return 0
}
// List a set of apiserver resources
func (lw *ListWatch) List(options metav1.ListOptions) (runtime.Object, error) {
if !lw.DisableChunking {
@@ -113,78 +102,3 @@ func (lw *ListWatch) List(options metav1.ListOptions) (runtime.Object, error) {
func (lw *ListWatch) Watch(options metav1.ListOptions) (watch.Interface, error) {
return lw.WatchFunc(options)
}
// ListWatchUntil checks the provided conditions against the items returned by the list watcher, returning wait.ErrWaitTimeout
// if timeout is exceeded without all conditions returning true, or an error if an error occurs.
// TODO: check for watch expired error and retry watch from latest point? Same issue exists for Until.
func ListWatchUntil(timeout time.Duration, lw ListerWatcher, conditions ...watchtools.ConditionFunc) (*watch.Event, error) {
if len(conditions) == 0 {
return nil, nil
}
list, err := lw.List(metav1.ListOptions{})
if err != nil {
return nil, err
}
initialItems, err := meta.ExtractList(list)
if err != nil {
return nil, err
}
// use the initial items as simulated "adds"
var lastEvent *watch.Event
currIndex := 0
passedConditions := 0
for _, condition := range conditions {
// check the next condition against the previous event and short circuit waiting for the next watch
if lastEvent != nil {
done, err := condition(*lastEvent)
if err != nil {
return lastEvent, err
}
if done {
passedConditions = passedConditions + 1
continue
}
}
ConditionSucceeded:
for currIndex < len(initialItems) {
lastEvent = &watch.Event{Type: watch.Added, Object: initialItems[currIndex]}
currIndex++
done, err := condition(*lastEvent)
if err != nil {
return lastEvent, err
}
if done {
passedConditions = passedConditions + 1
break ConditionSucceeded
}
}
}
if passedConditions == len(conditions) {
return lastEvent, nil
}
remainingConditions := conditions[passedConditions:]
metaObj, err := meta.ListAccessor(list)
if err != nil {
return nil, err
}
currResourceVersion := metaObj.GetResourceVersion()
watchInterface, err := lw.Watch(metav1.ListOptions{ResourceVersion: currResourceVersion})
if err != nil {
return nil, err
}
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout)
defer cancel()
evt, err := watchtools.UntilWithoutRetry(ctx, watchInterface, remainingConditions...)
if err == watchtools.ErrWatchClosed {
// present a consistent error interface to callers
err = wait.ErrWaitTimeout
}
return evt, err
}

View File

@@ -86,7 +86,7 @@ func NewSharedIndexInformer(lw ListerWatcher, objType runtime.Object, defaultEve
resyncCheckPeriod: defaultEventHandlerResyncPeriod,
defaultEventHandlerResyncPeriod: defaultEventHandlerResyncPeriod,
cacheMutationDetector: NewCacheMutationDetector(fmt.Sprintf("%T", objType)),
clock: realClock,
clock: realClock,
}
return sharedIndexInformer
}

View File

@@ -15,4 +15,5 @@ limitations under the License.
*/
// +k8s:deepcopy-gen=package
package api

View File

@@ -29,6 +29,8 @@ import (
func init() {
sDec, _ := base64.StdEncoding.DecodeString("REDACTED+")
redactedBytes = []byte(string(sDec))
sDec, _ = base64.StdEncoding.DecodeString("DATA+OMITTED")
dataOmittedBytes = []byte(string(sDec))
}
// IsConfigEmpty returns true if the config is empty.
@@ -79,7 +81,10 @@ func MinifyConfig(config *Config) error {
return nil
}
var redactedBytes []byte
var (
redactedBytes []byte
dataOmittedBytes []byte
)
// Flatten redacts raw data entries from the config object for a human-readable view.
func ShortenConfig(config *Config) {
@@ -97,7 +102,7 @@ func ShortenConfig(config *Config) {
}
for key, cluster := range config.Clusters {
if len(cluster.CertificateAuthorityData) > 0 {
cluster.CertificateAuthorityData = redactedBytes
cluster.CertificateAuthorityData = dataOmittedBytes
}
config.Clusters[key] = cluster
}

View File

@@ -229,7 +229,7 @@ func Example_minifyAndShorten() {
// clusters:
// cow-cluster:
// LocationOfOrigin: ""
// certificate-authority-data: REDACTED
// certificate-authority-data: DATA+OMITTED
// server: http://cow.org:8080
// contexts:
// federal-context:
@@ -276,14 +276,15 @@ func TestShortenSuccess(t *testing.T) {
}
redacted := string(redactedBytes)
dataOmitted := string(dataOmittedBytes)
if len(mutatingConfig.Clusters) != 2 {
t.Errorf("unexpected clusters: %v", mutatingConfig.Clusters)
}
if !reflect.DeepEqual(startingConfig.Clusters[unchangingCluster], mutatingConfig.Clusters[unchangingCluster]) {
t.Errorf("expected %v, got %v", startingConfig.Clusters[unchangingCluster], mutatingConfig.Clusters[unchangingCluster])
}
if string(mutatingConfig.Clusters[changingCluster].CertificateAuthorityData) != redacted {
t.Errorf("expected %v, got %v", redacted, string(mutatingConfig.Clusters[changingCluster].CertificateAuthorityData))
if string(mutatingConfig.Clusters[changingCluster].CertificateAuthorityData) != dataOmitted {
t.Errorf("expected %v, got %v", dataOmitted, string(mutatingConfig.Clusters[changingCluster].CertificateAuthorityData))
}
if len(mutatingConfig.AuthInfos) != 2 {

View File

@@ -15,4 +15,5 @@ limitations under the License.
*/
// +k8s:deepcopy-gen=package
package v1

View File

@@ -175,10 +175,6 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) {
// only try to read the auth information if we are secure
if restclient.IsConfigTransportTLS(*clientConfig) {
var err error
// mergo is a first write wins for map value and a last writing wins for interface values
// NOTE: This behavior changed with https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a.
// Our mergo.Merge version is older than this change.
var persister restclient.AuthProviderConfigPersister
if config.configAccess != nil {
authInfoName, _ := config.getAuthInfoName()
@@ -188,13 +184,13 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) {
if err != nil {
return nil, err
}
mergo.Merge(clientConfig, userAuthPartialConfig)
mergo.MergeWithOverwrite(clientConfig, userAuthPartialConfig)
serverAuthPartialConfig, err := getServerIdentificationPartialConfig(configAuthInfo, configClusterInfo)
if err != nil {
return nil, err
}
mergo.Merge(clientConfig, serverAuthPartialConfig)
mergo.MergeWithOverwrite(clientConfig, serverAuthPartialConfig)
}
return clientConfig, nil
@@ -214,7 +210,7 @@ func getServerIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo,
configClientConfig.CAFile = configClusterInfo.CertificateAuthority
configClientConfig.CAData = configClusterInfo.CertificateAuthorityData
configClientConfig.Insecure = configClusterInfo.InsecureSkipTLSVerify
mergo.Merge(mergedConfig, configClientConfig)
mergo.MergeWithOverwrite(mergedConfig, configClientConfig)
return mergedConfig, nil
}
@@ -279,8 +275,8 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI
promptedConfig := makeUserIdentificationConfig(*promptedAuthInfo)
previouslyMergedConfig := mergedConfig
mergedConfig = &restclient.Config{}
mergo.Merge(mergedConfig, promptedConfig)
mergo.Merge(mergedConfig, previouslyMergedConfig)
mergo.MergeWithOverwrite(mergedConfig, promptedConfig)
mergo.MergeWithOverwrite(mergedConfig, previouslyMergedConfig)
config.promptedCredentials.username = mergedConfig.Username
config.promptedCredentials.password = mergedConfig.Password
}
@@ -423,11 +419,11 @@ func (config *DirectClientConfig) getContext() (clientcmdapi.Context, error) {
mergedContext := clientcmdapi.NewContext()
if configContext, exists := contexts[contextName]; exists {
mergo.Merge(mergedContext, configContext)
mergo.MergeWithOverwrite(mergedContext, configContext)
} else if required {
return clientcmdapi.Context{}, fmt.Errorf("context %q does not exist", contextName)
}
mergo.Merge(mergedContext, config.overrides.Context)
mergo.MergeWithOverwrite(mergedContext, config.overrides.Context)
return *mergedContext, nil
}
@@ -439,11 +435,11 @@ func (config *DirectClientConfig) getAuthInfo() (clientcmdapi.AuthInfo, error) {
mergedAuthInfo := clientcmdapi.NewAuthInfo()
if configAuthInfo, exists := authInfos[authInfoName]; exists {
mergo.Merge(mergedAuthInfo, configAuthInfo)
mergo.MergeWithOverwrite(mergedAuthInfo, configAuthInfo)
} else if required {
return clientcmdapi.AuthInfo{}, fmt.Errorf("auth info %q does not exist", authInfoName)
}
mergo.Merge(mergedAuthInfo, config.overrides.AuthInfo)
mergo.MergeWithOverwrite(mergedAuthInfo, config.overrides.AuthInfo)
return *mergedAuthInfo, nil
}
@@ -454,13 +450,13 @@ func (config *DirectClientConfig) getCluster() (clientcmdapi.Cluster, error) {
clusterInfoName, required := config.getClusterName()
mergedClusterInfo := clientcmdapi.NewCluster()
mergo.Merge(mergedClusterInfo, config.overrides.ClusterDefaults)
mergo.MergeWithOverwrite(mergedClusterInfo, config.overrides.ClusterDefaults)
if configClusterInfo, exists := clusterInfos[clusterInfoName]; exists {
mergo.Merge(mergedClusterInfo, configClusterInfo)
mergo.MergeWithOverwrite(mergedClusterInfo, configClusterInfo)
} else if required {
return clientcmdapi.Cluster{}, fmt.Errorf("cluster %q does not exist", clusterInfoName)
}
mergo.Merge(mergedClusterInfo, config.overrides.ClusterInfo)
mergo.MergeWithOverwrite(mergedClusterInfo, config.overrides.ClusterInfo)
// An override of --insecure-skip-tls-verify=true and no accompanying CA/CA data should clear already-set CA/CA data
// otherwise, a kubeconfig containing a CA reference would return an error that "CA and insecure-skip-tls-verify couldn't both be set"
caLen := len(config.overrides.ClusterInfo.CertificateAuthority)

View File

@@ -28,21 +28,83 @@ import (
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
func TestOldMergoLib(t *testing.T) {
type T struct {
X string
func TestMergoSemantics(t *testing.T) {
type U struct {
A string
B int64
}
dst := T{X: "one"}
src := T{X: "two"}
mergo.Merge(&dst, &src)
if dst.X != "two" {
// mergo.Merge changed in an incompatible way with
//
// https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a
//
// We have to stay with the old version which still does eager
// copying from src to dst in structs.
t.Errorf("mergo.Merge library found with incompatible, new behavior")
type T struct {
S []string
X string
Y int64
U U
}
var testDataStruct = []struct {
dst T
src T
expected T
}{
{
dst: T{X: "one"},
src: T{X: "two"},
expected: T{X: "two"},
},
{
dst: T{X: "one", Y: 5, U: U{A: "four", B: 6}},
src: T{X: "two", U: U{A: "three", B: 4}},
expected: T{X: "two", Y: 5, U: U{A: "three", B: 4}},
},
{
dst: T{S: []string{"test3", "test4", "test5"}},
src: T{S: []string{"test1", "test2", "test3"}},
expected: T{S: []string{"test1", "test2", "test3"}},
},
}
for _, data := range testDataStruct {
err := mergo.MergeWithOverwrite(&data.dst, &data.src)
if err != nil {
t.Errorf("error while merging: %s", err)
}
if !reflect.DeepEqual(data.dst, data.expected) {
// The mergo library has previously changed in a an incompatible way.
// example:
//
// https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a
//
// This test verifies that the semantics of the merge are what we expect.
// If they are not, the mergo library may have been updated and broken
// unexpectedly.
t.Errorf("mergo.MergeWithOverwrite did not provide expected output: %+v doesn't match %+v", data.dst, data.expected)
}
}
var testDataMap = []struct {
dst map[string]int
src map[string]int
expected map[string]int
}{
{
dst: map[string]int{"rsc": 6543, "r": 2138, "gri": 1908, "adg": 912, "prt": 22},
src: map[string]int{"rsc": 3711, "r": 2138, "gri": 1908, "adg": 912},
expected: map[string]int{"rsc": 3711, "r": 2138, "gri": 1908, "adg": 912, "prt": 22},
},
}
for _, data := range testDataMap {
err := mergo.MergeWithOverwrite(&data.dst, &data.src)
if err != nil {
t.Errorf("error while merging: %s", err)
}
if !reflect.DeepEqual(data.dst, data.expected) {
// The mergo library has previously changed in a an incompatible way.
// example:
//
// https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a
//
// This test verifies that the semantics of the merge are what we expect.
// If they are not, the mergo library may have been updated and broken
// unexpectedly.
t.Errorf("mergo.MergeWithOverwrite did not provide expected output: %+v doesn't match %+v", data.dst, data.expected)
}
}
}
@@ -132,9 +194,17 @@ func TestModifyContext(t *testing.T) {
"clean": true,
}
tempPath, err := ioutil.TempFile("", "testclientcmd-")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer os.Remove(tempPath.Name())
pathOptions := NewDefaultPathOptions()
config := createValidTestConfig()
pathOptions.GlobalFile = tempPath.Name()
// define new context and assign it - our path options config
config.Contexts["updated"] = &clientcmdapi.Context{
Cluster: "updated",
@@ -173,7 +243,7 @@ func TestCertificateData(t *testing.T) {
config := clientcmdapi.NewConfig()
config.Clusters["clean"] = &clientcmdapi.Cluster{
Server: "https://localhost:8443",
Server: "https://localhost:8443",
CertificateAuthorityData: caData,
}
config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{
@@ -566,3 +636,46 @@ func TestNamespaceOverride(t *testing.T) {
matchStringArg("foo", ns, t)
}
func TestAuthConfigMerge(t *testing.T) {
content := `
apiVersion: v1
clusters:
- cluster:
server: https://localhost:8080
name: foo-cluster
contexts:
- context:
cluster: foo-cluster
user: foo-user
namespace: bar
name: foo-context
current-context: foo-context
kind: Config
users:
- name: foo-user
user:
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
args:
- arg-1
- arg-2
command: foo-command
`
tmpfile, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
t.Error(err)
}
defer os.Remove(tmpfile.Name())
if err := ioutil.WriteFile(tmpfile.Name(), []byte(content), 0666); err != nil {
t.Error(err)
}
config, err := BuildConfigFromFlags("", tmpfile.Name())
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(config.ExecProvider.Args, []string{"arg-1", "arg-2"}) {
t.Errorf("Got args %v when they should be %v\n", config.ExecProvider.Args, []string{"arg-1", "arg-2"})
}
}

View File

@@ -211,7 +211,7 @@ func (rules *ClientConfigLoadingRules) Load() (*clientcmdapi.Config, error) {
mapConfig := clientcmdapi.NewConfig()
for _, kubeconfig := range kubeconfigs {
mergo.Merge(mapConfig, kubeconfig)
mergo.MergeWithOverwrite(mapConfig, kubeconfig)
}
// merge all of the struct values in the reverse order so that priority is given correctly
@@ -219,14 +219,14 @@ func (rules *ClientConfigLoadingRules) Load() (*clientcmdapi.Config, error) {
nonMapConfig := clientcmdapi.NewConfig()
for i := len(kubeconfigs) - 1; i >= 0; i-- {
kubeconfig := kubeconfigs[i]
mergo.Merge(nonMapConfig, kubeconfig)
mergo.MergeWithOverwrite(nonMapConfig, kubeconfig)
}
// since values are overwritten, but maps values are not, we can merge the non-map config on top of the map config and
// get the values we expect.
config := clientcmdapi.NewConfig()
mergo.Merge(config, mapConfig)
mergo.Merge(config, nonMapConfig)
mergo.MergeWithOverwrite(config, mapConfig)
mergo.MergeWithOverwrite(config, nonMapConfig)
if rules.ResolvePaths() {
if err := ResolveLocalPaths(config); err != nil {

View File

@@ -80,7 +80,7 @@ func (cml *ConfigMapLock) Create(ler LeaderElectionRecord) error {
// Update will update an existing annotation on a given resource.
func (cml *ConfigMapLock) Update(ler LeaderElectionRecord) error {
if cml.cm == nil {
return errors.New("endpoint not initialized, call get or create first")
return errors.New("configmap not initialized, call get or create first")
}
recordBytes, err := json.Marshal(ler)
if err != nil {

View File

@@ -39,8 +39,9 @@ const PortForwardProtocolV1Name = "portforward.k8s.io"
// PortForwarder knows how to listen for local connections and forward them to
// a remote pod via an upgraded HTTP request.
type PortForwarder struct {
ports []ForwardedPort
stopChan <-chan struct{}
addresses []listenAddress
ports []ForwardedPort
stopChan <-chan struct{}
dialer httpstream.Dialer
streamConn httpstream.Connection
@@ -110,8 +111,52 @@ func parsePorts(ports []string) ([]ForwardedPort, error) {
return forwards, nil
}
// New creates a new PortForwarder.
type listenAddress struct {
address string
protocol string
failureMode string
}
func parseAddresses(addressesToParse []string) ([]listenAddress, error) {
var addresses []listenAddress
parsed := make(map[string]listenAddress)
for _, address := range addressesToParse {
if address == "localhost" {
ip := listenAddress{address: "127.0.0.1", protocol: "tcp4", failureMode: "all"}
parsed[ip.address] = ip
ip = listenAddress{address: "::1", protocol: "tcp6", failureMode: "all"}
parsed[ip.address] = ip
} else if net.ParseIP(address).To4() != nil {
parsed[address] = listenAddress{address: address, protocol: "tcp4", failureMode: "any"}
} else if net.ParseIP(address) != nil {
parsed[address] = listenAddress{address: address, protocol: "tcp6", failureMode: "any"}
} else {
return nil, fmt.Errorf("%s is not a valid IP", address)
}
}
addresses = make([]listenAddress, len(parsed))
id := 0
for _, v := range parsed {
addresses[id] = v
id++
}
return addresses, nil
}
// New creates a new PortForwarder with localhost listen addresses.
func New(dialer httpstream.Dialer, ports []string, stopChan <-chan struct{}, readyChan chan struct{}, out, errOut io.Writer) (*PortForwarder, error) {
return NewOnAddresses(dialer, []string{"localhost"}, ports, stopChan, readyChan, out, errOut)
}
// NewOnAddresses creates a new PortForwarder with custom listen addresses.
func NewOnAddresses(dialer httpstream.Dialer, addresses []string, ports []string, stopChan <-chan struct{}, readyChan chan struct{}, out, errOut io.Writer) (*PortForwarder, error) {
if len(addresses) == 0 {
return nil, errors.New("You must specify at least 1 address")
}
parsedAddresses, err := parseAddresses(addresses)
if err != nil {
return nil, err
}
if len(ports) == 0 {
return nil, errors.New("You must specify at least 1 port")
}
@@ -120,12 +165,13 @@ func New(dialer httpstream.Dialer, ports []string, stopChan <-chan struct{}, rea
return nil, err
}
return &PortForwarder{
dialer: dialer,
ports: parsedPorts,
stopChan: stopChan,
Ready: readyChan,
out: out,
errOut: errOut,
dialer: dialer,
addresses: parsedAddresses,
ports: parsedPorts,
stopChan: stopChan,
Ready: readyChan,
out: out,
errOut: errOut,
}, nil
}
@@ -181,13 +227,26 @@ func (pf *PortForwarder) forward() error {
return nil
}
// listenOnPort delegates tcp4 and tcp6 listener creation and waits for connections on both of these addresses.
// If both listener creation fail, an error is raised.
// listenOnPort delegates listener creation and waits for connections on requested bind addresses.
// An error is raised based on address groups (default and localhost) and their failure modes
func (pf *PortForwarder) listenOnPort(port *ForwardedPort) error {
errTcp4 := pf.listenOnPortAndAddress(port, "tcp4", "127.0.0.1")
errTcp6 := pf.listenOnPortAndAddress(port, "tcp6", "::1")
if errTcp4 != nil && errTcp6 != nil {
return fmt.Errorf("All listeners failed to create with the following errors: %s, %s", errTcp4, errTcp6)
var errors []error
failCounters := make(map[string]int, 2)
successCounters := make(map[string]int, 2)
for _, addr := range pf.addresses {
err := pf.listenOnPortAndAddress(port, addr.protocol, addr.address)
if err != nil {
errors = append(errors, err)
failCounters[addr.failureMode]++
} else {
successCounters[addr.failureMode]++
}
}
if successCounters["all"] == 0 && failCounters["all"] > 0 {
return fmt.Errorf("%s: %v", "Listeners failed to create with the following errors", errors)
}
if failCounters["any"] > 0 {
return fmt.Errorf("%s: %v", "Listeners failed to create with the following errors", errors)
}
return nil
}
@@ -216,6 +275,7 @@ func (pf *PortForwarder) getListener(protocol string, hostname string, port *For
localPortUInt, err := strconv.ParseUint(localPort, 10, 16)
if err != nil {
fmt.Fprintf(pf.out, "Failed to forward from %s:%d -> %d\n", hostname, localPortUInt, port.Remote)
return nil, fmt.Errorf("Error parsing local port: %s from %s (%s)", err, listenerAddress, host)
}
port.Local = uint16(localPortUInt)
@@ -340,3 +400,20 @@ func (pf *PortForwarder) Close() {
}
}
}
// GetPorts will return the ports that were forwarded; this can be used to
// retrieve the locally-bound port in cases where the input was port 0. This
// function will signal an error if the Ready channel is nil or if the
// listeners are not ready yet; this function will succeed after the Ready
// channel has been closed.
func (pf *PortForwarder) GetPorts() ([]ForwardedPort, error) {
if pf.Ready == nil {
return nil, fmt.Errorf("no Ready channel provided")
}
select {
case <-pf.Ready:
return pf.ports, nil
default:
return nil, fmt.Errorf("listeners not ready")
}
}

View File

@@ -20,6 +20,7 @@ import (
"net"
"os"
"reflect"
"sort"
"strings"
"testing"
@@ -40,23 +41,52 @@ func (d *fakeDialer) Dial(protocols ...string) (httpstream.Connection, string, e
func TestParsePortsAndNew(t *testing.T) {
tests := []struct {
input []string
expected []ForwardedPort
expectParseError bool
expectNewError bool
input []string
addresses []string
expectedPorts []ForwardedPort
expectedAddresses []listenAddress
expectPortParseError bool
expectAddressParseError bool
expectNewError bool
}{
{input: []string{}, expectNewError: true},
{input: []string{"a"}, expectParseError: true, expectNewError: true},
{input: []string{":a"}, expectParseError: true, expectNewError: true},
{input: []string{"-1"}, expectParseError: true, expectNewError: true},
{input: []string{"65536"}, expectParseError: true, expectNewError: true},
{input: []string{"0"}, expectParseError: true, expectNewError: true},
{input: []string{"0:0"}, expectParseError: true, expectNewError: true},
{input: []string{"a:5000"}, expectParseError: true, expectNewError: true},
{input: []string{"5000:a"}, expectParseError: true, expectNewError: true},
{input: []string{"a"}, expectPortParseError: true, expectAddressParseError: false, expectNewError: true},
{input: []string{":a"}, expectPortParseError: true, expectAddressParseError: false, expectNewError: true},
{input: []string{"-1"}, expectPortParseError: true, expectAddressParseError: false, expectNewError: true},
{input: []string{"65536"}, expectPortParseError: true, expectAddressParseError: false, expectNewError: true},
{input: []string{"0"}, expectPortParseError: true, expectAddressParseError: false, expectNewError: true},
{input: []string{"0:0"}, expectPortParseError: true, expectAddressParseError: false, expectNewError: true},
{input: []string{"a:5000"}, expectPortParseError: true, expectAddressParseError: false, expectNewError: true},
{input: []string{"5000:a"}, expectPortParseError: true, expectAddressParseError: false, expectNewError: true},
{input: []string{"5000:5000"}, addresses: []string{"127.0.0.257"}, expectPortParseError: false, expectAddressParseError: true, expectNewError: true},
{input: []string{"5000:5000"}, addresses: []string{"::g"}, expectPortParseError: false, expectAddressParseError: true, expectNewError: true},
{input: []string{"5000:5000"}, addresses: []string{"domain.invalid"}, expectPortParseError: false, expectAddressParseError: true, expectNewError: true},
{
input: []string{"5000", "5000:5000", "8888:5000", "5000:8888", ":5000", "0:5000"},
expected: []ForwardedPort{
input: []string{"5000:5000"},
addresses: []string{"localhost"},
expectedPorts: []ForwardedPort{
{5000, 5000},
},
expectedAddresses: []listenAddress{
{protocol: "tcp4", address: "127.0.0.1", failureMode: "all"},
{protocol: "tcp6", address: "::1", failureMode: "all"},
},
},
{
input: []string{"5000:5000"},
addresses: []string{"localhost", "127.0.0.1"},
expectedPorts: []ForwardedPort{
{5000, 5000},
},
expectedAddresses: []listenAddress{
{protocol: "tcp4", address: "127.0.0.1", failureMode: "any"},
{protocol: "tcp6", address: "::1", failureMode: "all"},
},
},
{
input: []string{"5000", "5000:5000", "8888:5000", "5000:8888", ":5000", "0:5000"},
addresses: []string{"127.0.0.1", "::1"},
expectedPorts: []ForwardedPort{
{5000, 5000},
{5000, 5000},
{8888, 5000},
@@ -64,34 +94,63 @@ func TestParsePortsAndNew(t *testing.T) {
{0, 5000},
{0, 5000},
},
expectedAddresses: []listenAddress{
{protocol: "tcp4", address: "127.0.0.1", failureMode: "any"},
{protocol: "tcp6", address: "::1", failureMode: "any"},
},
},
}
for i, test := range tests {
parsed, err := parsePorts(test.input)
parsedPorts, err := parsePorts(test.input)
haveError := err != nil
if e, a := test.expectParseError, haveError; e != a {
if e, a := test.expectPortParseError, haveError; e != a {
t.Fatalf("%d: parsePorts: error expected=%t, got %t: %s", i, e, a, err)
}
// default to localhost
if len(test.addresses) == 0 && len(test.expectedAddresses) == 0 {
test.addresses = []string{"localhost"}
test.expectedAddresses = []listenAddress{{protocol: "tcp4", address: "127.0.0.1"}, {protocol: "tcp6", address: "::1"}}
}
// assert address parser
parsedAddresses, err := parseAddresses(test.addresses)
haveError = err != nil
if e, a := test.expectAddressParseError, haveError; e != a {
t.Fatalf("%d: parseAddresses: error expected=%t, got %t: %s", i, e, a, err)
}
dialer := &fakeDialer{}
expectedStopChan := make(chan struct{})
readyChan := make(chan struct{})
pf, err := New(dialer, test.input, expectedStopChan, readyChan, os.Stdout, os.Stderr)
var pf *PortForwarder
if len(test.addresses) > 0 {
pf, err = NewOnAddresses(dialer, test.addresses, test.input, expectedStopChan, readyChan, os.Stdout, os.Stderr)
} else {
pf, err = New(dialer, test.input, expectedStopChan, readyChan, os.Stdout, os.Stderr)
}
haveError = err != nil
if e, a := test.expectNewError, haveError; e != a {
t.Fatalf("%d: New: error expected=%t, got %t: %s", i, e, a, err)
}
if test.expectParseError || test.expectNewError {
if test.expectPortParseError || test.expectAddressParseError || test.expectNewError {
continue
}
for pi, expectedPort := range test.expected {
if e, a := expectedPort.Local, parsed[pi].Local; e != a {
sort.Slice(test.expectedAddresses, func(i, j int) bool { return test.expectedAddresses[i].address < test.expectedAddresses[j].address })
sort.Slice(parsedAddresses, func(i, j int) bool { return parsedAddresses[i].address < parsedAddresses[j].address })
if !reflect.DeepEqual(test.expectedAddresses, parsedAddresses) {
t.Fatalf("%d: expectedAddresses: %v, got: %v", i, test.expectedAddresses, parsedAddresses)
}
for pi, expectedPort := range test.expectedPorts {
if e, a := expectedPort.Local, parsedPorts[pi].Local; e != a {
t.Fatalf("%d: local expected: %d, got: %d", i, e, a)
}
if e, a := expectedPort.Remote, parsed[pi].Remote; e != a {
if e, a := expectedPort.Remote, parsedPorts[pi].Remote; e != a {
t.Fatalf("%d: remote expected: %d, got: %d", i, e, a)
}
}
@@ -99,8 +158,17 @@ func TestParsePortsAndNew(t *testing.T) {
if dialer.dialed {
t.Fatalf("%d: expected not dialed", i)
}
if e, a := test.expected, pf.ports; !reflect.DeepEqual(e, a) {
t.Fatalf("%d: ports: expected %#v, got %#v", i, e, a)
if _, portErr := pf.GetPorts(); portErr == nil {
t.Fatalf("%d: GetPorts: error expected but got nil", i)
}
// mock-signal the Ready channel
close(readyChan)
if ports, portErr := pf.GetPorts(); portErr != nil {
t.Fatalf("%d: GetPorts: unable to retrieve ports: %s", i, portErr)
} else if !reflect.DeepEqual(test.expectedPorts, ports) {
t.Fatalf("%d: ports: expected %#v, got %#v", i, test.expectedPorts, ports)
}
if e, a := expectedStopChan, pf.stopChan; e != a {
t.Fatalf("%d: stopChan: expected %#v, got %#v", i, e, a)

View File

@@ -84,11 +84,6 @@ func getSpamKey(event *v1.Event) string {
// EventFilterFunc is a function that returns true if the event should be skipped
type EventFilterFunc func(event *v1.Event) bool
// DefaultEventFilterFunc returns false for all incoming events
func DefaultEventFilterFunc(event *v1.Event) bool {
return false
}
// EventSourceObjectSpamFilter is responsible for throttling
// the amount of events a source and object can produce.
type EventSourceObjectSpamFilter struct {

View File

@@ -127,14 +127,6 @@ func validateEvent(messagePrefix string, actualEvent *v1.Event, expectedEvent *v
return actualEvent, nil
}
// TestDefaultEventFilterFunc ensures that no events are filtered
func TestDefaultEventFilterFunc(t *testing.T) {
event := makeEvent("end-of-world", "it was fun", makeObjectReference("Pod", "pod1", "other"))
if DefaultEventFilterFunc(&event) {
t.Fatalf("DefaultEventFilterFunc should always return false")
}
}
// TestEventAggregatorByReasonFunc ensures that two events are aggregated if they vary only by event.message
func TestEventAggregatorByReasonFunc(t *testing.T) {
event1 := makeEvent("end-of-world", "it was fun", makeObjectReference("Pod", "pod1", "other"))

View File

@@ -0,0 +1,114 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package watch
import (
"sync"
"sync/atomic"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
)
func newTicketer() *ticketer {
return &ticketer{
cond: sync.NewCond(&sync.Mutex{}),
}
}
type ticketer struct {
counter uint64
cond *sync.Cond
current uint64
}
func (t *ticketer) GetTicket() uint64 {
// -1 to start from 0
return atomic.AddUint64(&t.counter, 1) - 1
}
func (t *ticketer) WaitForTicket(ticket uint64, f func()) {
t.cond.L.Lock()
defer t.cond.L.Unlock()
for ticket != t.current {
t.cond.Wait()
}
f()
t.current++
t.cond.Broadcast()
}
// NewIndexerInformerWatcher will create an IndexerInformer and wrap it into watch.Interface
// so you can use it anywhere where you'd have used a regular Watcher returned from Watch method.
func NewIndexerInformerWatcher(lw cache.ListerWatcher, objType runtime.Object) (cache.Indexer, cache.Controller, watch.Interface) {
ch := make(chan watch.Event)
w := watch.NewProxyWatcher(ch)
t := newTicketer()
indexer, informer := cache.NewIndexerInformer(lw, objType, 0, cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
go t.WaitForTicket(t.GetTicket(), func() {
select {
case ch <- watch.Event{
Type: watch.Added,
Object: obj.(runtime.Object),
}:
case <-w.StopChan():
}
})
},
UpdateFunc: func(old, new interface{}) {
go t.WaitForTicket(t.GetTicket(), func() {
select {
case ch <- watch.Event{
Type: watch.Modified,
Object: new.(runtime.Object),
}:
case <-w.StopChan():
}
})
},
DeleteFunc: func(obj interface{}) {
go t.WaitForTicket(t.GetTicket(), func() {
staleObj, stale := obj.(cache.DeletedFinalStateUnknown)
if stale {
// We have no means of passing the additional information down using watch API based on watch.Event
// but the caller can filter such objects by checking if metadata.deletionTimestamp is set
obj = staleObj
}
select {
case ch <- watch.Event{
Type: watch.Deleted,
Object: obj.(runtime.Object),
}:
case <-w.StopChan():
}
})
},
}, cache.Indexers{})
go func() {
informer.Run(w.StopChan())
}()
return indexer, informer, w
}

View File

@@ -0,0 +1,236 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package watch
import (
"math/rand"
"reflect"
"sort"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/diff"
"k8s.io/apimachinery/pkg/watch"
fakeclientset "k8s.io/client-go/kubernetes/fake"
testcore "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
)
type byEventTypeAndName []watch.Event
func (a byEventTypeAndName) Len() int { return len(a) }
func (a byEventTypeAndName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byEventTypeAndName) Less(i, j int) bool {
if a[i].Type < a[j].Type {
return true
}
if a[i].Type > a[j].Type {
return false
}
return a[i].Object.(*corev1.Secret).Name < a[j].Object.(*corev1.Secret).Name
}
func TestTicketer(t *testing.T) {
tg := newTicketer()
const numTickets = 100 // current golang limit for race detector is 8192 simultaneously alive goroutines
var tickets []uint64
for i := 0; i < numTickets; i++ {
ticket := tg.GetTicket()
tickets = append(tickets, ticket)
exp, got := uint64(i), ticket
if got != exp {
t.Fatalf("expected ticket %d, got %d", exp, got)
}
}
// shuffle tickets
rand.Shuffle(len(tickets), func(i, j int) {
tickets[i], tickets[j] = tickets[j], tickets[i]
})
res := make(chan uint64, len(tickets))
for _, ticket := range tickets {
go func(ticket uint64) {
time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
tg.WaitForTicket(ticket, func() {
res <- ticket
})
}(ticket)
}
for i := 0; i < numTickets; i++ {
exp, got := uint64(i), <-res
if got != exp {
t.Fatalf("expected ticket %d, got %d", exp, got)
}
}
}
func TestNewInformerWatcher(t *testing.T) {
// Make sure there are no 2 same types of events on a secret with the same name or that might be flaky.
tt := []struct {
name string
objects []runtime.Object
events []watch.Event
}{
{
name: "basic test",
objects: []runtime.Object{
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1",
},
StringData: map[string]string{
"foo-1": "initial",
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-2",
},
StringData: map[string]string{
"foo-2": "initial",
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-3",
},
StringData: map[string]string{
"foo-3": "initial",
},
},
},
events: []watch.Event{
{
Type: watch.Added,
Object: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-4",
},
StringData: map[string]string{
"foo-4": "initial",
},
},
},
{
Type: watch.Modified,
Object: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-2",
},
StringData: map[string]string{
"foo-2": "new",
},
},
},
{
Type: watch.Deleted,
Object: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-3",
},
},
},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
var expected []watch.Event
for _, o := range tc.objects {
expected = append(expected, watch.Event{
Type: watch.Added,
Object: o.DeepCopyObject(),
})
}
for _, e := range tc.events {
expected = append(expected, *e.DeepCopy())
}
fake := fakeclientset.NewSimpleClientset(tc.objects...)
fakeWatch := watch.NewFakeWithChanSize(len(tc.events), false)
fake.PrependWatchReactor("secrets", testcore.DefaultWatchReactor(fakeWatch, nil))
for _, e := range tc.events {
fakeWatch.Action(e.Type, e.Object)
}
lw := &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return fake.Core().Secrets("").List(options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return fake.Core().Secrets("").Watch(options)
},
}
_, _, w := NewIndexerInformerWatcher(lw, &corev1.Secret{})
var result []watch.Event
loop:
for {
var event watch.Event
var ok bool
select {
case event, ok = <-w.ResultChan():
if !ok {
t.Errorf("Failed to read event: channel is already closed!")
return
}
result = append(result, *event.DeepCopy())
case <-time.After(time.Second * 1):
// All the events are buffered -> this means we are done
// Also the one sec will make sure that we would detect RetryWatcher's incorrect behaviour after last event
break loop
}
}
// Informers don't guarantee event order so we need to sort these arrays to compare them
sort.Sort(byEventTypeAndName(expected))
sort.Sort(byEventTypeAndName(result))
if !reflect.DeepEqual(expected, result) {
t.Error(spew.Errorf("\nexpected: %#v,\ngot: %#v,\ndiff: %s", expected, result, diff.ObjectReflectDiff(expected, result)))
return
}
// Fill in some data to test watch closing while there are some events to be read
for _, e := range tc.events {
fakeWatch.Action(e.Type, e.Object)
}
// Stop before reading all the data to make sure the informer can deal with closed channel
w.Stop()
// Wait a bit to see if the informer won't panic
// TODO: Try to figure out a more reliable mechanism than time.Sleep (https://github.com/kubernetes/kubernetes/pull/50102/files#r184716591)
time.Sleep(1 * time.Second)
})
}
}

View File

@@ -19,13 +19,22 @@ package watch
import (
"context"
"errors"
"fmt"
"time"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
)
// PreconditionFunc returns true if the condition has been reached, false if it has not been reached yet,
// or an error if the condition failed or detected an error state.
type PreconditionFunc func(store cache.Store) (bool, error)
// ConditionFunc returns true if the condition has been reached, false if it has not been reached yet,
// or an error if the condition cannot be checked and should terminate. In general, it is better to define
// level driven conditions over edge driven conditions (pod has ready=true, vs pod modified and ready changed
@@ -86,6 +95,42 @@ func UntilWithoutRetry(ctx context.Context, watcher watch.Interface, conditions
return lastEvent, nil
}
// UntilWithSync creates an informer from lw, optionally checks precondition when the store is synced,
// and watches the output until each provided condition succeeds, in a way that is identical
// to function UntilWithoutRetry. (See above.)
// UntilWithSync can deal with all errors like API timeout, lost connections and 'Resource version too old'.
// It is the only function that can recover from 'Resource version too old', Until and UntilWithoutRetry will
// just fail in that case. On the other hand it can't provide you with guarantees as strong as using simple
// Watch method with Until. It can skip some intermediate events in case of watch function failing but it will
// re-list to recover and you always get an event, if there has been a change, after recovery.
// Also with the current implementation based on DeltaFIFO, order of the events you receive is guaranteed only for
// particular object, not between more of them even it's the same resource.
// The most frequent usage would be a command that needs to watch the "state of the world" and should't fail, like:
// waiting for object reaching a state, "small" controllers, ...
func UntilWithSync(ctx context.Context, lw cache.ListerWatcher, objType runtime.Object, precondition PreconditionFunc, conditions ...ConditionFunc) (*watch.Event, error) {
indexer, informer, watcher := NewIndexerInformerWatcher(lw, objType)
// Proxy watcher can be stopped multiple times so it's fine to use defer here to cover alternative branches and
// let UntilWithoutRetry to stop it
defer watcher.Stop()
if precondition != nil {
if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) {
return nil, fmt.Errorf("UntilWithSync: unable to sync caches: %v", ctx.Err())
}
done, err := precondition(indexer)
if err != nil {
return nil, err
}
if done {
return nil, nil
}
}
return UntilWithoutRetry(ctx, watcher, conditions...)
}
// ContextWithOptionalTimeout wraps context.WithTimeout and handles infinite timeouts expressed as 0 duration.
func ContextWithOptionalTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
if timeout < 0 {
@@ -100,3 +145,81 @@ func ContextWithOptionalTimeout(parent context.Context, timeout time.Duration) (
return context.WithTimeout(parent, timeout)
}
// ListWatchUntil checks the provided conditions against the items returned by the list watcher, returning wait.ErrWaitTimeout
// if timeout is exceeded without all conditions returning true, or an error if an error occurs.
// TODO: check for watch expired error and retry watch from latest point? Same issue exists for Until.
// TODO: remove when no longer used
//
// Deprecated: Use UntilWithSync instead.
func ListWatchUntil(timeout time.Duration, lw cache.ListerWatcher, conditions ...ConditionFunc) (*watch.Event, error) {
if len(conditions) == 0 {
return nil, nil
}
list, err := lw.List(metav1.ListOptions{})
if err != nil {
return nil, err
}
initialItems, err := meta.ExtractList(list)
if err != nil {
return nil, err
}
// use the initial items as simulated "adds"
var lastEvent *watch.Event
currIndex := 0
passedConditions := 0
for _, condition := range conditions {
// check the next condition against the previous event and short circuit waiting for the next watch
if lastEvent != nil {
done, err := condition(*lastEvent)
if err != nil {
return lastEvent, err
}
if done {
passedConditions = passedConditions + 1
continue
}
}
ConditionSucceeded:
for currIndex < len(initialItems) {
lastEvent = &watch.Event{Type: watch.Added, Object: initialItems[currIndex]}
currIndex++
done, err := condition(*lastEvent)
if err != nil {
return lastEvent, err
}
if done {
passedConditions = passedConditions + 1
break ConditionSucceeded
}
}
}
if passedConditions == len(conditions) {
return lastEvent, nil
}
remainingConditions := conditions[passedConditions:]
metaObj, err := meta.ListAccessor(list)
if err != nil {
return nil, err
}
currResourceVersion := metaObj.GetResourceVersion()
watchInterface, err := lw.Watch(metav1.ListOptions{ResourceVersion: currResourceVersion})
if err != nil {
return nil, err
}
ctx, cancel := ContextWithOptionalTimeout(context.Background(), timeout)
defer cancel()
evt, err := UntilWithoutRetry(ctx, watchInterface, remainingConditions...)
if err == ErrWatchClosed {
// present a consistent error interface to callers
err = wait.ErrWaitTimeout
}
return evt, err
}

View File

@@ -19,14 +19,19 @@ package watch
import (
"context"
"errors"
"reflect"
"strings"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
fakeclient "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/cache"
)
type fakePod struct {
@@ -172,3 +177,127 @@ func TestUntilErrorCondition(t *testing.T) {
t.Fatalf("expected %q in error string, got %q", expected, err.Error())
}
}
func TestUntilWithSync(t *testing.T) {
// FIXME: test preconditions
tt := []struct {
name string
lw *cache.ListWatch
preconditionFunc PreconditionFunc
conditionFunc ConditionFunc
expectedErr error
expectedEvent *watch.Event
}{
{
name: "doesn't wait for sync with no precondition",
lw: &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
select {}
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
select {}
},
},
preconditionFunc: nil,
conditionFunc: func(e watch.Event) (bool, error) {
return true, nil
},
expectedErr: errors.New("timed out waiting for the condition"),
expectedEvent: nil,
},
{
name: "waits indefinitely with precondition if it can't sync",
lw: &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
select {}
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
select {}
},
},
preconditionFunc: func(store cache.Store) (bool, error) {
return true, nil
},
conditionFunc: func(e watch.Event) (bool, error) {
return true, nil
},
expectedErr: errors.New("UntilWithSync: unable to sync caches: context deadline exceeded"),
expectedEvent: nil,
},
{
name: "precondition can stop the loop",
lw: func() *cache.ListWatch {
fakeclient := fakeclient.NewSimpleClientset(&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "first"}})
return &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return fakeclient.CoreV1().Secrets("").List(options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return fakeclient.CoreV1().Secrets("").Watch(options)
},
}
}(),
preconditionFunc: func(store cache.Store) (bool, error) {
_, exists, err := store.Get(&metav1.ObjectMeta{Namespace: "", Name: "first"})
if err != nil {
return true, err
}
if exists {
return true, nil
}
return false, nil
},
conditionFunc: func(e watch.Event) (bool, error) {
return true, errors.New("should never reach this")
},
expectedErr: nil,
expectedEvent: nil,
},
{
name: "precondition lets it proceed to regular condition",
lw: func() *cache.ListWatch {
fakeclient := fakeclient.NewSimpleClientset(&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "first"}})
return &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return fakeclient.CoreV1().Secrets("").List(options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return fakeclient.CoreV1().Secrets("").Watch(options)
},
}
}(),
preconditionFunc: func(store cache.Store) (bool, error) {
return false, nil
},
conditionFunc: func(e watch.Event) (bool, error) {
if e.Type == watch.Added {
return true, nil
}
panic("no other events are expected")
},
expectedErr: nil,
expectedEvent: &watch.Event{Type: watch.Added, Object: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "first"}}},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
// Informer waits for caches to sync by polling in 100ms intervals,
// timeout needs to be reasonably higher
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
event, err := UntilWithSync(ctx, tc.lw, &corev1.Secret{}, tc.preconditionFunc, tc.conditionFunc)
if !reflect.DeepEqual(err, tc.expectedErr) {
t.Errorf("expected error %#v, got %#v", tc.expectedErr, err)
}
if !reflect.DeepEqual(event, tc.expectedEvent) {
t.Errorf("expected event %#v, got %#v", tc.expectedEvent, event)
}
})
}
}

View File

@@ -135,7 +135,7 @@ func TestImpersonationRoundTripper(t *testing.T) {
},
},
expected: map[string][]string{
ImpersonateUserHeader: {"user"},
ImpersonateUserHeader: {"user"},
ImpersonateUserExtraHeaderPrefix + `Test.example.com%2fthing.thing`: {"A", "a"},
},
},
@@ -219,11 +219,11 @@ func TestAuthProxyRoundTripper(t *testing.T) {
username: "user",
groups: []string{"groupA", "groupB"},
extra: map[string][]string{
"one": {"alpha", "bravo"},
"one": {"alpha", "bravo"},
"example.com/two%20three": {"charlie", "delta"},
},
expectedExtra: map[string][]string{
"one": {"alpha", "bravo"},
"one": {"alpha", "bravo"},
"example.com%2ftwo%2520three": {"charlie", "delta"},
},
},

View File

@@ -38,7 +38,7 @@ func RoundTripperFor(config *restclient.Config) (http.RoundTripper, Upgrader, er
if err != nil {
return nil, nil, err
}
upgradeRoundTripper := spdy.NewRoundTripper(tlsConfig, true)
upgradeRoundTripper := spdy.NewRoundTripper(tlsConfig, true, false)
wrapper, err := restclient.HTTPWrappersForConfig(config, upgradeRoundTripper)
if err != nil {
return nil, nil, err

View File

@@ -18,8 +18,10 @@ package cert
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/x509"
@@ -63,7 +65,7 @@ func NewPrivateKey() (*rsa.PrivateKey, error) {
}
// NewSelfSignedCACert creates a CA certificate
func NewSelfSignedCACert(cfg Config, key *rsa.PrivateKey) (*x509.Certificate, error) {
func NewSelfSignedCACert(cfg Config, key crypto.Signer) (*x509.Certificate, error) {
now := time.Now()
tmpl := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(0),
@@ -75,7 +77,7 @@ func NewSelfSignedCACert(cfg Config, key *rsa.PrivateKey) (*x509.Certificate, er
NotAfter: now.Add(duration365d * 10).UTC(),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
IsCA: true,
}
certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key)
@@ -86,8 +88,8 @@ func NewSelfSignedCACert(cfg Config, key *rsa.PrivateKey) (*x509.Certificate, er
}
// NewSignedCert creates a signed certificate using the given CA certificate and key
func NewSignedCert(cfg Config, key *rsa.PrivateKey, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, error) {
serial, err := cryptorand.Int(cryptorand.Reader, new(big.Int).SetInt64(math.MaxInt64))
func NewSignedCert(cfg Config, key crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer) (*x509.Certificate, error) {
serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))
if err != nil {
return nil, err
}
@@ -186,7 +188,7 @@ func GenerateSelfSignedCertKeyWithFixtures(host string, alternateIPs []net.IP, a
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
IsCA: true,
}
caDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey)

View File

@@ -21,7 +21,6 @@ import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -171,11 +170,9 @@ func (s *fileStore) Current() (*tls.Certificate, error) {
}
func loadFile(pairFile string) (*tls.Certificate, error) {
certBlock, keyBlock, err := loadCertKeyBlocks(pairFile)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(pem.EncodeToMemory(certBlock), pem.EncodeToMemory(keyBlock))
// LoadX509KeyPair knows how to parse combined cert and private key from
// the same file.
cert, err := tls.LoadX509KeyPair(pairFile, pairFile)
if err != nil {
return nil, fmt.Errorf("could not convert data from %q into cert/key pair: %v", pairFile, err)
}
@@ -187,22 +184,6 @@ func loadFile(pairFile string) (*tls.Certificate, error) {
return &cert, nil
}
func loadCertKeyBlocks(pairFile string) (cert *pem.Block, key *pem.Block, err error) {
data, err := ioutil.ReadFile(pairFile)
if err != nil {
return nil, nil, fmt.Errorf("could not load cert/key pair from %q: %v", pairFile, err)
}
certBlock, rest := pem.Decode(data)
if certBlock == nil {
return nil, nil, fmt.Errorf("could not decode the first block from %q from expected PEM format", pairFile)
}
keyBlock, _ := pem.Decode(rest)
if keyBlock == nil {
return nil, nil, fmt.Errorf("could not decode the second block from %q from expected PEM format", pairFile)
}
return certBlock, keyBlock, nil
}
func (s *fileStore) Update(certData, keyData []byte) (*tls.Certificate, error) {
ts := time.Now().Format("2006-01-02-15-04-05")
pemFilename := s.filename(ts)

View File

@@ -17,12 +17,11 @@ limitations under the License.
package certificate
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"testing"
"k8s.io/client-go/util/cert"
)
func TestUpdateSymlinkExistingFileError(t *testing.T) {
@@ -69,16 +68,16 @@ func TestUpdateSymlinkNewFileNotExist(t *testing.T) {
pairNamePrefix: "kubelet",
}
if err := s.updateSymlink(oldPairFile); err != nil {
t.Errorf("Got %v, wanted successful update of the symlink to point to %q", err, oldPairFile)
t.Errorf("Got error %v, wanted successful update of the symlink to point to %q", err, oldPairFile)
}
if _, err := os.Stat(oldPairFile); err != nil {
t.Errorf("Got %v, wanted file %q to be there.", oldPairFile, err)
t.Errorf("Got error %v, wanted file %q to be there.", err, oldPairFile)
}
currentPairFile := filepath.Join(dir, "kubelet-current.pem")
if fi, err := os.Lstat(currentPairFile); err != nil {
t.Errorf("Got %v, wanted file %q to be there", currentPairFile, err)
t.Errorf("Got error %v, wanted file %q to be there", err, currentPairFile)
} else if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
t.Errorf("Got %q not a symlink.", currentPairFile)
}
@@ -113,7 +112,7 @@ func TestUpdateSymlinkNoSymlink(t *testing.T) {
}
if _, err := os.Stat(pairFile); err != nil {
t.Errorf("Got error %v, wanted file %q to be there", pairFile, err)
t.Errorf("Got error %v, wanted file %q to be there", err, pairFile)
}
currentPairFile := filepath.Join(dir, "kubelet-current.pem")
if fi, err := os.Lstat(currentPairFile); err != nil {
@@ -178,96 +177,6 @@ func TestUpdateSymlinkReplaceExistingSymlink(t *testing.T) {
}
}
func TestLoadCertKeyBlocksNoFile(t *testing.T) {
dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks")
if err != nil {
t.Fatalf("Unable to create the test directory %q: %v", dir, err)
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
t.Errorf("Unable to clean up test directory %q: %v", dir, err)
}
}()
pairFile := filepath.Join(dir, "kubelet-pair.pem")
if _, _, err := loadCertKeyBlocks(pairFile); err == nil {
t.Errorf("Got no error, but expected %q not found.", pairFile)
}
}
func TestLoadCertKeyBlocksEmptyFile(t *testing.T) {
dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks")
if err != nil {
t.Fatalf("Unable to create the test directory %q: %v", dir, err)
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
t.Errorf("Unable to clean up test directory %q: %v", dir, err)
}
}()
pairFile := filepath.Join(dir, "kubelet-pair.pem")
if err := ioutil.WriteFile(pairFile, nil, 0600); err != nil {
t.Fatalf("Unable to create the file %q: %v", pairFile, err)
}
if _, _, err := loadCertKeyBlocks(pairFile); err == nil {
t.Errorf("Got no error, but expected %q not found.", pairFile)
}
}
func TestLoadCertKeyBlocksPartialFile(t *testing.T) {
dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks")
if err != nil {
t.Fatalf("Unable to create the test directory %q: %v", dir, err)
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
t.Errorf("Unable to clean up test directory %q: %v", dir, err)
}
}()
pairFile := filepath.Join(dir, "kubelet-pair.pem")
if err := ioutil.WriteFile(pairFile, storeCertData.certificatePEM, 0600); err != nil {
t.Fatalf("Unable to create the file %q: %v", pairFile, err)
}
if _, _, err := loadCertKeyBlocks(pairFile); err == nil {
t.Errorf("Got no error, but expected %q invalid.", pairFile)
}
}
func TestLoadCertKeyBlocks(t *testing.T) {
dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks")
if err != nil {
t.Fatalf("Unable to create the test directory %q: %v", dir, err)
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
t.Errorf("Unable to clean up test directory %q: %v", dir, err)
}
}()
pairFile := filepath.Join(dir, "kubelet-pair.pem")
data := append(storeCertData.certificatePEM, []byte("\n")...)
data = append(data, storeCertData.keyPEM...)
if err := ioutil.WriteFile(pairFile, data, 0600); err != nil {
t.Fatalf("Unable to create the file %q: %v", pairFile, err)
}
certBlock, keyBlock, err := loadCertKeyBlocks(pairFile)
if err != nil {
t.Errorf("Got %v, but expected no error.", pairFile)
}
if certBlock.Type != cert.CertificateBlockType {
t.Errorf("Got %q loaded from the pair file, expected a %q.", certBlock.Type, cert.CertificateBlockType)
}
if keyBlock.Type != cert.RSAPrivateKeyBlockType {
t.Errorf("Got %q loaded from the pair file, expected a %q.", keyBlock.Type, cert.RSAPrivateKeyBlockType)
}
}
func TestLoadFile(t *testing.T) {
dir, err := ioutil.TempDir("", "k8s-test-load-cert-key-blocks")
if err != nil {
@@ -280,21 +189,30 @@ func TestLoadFile(t *testing.T) {
}()
pairFile := filepath.Join(dir, "kubelet-pair.pem")
data := append(storeCertData.certificatePEM, []byte("\n")...)
data = append(data, storeCertData.keyPEM...)
if err := ioutil.WriteFile(pairFile, data, 0600); err != nil {
t.Fatalf("Unable to create the file %q: %v", pairFile, err)
}
cert, err := loadFile(pairFile)
if err != nil {
t.Fatalf("Could not load certificate from disk: %v", err)
tests := []struct {
desc string
data []byte
}{
{desc: "cert and key", data: bytes.Join([][]byte{storeCertData.certificatePEM, storeCertData.keyPEM}, []byte("\n"))},
{desc: "key and cert", data: bytes.Join([][]byte{storeCertData.keyPEM, storeCertData.certificatePEM}, []byte("\n"))},
}
if cert == nil {
t.Fatalf("There was no error, but no certificate data was returned.")
}
if cert.Leaf == nil {
t.Fatalf("Got an empty leaf, expected private data.")
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
if err := ioutil.WriteFile(pairFile, tt.data, 0600); err != nil {
t.Fatalf("Unable to create the file %q: %v", pairFile, err)
}
cert, err := loadFile(pairFile)
if err != nil {
t.Fatalf("Could not load certificate from disk: %v", err)
}
if cert == nil {
t.Fatalf("There was no error, but no certificate data was returned.")
}
if cert.Leaf == nil {
t.Fatalf("Got an empty leaf, expected private data.")
}
})
}
}

View File

@@ -24,10 +24,11 @@ import (
"encoding/base64"
"encoding/pem"
"fmt"
"github.com/golang/glog"
"reflect"
"time"
"github.com/golang/glog"
certificates "k8s.io/api/certificates/v1beta1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -38,6 +39,7 @@ import (
"k8s.io/apimachinery/pkg/watch"
certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
"k8s.io/client-go/tools/cache"
watchtools "k8s.io/client-go/tools/watch"
certutil "k8s.io/client-go/util/cert"
)
@@ -121,7 +123,7 @@ func RequestCertificate(client certificatesclient.CertificateSigningRequestInter
func WaitForCertificate(client certificatesclient.CertificateSigningRequestInterface, req *certificates.CertificateSigningRequest, timeout time.Duration) (certData []byte, err error) {
fieldSelector := fields.OneTermEqualSelector("metadata.name", req.Name).String()
event, err := cache.ListWatchUntil(
event, err := watchtools.ListWatchUntil(
timeout,
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {

View File

@@ -17,6 +17,7 @@ limitations under the License.
package workqueue
import (
"context"
"sync"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -24,9 +25,22 @@ import (
type DoWorkPieceFunc func(piece int)
// Parallelize is a very simple framework that allow for parallelizing
// Parallelize is a very simple framework that allows for parallelizing
// N independent pieces of work.
//
// Deprecated: Use ParallelizeUntil instead.
func Parallelize(workers, pieces int, doWorkPiece DoWorkPieceFunc) {
ParallelizeUntil(nil, workers, pieces, doWorkPiece)
}
// ParallelizeUntil is a framework that allows for parallelizing N
// independent pieces of work until done or the context is canceled.
func ParallelizeUntil(ctx context.Context, workers, pieces int, doWorkPiece DoWorkPieceFunc) {
var stop <-chan struct{}
if ctx != nil {
stop = ctx.Done()
}
toProcess := make(chan int, pieces)
for i := 0; i < pieces; i++ {
toProcess <- i
@@ -44,7 +58,12 @@ func Parallelize(workers, pieces int, doWorkPiece DoWorkPieceFunc) {
defer utilruntime.HandleCrash()
defer wg.Done()
for piece := range toProcess {
doWorkPiece(piece)
select {
case <-stop:
return
default:
doWorkPiece(piece)
}
}
}()
}

View File

@@ -20,10 +20,10 @@ package workqueue
type RateLimitingInterface interface {
DelayingInterface
// AddRateLimited adds an item to the workqueue after the rate limiter says its ok
// AddRateLimited adds an item to the workqueue after the rate limiter says it's ok
AddRateLimited(item interface{})
// Forget indicates that an item is finished being retried. Doesn't matter whether its for perm failing
// Forget indicates that an item is finished being retried. Doesn't matter whether it's for perm failing
// or for success, we'll stop the rate limiter from tracking it. This only clears the `rateLimiter`, you
// still have to call `Done` on the queue.
Forget(item interface{})
@@ -55,7 +55,7 @@ type rateLimitingType struct {
rateLimiter RateLimiter
}
// AddRateLimited AddAfter's the item based on the time when the rate limiter says its ok
// AddRateLimited AddAfter's the item based on the time when the rate limiter says it's ok
func (q *rateLimitingType) AddRateLimited(item interface{}) {
q.DelayingInterface.AddAfter(item, q.rateLimiter.When(item))
}

View File

@@ -1,7 +1,8 @@
language: go
go:
- 1.3
- 1.4
- "1.3"
- "1.4"
- "1.10"
script:
- go test
- go build

View File

@@ -4,13 +4,13 @@
## Introduction
A wrapper around [go-yaml](https://github.com/go-yaml/yaml) designed to enable a better way of handling YAML when marshaling to and from structs.
A wrapper around [go-yaml](https://github.com/go-yaml/yaml) designed to enable a better way of handling YAML when marshaling to and from structs.
In short, this library first converts YAML to JSON using go-yaml and then uses `json.Marshal` and `json.Unmarshal` to convert to or from the struct. This means that it effectively reuses the JSON struct tags as well as the custom JSON methods `MarshalJSON` and `UnmarshalJSON` unlike go-yaml. For a detailed overview of the rationale behind this method, [see this blog post](http://ghodss.com/2014/the-right-way-to-handle-yaml-in-golang/).
## Compatibility
This package uses [go-yaml v2](https://github.com/go-yaml/yaml) and therefore supports [everything go-yaml supports](https://github.com/go-yaml/yaml#compatibility).
This package uses [go-yaml](https://github.com/go-yaml/yaml) and therefore supports [everything go-yaml supports](https://github.com/go-yaml/yaml#compatibility).
## Caveats
@@ -44,6 +44,8 @@ import "github.com/ghodss/yaml"
Usage is very similar to the JSON library:
```go
package main
import (
"fmt"
@@ -51,8 +53,8 @@ import (
)
type Person struct {
Name string `json:"name"` // Affects YAML field names too.
Age int `json:"name"`
Name string `json:"name"` // Affects YAML field names too.
Age int `json:"age"`
}
func main() {
@@ -65,13 +67,13 @@ func main() {
}
fmt.Println(string(y))
/* Output:
name: John
age: 30
name: John
*/
// Unmarshal the YAML back into a Person struct.
var p2 Person
err := yaml.Unmarshal(y, &p2)
err = yaml.Unmarshal(y, &p2)
if err != nil {
fmt.Printf("err: %v\n", err)
return
@@ -86,11 +88,14 @@ func main() {
`yaml.YAMLToJSON` and `yaml.JSONToYAML` methods are also available:
```go
package main
import (
"fmt"
"github.com/ghodss/yaml"
)
func main() {
j := []byte(`{"name": "John", "age": 30}`)
y, err := yaml.JSONToYAML(j)

View File

@@ -45,7 +45,11 @@ func indirect(v reflect.Value, decodingNull bool) (json.Unmarshaler, encoding.Te
break
}
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
if v.CanSet() {
v.Set(reflect.New(v.Type().Elem()))
} else {
v = reflect.New(v.Type().Elem())
}
}
if v.Type().NumMethod() > 0 {
if u, ok := v.Interface().(json.Unmarshaler); ok {

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"reflect"
"strconv"
@@ -15,26 +16,30 @@ import (
func Marshal(o interface{}) ([]byte, error) {
j, err := json.Marshal(o)
if err != nil {
return nil, fmt.Errorf("error marshaling into JSON: ", err)
return nil, fmt.Errorf("error marshaling into JSON: %v", err)
}
y, err := JSONToYAML(j)
if err != nil {
return nil, fmt.Errorf("error converting JSON to YAML: ", err)
return nil, fmt.Errorf("error converting JSON to YAML: %v", err)
}
return y, nil
}
// Converts YAML to JSON then uses JSON to unmarshal into an object.
func Unmarshal(y []byte, o interface{}) error {
// JSONOpt is a decoding option for decoding from JSON format.
type JSONOpt func(*json.Decoder) *json.Decoder
// Unmarshal converts YAML to JSON then uses JSON to unmarshal into an object,
// optionally configuring the behavior of the JSON unmarshal.
func Unmarshal(y []byte, o interface{}, opts ...JSONOpt) error {
vo := reflect.ValueOf(o)
j, err := yamlToJSON(y, &vo)
j, err := yamlToJSON(y, &vo, yaml.Unmarshal)
if err != nil {
return fmt.Errorf("error converting YAML to JSON: %v", err)
}
err = json.Unmarshal(j, o)
err = jsonUnmarshal(bytes.NewReader(j), o, opts...)
if err != nil {
return fmt.Errorf("error unmarshaling JSON: %v", err)
}
@@ -42,13 +47,28 @@ func Unmarshal(y []byte, o interface{}) error {
return nil
}
// jsonUnmarshal unmarshals the JSON byte stream from the given reader into the
// object, optionally applying decoder options prior to decoding. We are not
// using json.Unmarshal directly as we want the chance to pass in non-default
// options.
func jsonUnmarshal(r io.Reader, o interface{}, opts ...JSONOpt) error {
d := json.NewDecoder(r)
for _, opt := range opts {
d = opt(d)
}
if err := d.Decode(&o); err != nil {
return fmt.Errorf("while decoding JSON: %v", err)
}
return nil
}
// Convert JSON to YAML.
func JSONToYAML(j []byte) ([]byte, error) {
// Convert the JSON to an object.
var jsonObj interface{}
// We are using yaml.Unmarshal here (instead of json.Unmarshal) because the
// Go JSON library doesn't try to pick the right number type (int, float,
// etc.) when unmarshling to interface{}, it just picks float64
// etc.) when unmarshalling to interface{}, it just picks float64
// universally. go-yaml does go through the effort of picking the right
// number type, so we can preserve number type throughout this process.
err := yaml.Unmarshal(j, &jsonObj)
@@ -60,8 +80,8 @@ func JSONToYAML(j []byte) ([]byte, error) {
return yaml.Marshal(jsonObj)
}
// Convert YAML to JSON. Since JSON is a subset of YAML, passing JSON through
// this method should be a no-op.
// YAMLToJSON converts YAML to JSON. Since JSON is a subset of YAML,
// passing JSON through this method should be a no-op.
//
// Things YAML can do that are not supported by JSON:
// * In YAML you can have binary and null keys in your maps. These are invalid
@@ -70,14 +90,22 @@ func JSONToYAML(j []byte) ([]byte, error) {
// use binary data with this library, encode the data as base64 as usual but do
// not use the !!binary tag in your YAML. This will ensure the original base64
// encoded data makes it all the way through to the JSON.
//
// For strict decoding of YAML, use YAMLToJSONStrict.
func YAMLToJSON(y []byte) ([]byte, error) {
return yamlToJSON(y, nil)
return yamlToJSON(y, nil, yaml.Unmarshal)
}
func yamlToJSON(y []byte, jsonTarget *reflect.Value) ([]byte, error) {
// YAMLToJSONStrict is like YAMLToJSON but enables strict YAML decoding,
// returning an error on any duplicate field names.
func YAMLToJSONStrict(y []byte) ([]byte, error) {
return yamlToJSON(y, nil, yaml.UnmarshalStrict)
}
func yamlToJSON(y []byte, jsonTarget *reflect.Value, yamlUnmarshal func([]byte, interface{}) error) ([]byte, error) {
// Convert the YAML to an object.
var yamlObj interface{}
err := yaml.Unmarshal(y, &yamlObj)
err := yamlUnmarshal(y, &yamlObj)
if err != nil {
return nil, err
}

14
vendor/github.com/ghodss/yaml/yaml_go110.go generated vendored Normal file
View File

@@ -0,0 +1,14 @@
// This file contains changes that are only compatible with go 1.10 and onwards.
// +build go1.10
package yaml
import "encoding/json"
// DisallowUnknownFields configures the JSON decoder to error out if unknown
// fields come along, instead of dropping them by default.
func DisallowUnknownFields(d *json.Decoder) *json.Decoder {
d.DisallowUnknownFields()
return d
}

View File

@@ -10,5 +10,6 @@
# Please keep the list sorted.
Sendgrid, Inc
Vastech SA (PTY) LTD
Walter Schulze <awalterschulze@gmail.com>

View File

@@ -1,4 +1,5 @@
Anton Povarov <anton.povarov@gmail.com>
Brian Goff <cpuguy83@gmail.com>
Clayton Coleman <ccoleman@redhat.com>
Denis Smirnov <denis.smirnov.91@gmail.com>
DongYun Kang <ceram1000@gmail.com>
@@ -10,9 +11,12 @@ John Shahid <jvshahid@gmail.com>
John Tuley <john@tuley.org>
Laurent <laurent@adyoulike.com>
Patrick Lee <patrick@dropbox.com>
Roger Johansson <rogeralsing@gmail.com>
Sam Nguyen <sam.nguyen@sendgrid.com>
Sergio Arbeo <serabe@gmail.com>
Stephen J Day <stephen.day@docker.com>
Tamir Duberstein <tamird@gmail.com>
Todd Eisenberger <teisenberger@dropbox.com>
Tormod Erevik Lea <tormodlea@gmail.com>
Vyacheslav Kim <kane@sendgrid.com>
Walter Schulze <awalterschulze@gmail.com>

View File

@@ -174,11 +174,11 @@ func sizeFixed32(x uint64) int {
// This is the format used for the sint64 protocol buffer type.
func (p *Buffer) EncodeZigzag64(x uint64) error {
// use signed number to get arithmetic right shift.
return p.EncodeVarint(uint64((x << 1) ^ uint64((int64(x) >> 63))))
return p.EncodeVarint((x << 1) ^ uint64((int64(x) >> 63)))
}
func sizeZigzag64(x uint64) int {
return sizeVarint(uint64((x << 1) ^ uint64((int64(x) >> 63))))
return sizeVarint((x << 1) ^ uint64((int64(x) >> 63)))
}
// EncodeZigzag32 writes a zigzag-encoded 32-bit integer

View File

@@ -73,7 +73,6 @@ for a protocol buffer variable v:
When the .proto file specifies `syntax="proto3"`, there are some differences:
- Non-repeated fields of non-message type are values instead of pointers.
- Getters are only generated for message and oneof fields.
- Enum types do not get an Enum method.
The simplest way to describe this is to see an example.

View File

@@ -193,6 +193,7 @@ type Properties struct {
Default string // default value
HasDefault bool // whether an explicit default was provided
CustomType string
CastType string
StdTime bool
StdDuration bool
@@ -341,6 +342,8 @@ func (p *Properties) Parse(s string) {
p.OrigName = strings.Split(f, "=")[1]
case strings.HasPrefix(f, "customtype="):
p.CustomType = strings.Split(f, "=")[1]
case strings.HasPrefix(f, "casttype="):
p.CastType = strings.Split(f, "=")[1]
case f == "stdtime":
p.StdTime = true
case f == "stdduration":

View File

@@ -522,6 +522,17 @@ func (tm *TextMarshaler) writeAny(w *textWriter, v reflect.Value, props *Propert
}
return nil
}
} else if len(props.CastType) > 0 {
if _, ok := v.Interface().(interface {
String() string
}); ok {
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
_, err := fmt.Fprintf(w, "%d", v.Interface())
return err
}
}
} else if props.StdTime {
t, ok := v.Interface().(time.Time)
if !ok {
@@ -531,9 +542,9 @@ func (tm *TextMarshaler) writeAny(w *textWriter, v reflect.Value, props *Propert
if err != nil {
return err
}
props.StdTime = false
err = tm.writeAny(w, reflect.ValueOf(tproto), props)
props.StdTime = true
propsCopy := *props // Make a copy so that this is goroutine-safe
propsCopy.StdTime = false
err = tm.writeAny(w, reflect.ValueOf(tproto), &propsCopy)
return err
} else if props.StdDuration {
d, ok := v.Interface().(time.Duration)
@@ -541,9 +552,9 @@ func (tm *TextMarshaler) writeAny(w *textWriter, v reflect.Value, props *Propert
return fmt.Errorf("stdtime is not time.Duration, but %T", v.Interface())
}
dproto := durationProto(d)
props.StdDuration = false
err := tm.writeAny(w, reflect.ValueOf(dproto), props)
props.StdDuration = true
propsCopy := *props // Make a copy so that this is goroutine-safe
propsCopy.StdDuration = false
err := tm.writeAny(w, reflect.ValueOf(dproto), &propsCopy)
return err
}
}

View File

@@ -983,7 +983,7 @@ func (p *textParser) readAny(v reflect.Value, props *Properties) error {
return p.readStruct(fv, terminator)
case reflect.Uint32:
if x, err := strconv.ParseUint(tok.value, 0, 32); err == nil {
fv.SetUint(uint64(x))
fv.SetUint(x)
return nil
}
case reflect.Uint64:

33
vendor/github.com/imdario/mergo/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,33 @@
#### joe made this: http://goel.io/joe
#### go ####
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
#### vim ####
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# Session
Session.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags

View File

@@ -1,2 +1,7 @@
language: go
install: go get -t
install:
- go get -t
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
script:
- $HOME/gopath/bin/goveralls -service=travis-ci -repotoken $COVERALLS_TOKEN

46
vendor/github.com/imdario/mergo/CODE_OF_CONDUCT.md generated vendored Normal file
View File

@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at i@dario.im. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -2,14 +2,72 @@
A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements.
Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the Province of Ancona in the Italian region Marche.
![Mergo dall'alto](http://www.comune.mergo.an.it/Siti/Mergo/Immagini/Foto/mergo_dall_alto.jpg)
Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the Province of Ancona in the Italian region of Marche.
## Status
It is ready for production use. It works fine although it may use more of testing. Here some projects in the wild using Mergo:
It is ready for production use. [It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, etc](https://github.com/imdario/mergo#mergo-in-the-wild).
[![GoDoc][3]][4]
[![GoCard][5]][6]
[![Build Status][1]][2]
[![Coverage Status][7]][8]
[![Sourcegraph][9]][10]
[1]: https://travis-ci.org/imdario/mergo.png
[2]: https://travis-ci.org/imdario/mergo
[3]: https://godoc.org/github.com/imdario/mergo?status.svg
[4]: https://godoc.org/github.com/imdario/mergo
[5]: https://goreportcard.com/badge/imdario/mergo
[6]: https://goreportcard.com/report/github.com/imdario/mergo
[7]: https://coveralls.io/repos/github/imdario/mergo/badge.svg?branch=master
[8]: https://coveralls.io/github/imdario/mergo?branch=master
[9]: https://sourcegraph.com/github.com/imdario/mergo/-/badge.svg
[10]: https://sourcegraph.com/github.com/imdario/mergo?badge
### Latest release
[Release v0.3.4](https://github.com/imdario/mergo/releases/tag/v0.3.4).
### Important note
Please keep in mind that in [0.3.2](//github.com/imdario/mergo/releases/tag/0.3.2) Mergo changed `Merge()`and `Map()` signatures to support [transformers](#transformers). An optional/variadic argument has been added, so it won't break existing code.
If you were using Mergo **before** April 6th 2015, please check your project works as intended after updating your local copy with ```go get -u github.com/imdario/mergo```. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause (I hope it won't!) in existing projects after the change (release 0.2.0).
### Donations
If Mergo is useful to you, consider buying me a coffee, a beer or making a monthly donation so I can keep building great free software. :heart_eyes:
<a href='https://ko-fi.com/B0B58839' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
[![Beerpay](https://beerpay.io/imdario/mergo/badge.svg)](https://beerpay.io/imdario/mergo)
[![Beerpay](https://beerpay.io/imdario/mergo/make-wish.svg)](https://beerpay.io/imdario/mergo)
<a href="https://liberapay.com/dario/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
### Mergo in the wild
- [moby/moby](https://github.com/moby/moby)
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)
- [vmware/dispatch](https://github.com/vmware/dispatch)
- [Shopify/themekit](https://github.com/Shopify/themekit)
- [imdario/zas](https://github.com/imdario/zas)
- [matcornic/hermes](https://github.com/matcornic/hermes)
- [OpenBazaar/openbazaar-go](https://github.com/OpenBazaar/openbazaar-go)
- [kataras/iris](https://github.com/kataras/iris)
- [michaelsauter/crane](https://github.com/michaelsauter/crane)
- [go-task/task](https://github.com/go-task/task)
- [sensu/uchiwa](https://github.com/sensu/uchiwa)
- [ory/hydra](https://github.com/ory/hydra)
- [sisatech/vcli](https://github.com/sisatech/vcli)
- [dairycart/dairycart](https://github.com/dairycart/dairycart)
- [projectcalico/felix](https://github.com/projectcalico/felix)
- [resin-os/balena](https://github.com/resin-os/balena)
- [go-kivik/kivik](https://github.com/go-kivik/kivik)
- [Telefonica/govice](https://github.com/Telefonica/govice)
- [supergiant/supergiant](supergiant/supergiant)
- [SergeyTsalkov/brooce](https://github.com/SergeyTsalkov/brooce)
- [soniah/dnsmadeeasy](https://github.com/soniah/dnsmadeeasy)
- [ohsu-comp-bio/funnel](https://github.com/ohsu-comp-bio/funnel)
- [EagerIO/Stout](https://github.com/EagerIO/Stout)
- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api)
- [russross/canvasassignments](https://github.com/russross/canvasassignments)
@@ -17,12 +75,17 @@ It is ready for production use. It works fine although it may use more of testin
- [casualjim/exeggutor](https://github.com/casualjim/exeggutor)
- [divshot/gitling](https://github.com/divshot/gitling)
- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl)
[![Build Status][1]][2]
[![GoDoc](https://godoc.org/github.com/imdario/mergo?status.svg)](https://godoc.org/github.com/imdario/mergo)
[1]: https://travis-ci.org/imdario/mergo.png
[2]: https://travis-ci.org/imdario/mergo
- [andrerocker/deploy42](https://github.com/andrerocker/deploy42)
- [elwinar/rambler](https://github.com/elwinar/rambler)
- [tmaiaroto/gopartman](https://github.com/tmaiaroto/gopartman)
- [jfbus/impressionist](https://github.com/jfbus/impressionist)
- [Jmeyering/zealot](https://github.com/Jmeyering/zealot)
- [godep-migrator/rigger-host](https://github.com/godep-migrator/rigger-host)
- [Dronevery/MultiwaySwitch-Go](https://github.com/Dronevery/MultiwaySwitch-Go)
- [thoas/picfit](https://github.com/thoas/picfit)
- [mantasmatelis/whooplist-server](https://github.com/mantasmatelis/whooplist-server)
- [jnuthong/item_search](https://github.com/jnuthong/item_search)
- [bukalapak/snowboard](https://github.com/bukalapak/snowboard)
## Installation
@@ -35,25 +98,116 @@ It is ready for production use. It works fine although it may use more of testin
## Usage
You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. Also maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection).
You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. It won't merge empty structs value as [they are not considered zero values](https://golang.org/ref/spec#The_zero_value) either. Also maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection).
if err := mergo.Merge(&dst, src); err != nil {
// ...
}
```go
if err := mergo.Merge(&dst, src); err != nil {
// ...
}
```
Additionally, you can map a map[string]interface{} to a struct (and otherwise, from struct to map), following the same restrictions as in Merge(). Keys are capitalized to find each corresponding exported field.
Also, you can merge overwriting values using the transformer `WithOverride`.
if err := mergo.Map(&dst, srcMap); err != nil {
// ...
}
```go
if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil {
// ...
}
```
Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as map[string]interface{}. They will be just assigned as values.
Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field.
```go
if err := mergo.Map(&dst, srcMap); err != nil {
// ...
}
```
Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as `map[string]interface{}`. They will be just assigned as values.
More information and examples in [godoc documentation](http://godoc.org/github.com/imdario/mergo).
### Nice example
```go
package main
import (
"fmt"
"github.com/imdario/mergo"
)
type Foo struct {
A string
B int64
}
func main() {
src := Foo{
A: "one",
B: 2,
}
dest := Foo{
A: "two",
}
mergo.Merge(&dest, src)
fmt.Println(dest)
// Will print
// {two 2}
}
```
Note: if test are failing due missing package, please execute:
go get gopkg.in/yaml.v1
go get gopkg.in/yaml.v2
### Transformers
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`?
```go
package main
import (
"fmt"
"github.com/imdario/mergo"
"reflect"
"time"
)
type timeTransfomer struct {
}
func (t timeTransfomer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
if typ == reflect.TypeOf(time.Time{}) {
return func(dst, src reflect.Value) error {
if dst.CanSet() {
isZero := dst.MethodByName("IsZero")
result := isZero.Call([]reflect.Value{})
if result[0].Bool() {
dst.Set(src)
}
}
return nil
}
}
return nil
}
type Snapshot struct {
Time time.Time
// ...
}
func main() {
src := Snapshot{time.Now()}
dest := Snapshot{}
mergo.Merge(&dest, src, mergo.WithTransformers(timeTransfomer{}))
fmt.Println(dest)
// Will print
// { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 }
}
```
## Contact me

View File

@@ -31,7 +31,8 @@ func isExported(field reflect.StructField) bool {
// Traverses recursively both values, assigning src's fields values to dst.
// The map argument tracks comparisons that have already been seen, which allows
// short circuiting on recursive types.
func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) {
func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *Config) (err error) {
overwrite := config.Overwrite
if dst.CanAddr() {
addr := dst.UnsafeAddr()
h := 17 * addr
@@ -57,10 +58,17 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err
}
fieldName := field.Name
fieldName = changeInitialCase(fieldName, unicode.ToLower)
if v, ok := dstMap[fieldName]; !ok || isEmptyValue(reflect.ValueOf(v)) {
if v, ok := dstMap[fieldName]; !ok || (isEmptyValue(reflect.ValueOf(v)) || overwrite) {
dstMap[fieldName] = src.Field(i).Interface()
}
}
case reflect.Ptr:
if dst.IsNil() {
v := reflect.New(dst.Type().Elem())
dst.Set(v)
}
dst = dst.Elem()
fallthrough
case reflect.Struct:
srcMap := src.Interface().(map[string]interface{})
for key := range srcMap {
@@ -85,21 +93,24 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err
srcKind = reflect.Ptr
}
}
if !srcElement.IsValid() {
continue
}
if srcKind == dstKind {
if err = deepMerge(dstElement, srcElement, visited, depth+1); err != nil {
if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
} else if dstKind == reflect.Interface && dstElement.Kind() == reflect.Interface {
if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
} else if srcKind == reflect.Map {
if err = deepMap(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
} else {
if srcKind == reflect.Map {
if err = deepMap(dstElement, srcElement, visited, depth+1); err != nil {
return
}
} else {
return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind)
}
return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind)
}
}
}
@@ -117,18 +128,35 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err
// doesn't apply if dst is a map.
// This is separated method from Merge because it is cleaner and it keeps sane
// semantics: merging equal types, mapping different (restricted) types.
func Map(dst, src interface{}) error {
func Map(dst, src interface{}, opts ...func(*Config)) error {
return _map(dst, src, opts...)
}
// MapWithOverwrite will do the same as Map except that non-empty dst attributes will be overridden by
// non-empty src attribute values.
// Deprecated: Use Map(…) with WithOverride
func MapWithOverwrite(dst, src interface{}, opts ...func(*Config)) error {
return _map(dst, src, append(opts, WithOverride)...)
}
func _map(dst, src interface{}, opts ...func(*Config)) error {
var (
vDst, vSrc reflect.Value
err error
)
config := &Config{}
for _, opt := range opts {
opt(config)
}
if vDst, vSrc, err = resolveValues(dst, src); err != nil {
return err
}
// To be friction-less, we redirect equal-type arguments
// to deepMerge. Only because arguments can be anything.
if vSrc.Kind() == vDst.Kind() {
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0)
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config)
}
switch vSrc.Kind() {
case reflect.Struct:
@@ -142,5 +170,5 @@ func Map(dst, src interface{}) error {
default:
return ErrNotSupported
}
return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0)
return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0, config)
}

View File

@@ -12,10 +12,34 @@ import (
"reflect"
)
func hasExportedField(dst reflect.Value) (exported bool) {
for i, n := 0, dst.NumField(); i < n; i++ {
field := dst.Type().Field(i)
if field.Anonymous && dst.Field(i).Kind() == reflect.Struct {
exported = exported || hasExportedField(dst.Field(i))
} else {
exported = exported || len(field.PkgPath) == 0
}
}
return
}
type Config struct {
Overwrite bool
AppendSlice bool
Transformers Transformers
}
type Transformers interface {
Transformer(reflect.Type) func(dst, src reflect.Value) error
}
// Traverses recursively both values, assigning src's fields values to dst.
// The map argument tracks comparisons that have already been seen, which allows
// short circuiting on recursive types.
func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) {
func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *Config) (err error) {
overwrite := config.Overwrite
if !src.IsValid() {
return
}
@@ -32,68 +56,190 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (e
// Remember, remember...
visited[h] = &visit{addr, typ, seen}
}
if config.Transformers != nil && !isEmptyValue(dst) {
if fn := config.Transformers.Transformer(dst.Type()); fn != nil {
err = fn(dst, src)
return
}
}
switch dst.Kind() {
case reflect.Struct:
for i, n := 0, dst.NumField(); i < n; i++ {
if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1); err != nil {
return
if hasExportedField(dst) {
for i, n := 0, dst.NumField(); i < n; i++ {
if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1, config); err != nil {
return
}
}
} else {
if dst.CanSet() && !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) {
dst.Set(src)
}
}
case reflect.Map:
if dst.IsNil() && !src.IsNil() {
dst.Set(reflect.MakeMap(dst.Type()))
}
for _, key := range src.MapKeys() {
srcElement := src.MapIndex(key)
if !srcElement.IsValid() {
continue
}
dstElement := dst.MapIndex(key)
switch reflect.TypeOf(srcElement.Interface()).Kind() {
case reflect.Struct:
switch srcElement.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Interface, reflect.Slice:
if srcElement.IsNil() {
continue
}
fallthrough
case reflect.Map:
if err = deepMerge(dstElement, srcElement, visited, depth+1); err != nil {
return
default:
if !srcElement.CanInterface() {
continue
}
switch reflect.TypeOf(srcElement.Interface()).Kind() {
case reflect.Struct:
fallthrough
case reflect.Ptr:
fallthrough
case reflect.Map:
srcMapElm := srcElement
dstMapElm := dstElement
if srcMapElm.CanInterface() {
srcMapElm = reflect.ValueOf(srcMapElm.Interface())
if dstMapElm.IsValid() {
dstMapElm = reflect.ValueOf(dstMapElm.Interface())
}
}
if err = deepMerge(dstMapElm, srcMapElm, visited, depth+1, config); err != nil {
return
}
case reflect.Slice:
srcSlice := reflect.ValueOf(srcElement.Interface())
var dstSlice reflect.Value
if !dstElement.IsValid() || dstElement.IsNil() {
dstSlice = reflect.MakeSlice(srcSlice.Type(), 0, srcSlice.Len())
} else {
dstSlice = reflect.ValueOf(dstElement.Interface())
}
if !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice {
dstSlice = srcSlice
} else if config.AppendSlice {
dstSlice = reflect.AppendSlice(dstSlice, srcSlice)
}
dst.SetMapIndex(key, dstSlice)
}
}
if !dstElement.IsValid() {
if dstElement.IsValid() && reflect.TypeOf(srcElement.Interface()).Kind() == reflect.Map {
continue
}
if srcElement.IsValid() && (overwrite || (!dstElement.IsValid() || isEmptyValue(dstElement))) {
if dst.IsNil() {
dst.Set(reflect.MakeMap(dst.Type()))
}
dst.SetMapIndex(key, srcElement)
}
}
case reflect.Slice:
if !dst.CanSet() {
break
}
if !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice {
dst.Set(src)
} else if config.AppendSlice {
dst.Set(reflect.AppendSlice(dst, src))
}
case reflect.Ptr:
fallthrough
case reflect.Interface:
if src.IsNil() {
break
} else if dst.IsNil() {
if dst.CanSet() && isEmptyValue(dst) {
}
if src.Kind() != reflect.Interface {
if dst.IsNil() || overwrite {
if dst.CanSet() && (overwrite || isEmptyValue(dst)) {
dst.Set(src)
}
} else if src.Kind() == reflect.Ptr {
if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
return
}
} else if dst.Elem().Type() == src.Type() {
if err = deepMerge(dst.Elem(), src, visited, depth+1, config); err != nil {
return
}
} else {
return ErrDifferentArgumentsTypes
}
break
}
if dst.IsNil() || overwrite {
if dst.CanSet() && (overwrite || isEmptyValue(dst)) {
dst.Set(src)
}
} else if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1); err != nil {
} else if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
return
}
default:
if dst.CanSet() && !isEmptyValue(src) {
if dst.CanSet() && !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) {
dst.Set(src)
}
}
return
}
// Merge sets fields' values in dst from src if they have a zero
// value of their type.
// dst and src must be valid same-type structs and dst must be
// a pointer to struct.
// It won't merge unexported (private) fields and will do recursively
// any exported field.
func Merge(dst, src interface{}) error {
// Merge will fill any empty for value type attributes on the dst struct using corresponding
// src attributes if they themselves are not empty. dst and src must be valid same-type structs
// and dst must be a pointer to struct.
// It won't merge unexported (private) fields and will do recursively any exported field.
func Merge(dst, src interface{}, opts ...func(*Config)) error {
return merge(dst, src, opts...)
}
// MergeWithOverwrite will do the same as Merge except that non-empty dst attributes will be overriden by
// non-empty src attribute values.
// Deprecated: use Merge(…) with WithOverride
func MergeWithOverwrite(dst, src interface{}, opts ...func(*Config)) error {
return merge(dst, src, append(opts, WithOverride)...)
}
// WithTransformers adds transformers to merge, allowing to customize the merging of some types.
func WithTransformers(transformers Transformers) func(*Config) {
return func(config *Config) {
config.Transformers = transformers
}
}
// WithOverride will make merge override non-empty dst attributes with non-empty src attributes values.
func WithOverride(config *Config) {
config.Overwrite = true
}
// WithAppendSlice will make merge append slices instead of overwriting it
func WithAppendSlice(config *Config) {
config.AppendSlice = true
}
func merge(dst, src interface{}, opts ...func(*Config)) error {
var (
vDst, vSrc reflect.Value
err error
)
config := &Config{}
for _, opt := range opts {
opt(config)
}
if vDst, vSrc, err = resolveValues(dst, src); err != nil {
return err
}
if vDst.Type() != vSrc.Type() {
return ErrDifferentArgumentsTypes
}
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0)
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config)
}

View File

@@ -32,7 +32,7 @@ type visit struct {
next *visit
}
// From src/pkg/encoding/json.
// From src/pkg/encoding/json/encode.go.
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
@@ -46,7 +46,14 @@ func isEmptyValue(v reflect.Value) bool {
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
if v.IsNil() {
return true
}
return isEmptyValue(v.Elem())
case reflect.Func:
return v.IsNil()
case reflect.Invalid:
return true
}
return false
}

View File

@@ -6,6 +6,7 @@ go:
before_install:
- go get -t -v ./...
- go get -t -v github.com/modern-go/reflect2-tests/...
script:
- ./test.sh

View File

@@ -24,7 +24,7 @@
# go-tests = true
# unused-packages = true
ignored = ["github.com/modern-go/test","github.com/modern-go/test/must","github.com/modern-go/test/should"]
ignored = []
[[constraint]]
name = "github.com/modern-go/concurrent"

View File

@@ -150,6 +150,9 @@ func (cfg *frozenConfig) TypeOf(obj interface{}) Type {
}
func (cfg *frozenConfig) Type2(type1 reflect.Type) Type {
if type1 == nil {
return nil
}
cacheKey := uintptr(unpackEFace(type1).data)
typeObj, found := cfg.cache.Load(cacheKey)
if found {

Some files were not shown because too many files have changed in this diff Show More