mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-02-23 06:13:51 +00:00
Compare commits
1005 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55bc3dee7f | ||
|
|
fdbeb213fb | ||
|
|
1817d6c751 | ||
|
|
476cc98e5a | ||
|
|
4cbf4959f2 | ||
|
|
e4e4e686f6 | ||
|
|
d5be37673a | ||
|
|
7bb5657c4d | ||
|
|
fe9f12a29c | ||
|
|
bea75bb7ac | ||
|
|
081de8da62 | ||
|
|
469f864de3 | ||
|
|
dc510792c4 | ||
|
|
8b6fe0ac70 | ||
|
|
d47603472d | ||
|
|
d4baeff94e | ||
|
|
210114dbe1 | ||
|
|
4ebd0f5f12 | ||
|
|
f02b39b80f | ||
|
|
608be8332e | ||
|
|
3742719427 | ||
|
|
f00a02dcca | ||
|
|
017c891fb8 | ||
|
|
d8baa43903 | ||
|
|
5c741bc423 | ||
|
|
e99e175ce2 | ||
|
|
003e3e3c4d | ||
|
|
391202c253 | ||
|
|
95bb4c4be5 | ||
|
|
32422f18f1 | ||
|
|
d52f500b83 | ||
|
|
defad3cdd7 | ||
|
|
c6d7724b67 | ||
|
|
3359311228 | ||
|
|
7e16619146 | ||
|
|
a084544f08 | ||
|
|
c2588cf035 | ||
|
|
2179c2879a | ||
|
|
b6e217e13a | ||
|
|
6f2882b831 | ||
|
|
cd6e48bfa8 | ||
|
|
818535f30d | ||
|
|
c0361645e2 | ||
|
|
6bf8bfe9a8 | ||
|
|
ea130ea781 | ||
|
|
03619fc878 | ||
|
|
454348b2fd | ||
|
|
cda8bd6e26 | ||
|
|
c0d32f10b2 | ||
|
|
ce5b05f912 | ||
|
|
176fb6a139 | ||
|
|
9501168265 | ||
|
|
2e79664f3d | ||
|
|
e70788204b | ||
|
|
f6646eb2b7 | ||
|
|
75cfda0ffe | ||
|
|
bde54ef643 | ||
|
|
d90398815b | ||
|
|
7683a98792 | ||
|
|
d7e9568137 | ||
|
|
904086cbec | ||
|
|
5e95c25d4f | ||
|
|
c9b1982767 | ||
|
|
f69d095a69 | ||
|
|
1e7f2c7735 | ||
|
|
9af75d23fb | ||
|
|
d0df2009ac | ||
|
|
964d4889c4 | ||
|
|
a537287601 | ||
|
|
fdfc854f8c | ||
|
|
331fef8fae | ||
|
|
4470d3d2d1 | ||
|
|
698bffc2ad | ||
|
|
6ff3e42602 | ||
|
|
3e50b4e129 | ||
|
|
d856221f56 | ||
|
|
f519f0cb09 | ||
|
|
c03fe2d1fe | ||
|
|
2749044625 | ||
|
|
f73c70d8f9 | ||
|
|
ebd5e45fa6 | ||
|
|
6154883855 | ||
|
|
ebe01a5aef | ||
|
|
28d00ce67b | ||
|
|
50e4531215 | ||
|
|
1a9922d050 | ||
|
|
f2a48aee2b | ||
|
|
d162cb9adf | ||
|
|
14a28bec24 | ||
|
|
dae62929e0 | ||
|
|
c22ac17dfe | ||
|
|
08c446a3e1 | ||
|
|
bd8c243636 | ||
|
|
e4bf6e068f | ||
|
|
120e46b5f7 | ||
|
|
257d69045d | ||
|
|
05a188d4cd | ||
|
|
205c22ddbe | ||
|
|
aa79bc7609 | ||
|
|
a36914f5ca | ||
|
|
cc8f0b623c | ||
|
|
de6837226e | ||
|
|
3a32833306 | ||
|
|
74df6d138b | ||
|
|
0dd2b358fb | ||
|
|
6520c5a3a1 | ||
|
|
5a43a5d53a | ||
|
|
897340860b | ||
|
|
4d2035ab2a | ||
|
|
d85135c12e | ||
|
|
30a392b900 | ||
|
|
4ab3c64b70 | ||
|
|
2515b2d710 | ||
|
|
10a1e29e15 | ||
|
|
2319606cd2 | ||
|
|
10168ab2e7 | ||
|
|
c5b784465b | ||
|
|
236dbdb2c4 | ||
|
|
6887d0aca2 | ||
|
|
64e0dbb481 | ||
|
|
e47543233c | ||
|
|
2460568be3 | ||
|
|
1b31489347 | ||
|
|
ab6452ace7 | ||
|
|
c46aa1c29d | ||
|
|
939ea30030 | ||
|
|
efd973fa17 | ||
|
|
4f671f5dca | ||
|
|
a5384a6e38 | ||
|
|
e64f2fe7fb | ||
|
|
035362f4d3 | ||
|
|
8065a8d2e6 | ||
|
|
e22ad6171a | ||
|
|
c2b0acf241 | ||
|
|
00694c9cb6 | ||
|
|
dc96f398da | ||
|
|
755a87cdbb | ||
|
|
c538a4e8e8 | ||
|
|
41949d8e07 | ||
|
|
4c162be8bf | ||
|
|
b530cef3b1 | ||
|
|
c82f568b2c | ||
|
|
5e4746e96b | ||
|
|
077aa8a42e | ||
|
|
d509e7012e | ||
|
|
8c0bafd5be | ||
|
|
12b13b1ea5 | ||
|
|
5b1dc0abdf | ||
|
|
253e0f8e9a | ||
|
|
87f2899047 | ||
|
|
6ddf4c04e6 | ||
|
|
1d68841c78 | ||
|
|
f77c92560f | ||
|
|
c12a23725d | ||
|
|
d5beba354b | ||
|
|
71712b2d00 | ||
|
|
ad3f04a982 | ||
|
|
a52455504f | ||
|
|
4f154100ff | ||
|
|
d2d9b1e49e | ||
|
|
c9ce067a0e | ||
|
|
1af25552a0 | ||
|
|
a64786a728 | ||
|
|
2d28d1da19 | ||
|
|
78fdc59d2d | ||
|
|
29d7f406f7 | ||
|
|
3449b896d6 | ||
|
|
22ca2da1ff | ||
|
|
e98c6dfdd8 | ||
|
|
fcd8c585c3 | ||
|
|
a918e9fb97 | ||
|
|
34accc3dee | ||
|
|
61d64fc4c6 | ||
|
|
b793b9a17e | ||
|
|
7b1ecf79a6 | ||
|
|
32b038c639 | ||
|
|
d13bb07b3e | ||
|
|
24396b6af1 | ||
|
|
006dc8aa79 | ||
|
|
2a2e2f532b | ||
|
|
1078bf4dfb | ||
|
|
c14621428f | ||
|
|
6582c23edb | ||
|
|
0b300cbe42 | ||
|
|
876f0a55d8 | ||
|
|
c853707889 | ||
|
|
005133fbfb | ||
|
|
0cb1538b39 | ||
|
|
0abe10e6b2 | ||
|
|
883b90923d | ||
|
|
d6a0dfa497 | ||
|
|
29d5e43220 | ||
|
|
eef1fd0c64 | ||
|
|
b2be83ee45 | ||
|
|
b20a8358d3 | ||
|
|
a58b460bcb | ||
|
|
8fd6a71312 | ||
|
|
6efbd81f75 | ||
|
|
a059d8dfce | ||
|
|
8c0a073cb6 | ||
|
|
389cd3486b | ||
|
|
eac108aee5 | ||
|
|
49ec16038c | ||
|
|
4bd68b1fa1 | ||
|
|
73419313ee | ||
|
|
4750d7d7d2 | ||
|
|
ba0dc3bf52 | ||
|
|
5d8594b285 | ||
|
|
ce1b6303d9 | ||
|
|
36bc679142 | ||
|
|
c4f6fd5b3c | ||
|
|
52f58477b8 | ||
|
|
d848499176 | ||
|
|
c3b7d21037 | ||
|
|
832bc2726e | ||
|
|
3833ba0430 | ||
|
|
ec133b9743 | ||
|
|
d8c6894cbc | ||
|
|
b102aa8991 | ||
|
|
9eb97e2683 | ||
|
|
fea626b654 | ||
|
|
16163b989b | ||
|
|
165fce67af | ||
|
|
6a8f377781 | ||
|
|
d24cf4b8a7 | ||
|
|
34e15f03c3 | ||
|
|
274e6281a8 | ||
|
|
7146cb3880 | ||
|
|
9dfbe60253 | ||
|
|
1734280a19 | ||
|
|
9a0f75980d | ||
|
|
ddd1d29e5d | ||
|
|
03f09c6870 | ||
|
|
f99c186c55 | ||
|
|
14b8def320 | ||
|
|
5697adc36a | ||
|
|
9c1c760f56 | ||
|
|
48f2ae9eb4 | ||
|
|
7c9aff3278 | ||
|
|
58607c7e81 | ||
|
|
1b3103c9b5 | ||
|
|
666c0b0e18 | ||
|
|
f0fc84c922 | ||
|
|
7b7901af36 | ||
|
|
57453773ea | ||
|
|
f4fcb9bde6 | ||
|
|
0799a538dc | ||
|
|
4f700d4811 | ||
|
|
d7edc41c24 | ||
|
|
333a3ab4c2 | ||
|
|
730092f39c | ||
|
|
d3599c541b | ||
|
|
454f35ccd6 | ||
|
|
27daf0a2fe | ||
|
|
8bf03257f4 | ||
|
|
1ad2c38509 | ||
|
|
84cc42b2ca | ||
|
|
4c68050706 | ||
|
|
aa826a1579 | ||
|
|
60f92d5fe2 | ||
|
|
df27c2e1fc | ||
|
|
45f57939af | ||
|
|
30f5f66090 | ||
|
|
2a29303e3f | ||
|
|
643c60fd7a | ||
|
|
7174f857d8 | ||
|
|
0be2c0d40f | ||
|
|
a75c2194bc | ||
|
|
41140766f0 | ||
|
|
045c427317 | ||
|
|
ac404af48f | ||
|
|
a2ecd05240 | ||
|
|
a778a5ef81 | ||
|
|
c94ee7188c | ||
|
|
7ef6a02d0a | ||
|
|
c832cab8d0 | ||
|
|
234465789b | ||
|
|
da6d69d807 | ||
|
|
04ef7c5456 | ||
|
|
f05c3092b5 | ||
|
|
2637dc00da | ||
|
|
e8365d2c57 | ||
|
|
dd151b3f50 | ||
|
|
f1eeae8c71 | ||
|
|
41e4a74b57 | ||
|
|
fa49beb623 | ||
|
|
9bd206cedb | ||
|
|
5b01e4be2d | ||
|
|
bbbb40994d | ||
|
|
f709da5569 | ||
|
|
ccb17843c1 | ||
|
|
f8111db5ff | ||
|
|
3fcde8088c | ||
|
|
f937ae2c07 | ||
|
|
1c7c22352f | ||
|
|
0cae72b391 | ||
|
|
9a8c80f20a | ||
|
|
a42e3708aa | ||
|
|
c8fc8a0b65 | ||
|
|
8fc68a4b21 | ||
|
|
975d493b8a | ||
|
|
aee7a7a72b | ||
|
|
a31c24e5a0 | ||
|
|
943b0ff6ec | ||
|
|
d42c533fbb | ||
|
|
4dbde4cf7f | ||
|
|
7be8927d5e | ||
|
|
96d7743eab | ||
|
|
2254f76b30 | ||
|
|
852c1b7a27 | ||
|
|
522210adb6 | ||
|
|
a4089fcc72 | ||
|
|
60034b39a3 | ||
|
|
2f7c80a5e0 | ||
|
|
827e6e0dc0 | ||
|
|
dac1c9939e | ||
|
|
a6d74ea876 | ||
|
|
7a1d92a8d4 | ||
|
|
f2db76a0d5 | ||
|
|
3721632de2 | ||
|
|
4de949fe18 | ||
|
|
069b3fba37 | ||
|
|
e74dd47b1d | ||
|
|
6a9f57f83d | ||
|
|
80ff5c1f17 | ||
|
|
aa22047a0f | ||
|
|
abc941097c | ||
|
|
62630d6449 | ||
|
|
f228f022f5 | ||
|
|
1c1decfaf1 | ||
|
|
7786c83b0d | ||
|
|
41b75e6977 | ||
|
|
a54e1145a5 | ||
|
|
b8592a361c | ||
|
|
19881e4d7f | ||
|
|
126f9c0da3 | ||
|
|
7a140bf63c | ||
|
|
f5fedbb6b2 | ||
|
|
957cb2d56c | ||
|
|
b3cdc438ce | ||
|
|
22a3e73bac | ||
|
|
0ad91c43f7 | ||
|
|
2b208807a6 | ||
|
|
25f841d063 | ||
|
|
10b769c676 | ||
|
|
67da840097 | ||
|
|
93d4581721 | ||
|
|
0a7c5b0604 | ||
|
|
acbeb93f79 | ||
|
|
6565265bee | ||
|
|
b42a34d822 | ||
|
|
3ce3403b95 | ||
|
|
eb19980110 | ||
|
|
c7905c6638 | ||
|
|
fdd8ef5835 | ||
|
|
25bc8dd8a9 | ||
|
|
6512ab1351 | ||
|
|
5cd60fa5f9 | ||
|
|
fac571b51a | ||
|
|
9b87906a30 | ||
|
|
c8b1f00107 | ||
|
|
f015ad5852 | ||
|
|
b04fd46319 | ||
|
|
4c304e4224 | ||
|
|
0a9f446893 | ||
|
|
96cec59236 | ||
|
|
4faf724c2c | ||
|
|
de88ae2f61 | ||
|
|
dd3d1c8b1b | ||
|
|
2e9baf9fa6 | ||
|
|
ac01186499 | ||
|
|
2eb01bd307 | ||
|
|
741b8fe88d | ||
|
|
d25c6d9d0a | ||
|
|
89b00e3702 | ||
|
|
d2480e6300 | ||
|
|
4205e3dedc | ||
|
|
ee80920ffd | ||
|
|
45f4a0528c | ||
|
|
d0266cecdb | ||
|
|
0fc1f17866 | ||
|
|
ae6503e972 | ||
|
|
44b7679e9f | ||
|
|
12d5b8959d | ||
|
|
5b076e7421 | ||
|
|
1ffe70bbea | ||
|
|
e4c49c37b9 | ||
|
|
268ca5b7f6 | ||
|
|
cf735715f6 | ||
|
|
2679d27ced | ||
|
|
6b71b8d8ad | ||
|
|
8697488126 | ||
|
|
dfcc2a1eb8 | ||
|
|
812f5084a1 | ||
|
|
43da4ab2e0 | ||
|
|
e4d8af6701 | ||
|
|
d06c935c2c | ||
|
|
9399b5d800 | ||
|
|
05a471fdf9 | ||
|
|
81d4e50f94 | ||
|
|
850f030fe3 | ||
|
|
f7958ae75b | ||
|
|
ee05f155ca | ||
|
|
2ae631b603 | ||
|
|
9c64476aee | ||
|
|
b6e98b5783 | ||
|
|
9addb4d6e0 | ||
|
|
2a921f7090 | ||
|
|
bb8b65cca6 | ||
|
|
5c331e9002 | ||
|
|
1382fc6e5f | ||
|
|
cc8c917249 | ||
|
|
ae498f14b4 | ||
|
|
288d9c999e | ||
|
|
26922307ad | ||
|
|
5549a262b9 | ||
|
|
6b46bae6c6 | ||
|
|
c5df66fbd5 | ||
|
|
23e8c35918 | ||
|
|
ab60396ac4 | ||
|
|
343c275f46 | ||
|
|
12e41d783f | ||
|
|
2f891b4bfb | ||
|
|
170b86d0c6 | ||
|
|
07b7b743b4 | ||
|
|
64aff7b983 | ||
|
|
1299231a48 | ||
|
|
b6abb022f6 | ||
|
|
300d7bd99c | ||
|
|
012bebd66e | ||
|
|
e1d06ce4d8 | ||
|
|
52b98bdb87 | ||
|
|
62c117421a | ||
|
|
efe1fa89fe | ||
|
|
93d25a349f | ||
|
|
93ebd0f949 | ||
|
|
74a8005f92 | ||
|
|
5b4e58f0b8 | ||
|
|
b871a02ca3 | ||
|
|
6a20bbf607 | ||
|
|
dfa4d639e6 | ||
|
|
8b4024bf82 | ||
|
|
d89c6546e7 | ||
|
|
2710591429 | ||
|
|
02815cfb26 | ||
|
|
3f7cb5d9f8 | ||
|
|
46ad41e813 | ||
|
|
d4eca3a82a | ||
|
|
c03a088399 | ||
|
|
f81dda4eda | ||
|
|
1ceef5874e | ||
|
|
1b224bc4f2 | ||
|
|
530d6961c2 | ||
|
|
fe500882ef | ||
|
|
8358c26107 | ||
|
|
ad9a187522 | ||
|
|
8a41419b94 | ||
|
|
6ef7ec21cd | ||
|
|
b77297c68d | ||
|
|
df1d15ebd1 | ||
|
|
b3732e8b6c | ||
|
|
7e887666ce | ||
|
|
d6e6f51ced | ||
|
|
9e21de9c47 | ||
|
|
04c4cd9534 | ||
|
|
5821faec03 | ||
|
|
8bca244d59 | ||
|
|
79fa96cfbc | ||
|
|
b5cbe018e3 | ||
|
|
33f4b671d1 | ||
|
|
50c3e4c00f | ||
|
|
5486427d88 | ||
|
|
906bfa023c | ||
|
|
1c3518e18a | ||
|
|
88fd9e5c5e | ||
|
|
616211c1bc | ||
|
|
7a9c0e8c69 | ||
|
|
c09020102c | ||
|
|
af11d8cd58 | ||
|
|
93ba1b54f2 | ||
|
|
156e8d9df4 | ||
|
|
6a0dc1e2bb | ||
|
|
b95f2c97b9 | ||
|
|
d11a73c519 | ||
|
|
6fce1bd6bb | ||
|
|
5e60c14ce7 | ||
|
|
434448a2f9 | ||
|
|
8a916ce8ae | ||
|
|
a0546942b8 | ||
|
|
792bb98680 | ||
|
|
3151ca92db | ||
|
|
3c3da9e75d | ||
|
|
3f08f2e11e | ||
|
|
6fff179e39 | ||
|
|
3569076d3e | ||
|
|
2686031ac1 | ||
|
|
9051342d6d | ||
|
|
6f04613aed | ||
|
|
326f10bbbf | ||
|
|
6a9976742c | ||
|
|
1b770b01ae | ||
|
|
5611212ea9 | ||
|
|
b8f56bd10b | ||
|
|
bba0f3a230 | ||
|
|
9b8e4f4d5b | ||
|
|
b7cd026bd6 | ||
|
|
553e25cbb7 | ||
|
|
988eee82cf | ||
|
|
da1bf06764 | ||
|
|
13d17ba352 | ||
|
|
3d8616e75f | ||
|
|
e7884d8793 | ||
|
|
19d592566d | ||
|
|
afa140b6a6 | ||
|
|
ea6ebd0226 | ||
|
|
53a185083c | ||
|
|
f1e177fee7 | ||
|
|
75bc5bdc7e | ||
|
|
0d4588aa8d | ||
|
|
40753d1454 | ||
|
|
dd3c990a51 | ||
|
|
ef74ba7238 | ||
|
|
b4415a05d0 | ||
|
|
7817d15657 | ||
|
|
f25b4a3e12 | ||
|
|
8422659ee5 | ||
|
|
ef828cf2e1 | ||
|
|
546b8b5d25 | ||
|
|
a7f383f610 | ||
|
|
116c8dd6c5 | ||
|
|
1b5e8c3439 | ||
|
|
80031deab7 | ||
|
|
a005b8dce1 | ||
|
|
cc5af1a810 | ||
|
|
23be766c8b | ||
|
|
2f518b8b7c | ||
|
|
6cae776e48 | ||
|
|
cff2dc1379 | ||
|
|
fc250f98d0 | ||
|
|
8177db3601 | ||
|
|
b3e428c9de | ||
|
|
afc39cd2f7 | ||
|
|
7c9f40b6d9 | ||
|
|
8313ffcf7f | ||
|
|
0b12b30cb1 | ||
|
|
c27d02a929 | ||
|
|
4dbd8c9cae | ||
|
|
1056cef384 | ||
|
|
40d93ff33b | ||
|
|
1af06bbcc9 | ||
|
|
6c210b67d4 | ||
|
|
3a4405659e | ||
|
|
187bd9060c | ||
|
|
2e191084b0 | ||
|
|
7a98900b28 | ||
|
|
28e23e14b5 | ||
|
|
5f2807e693 | ||
|
|
e0b94f4780 | ||
|
|
587cced768 | ||
|
|
50964c6677 | ||
|
|
81eb0735d1 | ||
|
|
c7931bc6d5 | ||
|
|
b27e3e1a89 | ||
|
|
8db9331fed | ||
|
|
3e15e184ef | ||
|
|
6a457466df | ||
|
|
3a81fbd1b4 | ||
|
|
421c17c421 | ||
|
|
780d236d89 | ||
|
|
55483b726b | ||
|
|
157d041b6a | ||
|
|
32602f579b | ||
|
|
65e7df1417 | ||
|
|
b96d49df0f | ||
|
|
152838e998 | ||
|
|
9183c3897f | ||
|
|
b009cee877 | ||
|
|
41832369fd | ||
|
|
cc5cb394e0 | ||
|
|
b60542f0d1 | ||
|
|
dc8e7a2f39 | ||
|
|
34e6e7567f | ||
|
|
04d54e622a | ||
|
|
4c6e1e5fb3 | ||
|
|
b2b906f4fe | ||
|
|
40586b255c | ||
|
|
196e43aa48 | ||
|
|
fbe1a202c2 | ||
|
|
7dae166a69 | ||
|
|
72ce69410e | ||
|
|
7bb0d649c0 | ||
|
|
c110e173ac | ||
|
|
111f6513ac | ||
|
|
5367fd9fcb | ||
|
|
095ba14cc8 | ||
|
|
446863ad96 | ||
|
|
8527c363bb | ||
|
|
05127f4cfb | ||
|
|
653224c2ad | ||
|
|
406fc95501 | ||
|
|
01b6bf7850 | ||
|
|
2840e4e152 | ||
|
|
3948bb76d8 | ||
|
|
24c01d3e54 | ||
|
|
74e52187a3 | ||
|
|
602f3c59ba | ||
|
|
a33dace80b | ||
|
|
1d4012cabf | ||
|
|
dcb19150fc | ||
|
|
bc1dc0805e | ||
|
|
fec80113c7 | ||
|
|
5bdbfe1bc6 | ||
|
|
404ff93102 | ||
|
|
78df80f128 | ||
|
|
40c6a67631 | ||
|
|
91af51d38e | ||
|
|
a10d219049 | ||
|
|
0758ecfea8 | ||
|
|
05ab8f375e | ||
|
|
0bd428e45d | ||
|
|
720bc7ae42 | ||
|
|
056afc17bd | ||
|
|
35bb76ea82 | ||
|
|
3d4717b772 | ||
|
|
2b7685fa23 | ||
|
|
9d9040944a | ||
|
|
2b2f1bbfc9 | ||
|
|
2edcdc92f4 | ||
|
|
0e60c93cef | ||
|
|
0b38d6c763 | ||
|
|
ff49647de4 | ||
|
|
e0eba9d5a6 | ||
|
|
5ad3c65ae1 | ||
|
|
aca9af748b | ||
|
|
8cdcb89cef | ||
|
|
70fd330178 | ||
|
|
ad5e257600 | ||
|
|
4088793cc5 | ||
|
|
b6edc3dc08 | ||
|
|
fe4e2d620d | ||
|
|
f9691208d5 | ||
|
|
71850419c1 | ||
|
|
dfbb5b60de | ||
|
|
3b5f00439c | ||
|
|
9b7fe01648 | ||
|
|
2e784e006c | ||
|
|
08cf2f7cd1 | ||
|
|
be4e34d0c0 | ||
|
|
50f9b434e7 | ||
|
|
43bb7117b7 | ||
|
|
7320928235 | ||
|
|
d2498c96e0 | ||
|
|
82ae98d9d0 | ||
|
|
60d4a7beac | ||
|
|
9a3e60d4df | ||
|
|
e03e344dcd | ||
|
|
bf86bc3383 | ||
|
|
16dfab0aff | ||
|
|
b799515f84 | ||
|
|
417e6b1fee | ||
|
|
afcd5e3e36 | ||
|
|
b1ee434ddf | ||
|
|
6e8d564013 | ||
|
|
16907e4453 | ||
|
|
9c79adcb26 | ||
|
|
5b7a86ecc1 | ||
|
|
cae0023234 | ||
|
|
2f28d2a96b | ||
|
|
e3ea141bf3 | ||
|
|
b043dae149 | ||
|
|
3ca877f1df | ||
|
|
3e31668eb0 | ||
|
|
9e2213cbae | ||
|
|
a5c07042c1 | ||
|
|
7cda6628a6 | ||
|
|
020fbcf190 | ||
|
|
791c50fd33 | ||
|
|
2a19dd0d2e | ||
|
|
ded28dff15 | ||
|
|
baa1a4a2fc | ||
|
|
022dcd1909 | ||
|
|
e2aad48852 | ||
|
|
e17bc31b29 | ||
|
|
22c5b102ed | ||
|
|
0246e57d7f | ||
|
|
9460b08873 | ||
|
|
ed9b3ffce5 | ||
|
|
a3285fc187 | ||
|
|
e1173eb5eb | ||
|
|
72bc458c8e | ||
|
|
e067892ffc | ||
|
|
2f87be3f94 | ||
|
|
1291380611 | ||
|
|
ccac124b7a | ||
|
|
d8212d1337 | ||
|
|
030edaf72d | ||
|
|
c3f73ffb57 | ||
|
|
3e112fb1ac | ||
|
|
afd216308b | ||
|
|
b0c354637d | ||
|
|
c001bb876e | ||
|
|
3c6d1a1924 | ||
|
|
6f40dcb471 | ||
|
|
a561fd21d9 | ||
|
|
40c9e8472c | ||
|
|
e7338da3dc | ||
|
|
0c52739997 | ||
|
|
9d3c98232b | ||
|
|
5a0918afde | ||
|
|
4395d5a0ca | ||
|
|
d83927ae75 | ||
|
|
86c75b7a80 | ||
|
|
f1f8ffa456 | ||
|
|
4a5f8e30a8 | ||
|
|
e111ca02da | ||
|
|
6ec3589112 | ||
|
|
2ddba8d825 | ||
|
|
218f27306c | ||
|
|
fde2e6fa97 | ||
|
|
4d82ec1283 | ||
|
|
5b7c510577 | ||
|
|
0abadddb1a | ||
|
|
5f6e7de785 | ||
|
|
64631d5780 | ||
|
|
0386658d26 | ||
|
|
167d440b65 | ||
|
|
3e6ebab389 | ||
|
|
f90b5d48de | ||
|
|
016b0e9a8e | ||
|
|
51c828382f | ||
|
|
02d96d731f | ||
|
|
cac3a3520f | ||
|
|
b04db6ad2b | ||
|
|
f1aff2faab | ||
|
|
b1542be7b1 | ||
|
|
1db2ae3a45 | ||
|
|
e25d090ca9 | ||
|
|
5f4348c57d | ||
|
|
644cb687b9 | ||
|
|
bebe25c32e | ||
|
|
4c0fb12cf6 | ||
|
|
93cfd8c93a | ||
|
|
5f1bd5ec31 | ||
|
|
8fcc176d8b | ||
|
|
6420caca94 | ||
|
|
f84dda937b | ||
|
|
ef4ef583dc | ||
|
|
f103c02408 | ||
|
|
ef3f837800 | ||
|
|
170982a688 | ||
|
|
a852baac75 | ||
|
|
381a2e749a | ||
|
|
9ed5dcb031 | ||
|
|
e0ee18a993 | ||
|
|
0efc19a1b7 | ||
|
|
57103e0a9f | ||
|
|
946b0539d2 | ||
|
|
a9111f39af | ||
|
|
18d90a727e | ||
|
|
c090eb6a62 | ||
|
|
8f51993db2 | ||
|
|
8d2b8ae6b5 | ||
|
|
afbef23a51 | ||
|
|
e5ecaf01a0 | ||
|
|
b7b6816531 | ||
|
|
bfcd2569e9 | ||
|
|
d91baba240 | ||
|
|
6a90a10123 | ||
|
|
12e5f94e75 | ||
|
|
e1ae48f2e4 | ||
|
|
dcaf9166dc | ||
|
|
9e945d7547 | ||
|
|
648fa4b9ba | ||
|
|
e0b6133bf1 | ||
|
|
ac19782405 | ||
|
|
858356610c | ||
|
|
040ad3293a | ||
|
|
66270fded0 | ||
|
|
26a8747509 | ||
|
|
ac83633888 | ||
|
|
c6ead9d7dd | ||
|
|
8c3be3ffb2 | ||
|
|
014d760f3d | ||
|
|
8d5f4a93ed | ||
|
|
37631b41ea | ||
|
|
03806629b8 | ||
|
|
83e0934864 | ||
|
|
2dc3ab1840 | ||
|
|
7b088d611d | ||
|
|
f0ebd808d7 | ||
|
|
0bb2b10b3b | ||
|
|
fa94ebfbd1 | ||
|
|
c18c670765 | ||
|
|
f410da0ed2 | ||
|
|
58237d0e7d | ||
|
|
c8abc79d9b | ||
|
|
9455a66be8 | ||
|
|
05085d8e23 | ||
|
|
8563c05baf | ||
|
|
67bf54a9f9 | ||
|
|
408fbe4f76 | ||
|
|
cb5e494815 | ||
|
|
954591d2db | ||
|
|
2f1a67ef0d | ||
|
|
d7b1ab8e43 | ||
|
|
1d44a0cdfa | ||
|
|
1fa41c4d0a | ||
|
|
0deb7cc09a | ||
|
|
fe2e2bdff1 | ||
|
|
95093ab0af | ||
|
|
1dd7c82af6 | ||
|
|
64ef53402d | ||
|
|
37c5e121c4 | ||
|
|
879525faac | ||
|
|
6ed9107df0 | ||
|
|
c320132289 | ||
|
|
ae9bdc1d61 | ||
|
|
c0f13ef4ac | ||
|
|
f40144e1a9 | ||
|
|
0ccf14801e | ||
|
|
273ac62ec2 | ||
|
|
545c26e5fe | ||
|
|
22953cdb78 | ||
|
|
fe0481c304 | ||
|
|
fde56164cd | ||
|
|
4fe691de92 | ||
|
|
c23c54f500 | ||
|
|
9419b7392d | ||
|
|
09e6c86c46 | ||
|
|
7e78c9322c | ||
|
|
31810a97e1 | ||
|
|
8e4c85d816 | ||
|
|
970be58847 | ||
|
|
d60c184424 | ||
|
|
f38c150f6a | ||
|
|
c8eaa3f383 | ||
|
|
be8f11fe5a | ||
|
|
b272b3f331 | ||
|
|
4b60c922ef | ||
|
|
25ee99f93a | ||
|
|
d32583dd7f | ||
|
|
d64acbb5a9 | ||
|
|
24c4bc0dd4 | ||
|
|
58a3e35c51 | ||
|
|
25bbd28527 | ||
|
|
385d2db445 | ||
|
|
eae6d355f8 | ||
|
|
5be46d0bb7 | ||
|
|
5b04192945 | ||
|
|
e6b6c0e3ab | ||
|
|
dfb6544171 | ||
|
|
3596610f40 | ||
|
|
ccddeb4cda | ||
|
|
d39cc08b66 | ||
|
|
c4ff1ca304 | ||
|
|
b21f0035d7 | ||
|
|
ad9439eef2 | ||
|
|
72321fc106 | ||
|
|
541019eb98 | ||
|
|
15bffc6b16 | ||
|
|
901242c1e1 | ||
|
|
fd0e0bb4c9 | ||
|
|
53bece2186 | ||
|
|
1a881e4f2b | ||
|
|
488d1b663a | ||
|
|
8f5d1709a1 | ||
|
|
bc700d58ae | ||
|
|
f8d76066c5 | ||
|
|
b8fb37b9f6 | ||
|
|
4a28d1f800 | ||
|
|
b25696a1fb | ||
|
|
b49d37ca54 | ||
|
|
20b62b8841 | ||
|
|
83101eefce | ||
|
|
86865d155a | ||
|
|
3575be7742 | ||
|
|
b7d823a077 | ||
|
|
a47617cad0 | ||
|
|
ee84f31f42 | ||
|
|
ace861f722 | ||
|
|
2e62be3ebb | ||
|
|
48e0250649 | ||
|
|
6c72507bca | ||
|
|
63b8c6e4b2 | ||
|
|
ffdb7fa795 | ||
|
|
652ea6bd2a | ||
|
|
3bc5952f7e | ||
|
|
7520dadbdd | ||
|
|
8a4be431f6 | ||
|
|
c32e452db8 | ||
|
|
24bd8b2e42 | ||
|
|
227fbd63aa | ||
|
|
c83cec341b | ||
|
|
7404ee4531 | ||
|
|
e0a9bef6ce | ||
|
|
428b9f2758 | ||
|
|
0d1ad6e1df | ||
|
|
6ce2f109bf | ||
|
|
3b9fb71dd1 | ||
|
|
97552aec5f | ||
|
|
d6d808d185 | ||
|
|
b75a6cdb76 | ||
|
|
b31deff0fb | ||
|
|
ee978fdde8 | ||
|
|
e867fb82b9 | ||
|
|
b17ac6ec0b | ||
|
|
dd2133458e | ||
|
|
e7ecfd3954 | ||
|
|
c8b17978a9 | ||
|
|
a4733025ce | ||
|
|
1c7601a2b5 | ||
|
|
052cdc40dc | ||
|
|
332ed8e50b | ||
|
|
4138c9244f | ||
|
|
57a2dc9fc1 | ||
|
|
9bb9402e89 | ||
|
|
3ef1171667 | ||
|
|
84b61fac88 | ||
|
|
c10393b495 | ||
|
|
d3d8ef44a0 | ||
|
|
d5ee925e62 | ||
|
|
47d216caae | ||
|
|
406d6b5544 | ||
|
|
ab87977c08 | ||
|
|
f4dfc22f8e | ||
|
|
785a1d14fb | ||
|
|
d68a4b85f4 | ||
|
|
cbd71df574 | ||
|
|
c05cbca0b0 | ||
|
|
2e7d869ccc | ||
|
|
bac3c19bec | ||
|
|
81b9a48437 | ||
|
|
271640b66d | ||
|
|
6b0d4184d5 | ||
|
|
d351ef430c | ||
|
|
e6f128e2a7 | ||
|
|
080bb594b2 | ||
|
|
f1696411d9 | ||
|
|
5580ca82ac | ||
|
|
7f2c43cd62 | ||
|
|
372cfe1601 | ||
|
|
d73fdb1d33 | ||
|
|
821190004c | ||
|
|
8321773a22 | ||
|
|
3a943a3b9a | ||
|
|
6d380c629a | ||
|
|
5fd105496f | ||
|
|
b3e622c914 | ||
|
|
c4ed768c9e | ||
|
|
ef11f97a75 | ||
|
|
0b41469527 | ||
|
|
8859172025 | ||
|
|
9c8b081906 | ||
|
|
300d522eb0 | ||
|
|
203e040be1 | ||
|
|
fdcea0de05 | ||
|
|
db6fc234b7 | ||
|
|
e6838ace6b | ||
|
|
4b8c1de647 | ||
|
|
c2262773e6 | ||
|
|
f806768039 | ||
|
|
83a156d72b | ||
|
|
724c0d3eb0 | ||
|
|
dd190dede6 | ||
|
|
5b8e0c4d99 | ||
|
|
b2b8d5457d | ||
|
|
16ef0b2d41 | ||
|
|
d097de7fdf | ||
|
|
101394c714 | ||
|
|
06df825dab | ||
|
|
f7efc360a0 | ||
|
|
ad74f259de | ||
|
|
005225d5f9 | ||
|
|
b9726615dd | ||
|
|
01941d6b2a | ||
|
|
b21c27b219 | ||
|
|
9bfcaa33c6 | ||
|
|
1c60e09f13 | ||
|
|
15a5332428 | ||
|
|
a5643e3738 | ||
|
|
9356f64c55 | ||
|
|
246471bc91 | ||
|
|
896e1b45f0 | ||
|
|
4032ed32ae | ||
|
|
33ce79f89d | ||
|
|
3bc13517b2 | ||
|
|
a36f7c6c07 | ||
|
|
ba688f56aa | ||
|
|
8684f8f628 | ||
|
|
2564d1be42 | ||
|
|
4da3d93f6e | ||
|
|
0045ce4286 | ||
|
|
418f4d20ae | ||
|
|
8a7e22e63e | ||
|
|
9e4ffd1cce | ||
|
|
6fe455c687 | ||
|
|
d8c8f04860 | ||
|
|
e8f433643f | ||
|
|
4f95e6a372 | ||
|
|
259ffb5267 | ||
|
|
aab0fd644f | ||
|
|
e7a817e67a | ||
|
|
0bbf55e46f | ||
|
|
c34e5a727d | ||
|
|
0d8477ea8a |
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
./.*
|
||||
./*.md
|
||||
./*.yaml
|
||||
./apis
|
||||
./deploy
|
||||
./Dockerfile
|
||||
./generated/1.1*
|
||||
./hack/lib/tilt/
|
||||
./internal/mocks
|
||||
./LICENSE
|
||||
./site/
|
||||
./test
|
||||
**/*_test.go
|
||||
15
.github/codecov.yml
vendored
Normal file
15
.github/codecov.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
codecov:
|
||||
strict_yaml_branch: main
|
||||
require_ci_to_pass: no
|
||||
notify:
|
||||
wait_for_ci: no
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
ignore:
|
||||
- cmd/local-user-authenticator/
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,8 +14,11 @@
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# goland
|
||||
# GoLand
|
||||
.idea
|
||||
|
||||
# Intermediate files used by Tilt
|
||||
/hack/lib/tilt/build
|
||||
|
||||
# MacOS Desktop Services Store
|
||||
.DS_Store
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# https://github.com/golangci/golangci-lint#config-file
|
||||
run:
|
||||
deadline: 1m
|
||||
skip-dirs:
|
||||
- generated
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
@@ -30,7 +32,6 @@ linters:
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- goerr113
|
||||
- goheader
|
||||
- goimports
|
||||
- golint
|
||||
@@ -56,15 +57,18 @@ issues:
|
||||
linters:
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- goerr113
|
||||
|
||||
linters-settings:
|
||||
funlen:
|
||||
lines: 125
|
||||
lines: 150
|
||||
statements: 50
|
||||
goheader:
|
||||
values:
|
||||
regexp:
|
||||
# YYYY or YYYY-YYYY
|
||||
YEARS: \d\d\d\d(-\d\d\d\d)?
|
||||
template: |-
|
||||
Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
Copyright {{YEARS}} the Pinniped contributors. All Rights Reserved.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
goimports:
|
||||
local-prefixes: go.pinniped.dev
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
exclude: '^(generated|hack/lib/tilt/tilt_modules)/'
|
||||
# This is a configuration for https://pre-commit.com/.
|
||||
# On macOS, try `brew install pre-commit` and then run `pre-commit install`.
|
||||
exclude: '^(site|generated|hack/lib/tilt/tilt_modules)/'
|
||||
repos:
|
||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
@@ -15,3 +17,9 @@ repos:
|
||||
- id: detect-private-key
|
||||
exclude: testdata
|
||||
- id: mixed-line-ending
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: validate-copyright-year
|
||||
name: Validate copyright year
|
||||
entry: hack/check-copyright-year.sh
|
||||
language: script
|
||||
|
||||
33
ADOPTERS.md
33
ADOPTERS.md
@@ -1,9 +1,32 @@
|
||||
# Pinniped Adopters
|
||||
|
||||
These organizations are using Pinniped.
|
||||
If you're using Pinniped and want to add your organization to this
|
||||
list, [follow these directions](#adding-your-organization-to-the-list-of-adopters)!
|
||||
|
||||
* [VMware Tanzu](https://tanzu.vmware.com/) ([Tanzu Mission Control](https://tanzu.vmware.com/mission-control))
|
||||
## Organizations using Pinniped
|
||||
|
||||
If you are using Pinniped and are not on this list, you can open a [pull
|
||||
request](https://github.com/vmware-tanzu/pinniped/issues/new?template=feature-proposal.md)
|
||||
to add yourself.
|
||||
<a href="https://tanzu.vmware.com/tanzu" border="0" target="_blank"><img alt="vmware-tanzu" src="site/themes/pinniped/static/img/vmware-tanzu.svg" height="50"></a>
|
||||
|
||||
<a href="https://kubeapps.com/" border="0" target="_blank"><img alt="kubeapps" src="site/themes/pinniped/static/img/kubeapps.svg" height="50"></a>
|
||||
|
||||
## Solutions built with Pinniped
|
||||
|
||||
Below is a list of solutions where Pinniped is being used as a component.
|
||||
|
||||
**[Kubeapps](https://kubeapps.com/)**
|
||||
|
||||
Kubeapps uses Pinniped to [enable SSO authentication](https://github.com/kubeapps/kubeapps/blob/master/docs/user/using-an-OIDC-provider-with-pinniped.md) when running on clusters where SSO cannot be configured for the cluster API server.
|
||||
|
||||
**[VMware Tanzu Kubernetes Grid (TKG)](https://tanzu.vmware.com/kubernetes-grid)**
|
||||
|
||||
TKG uses Pinniped to provide a seamless SSO experience across management and workload clusters.
|
||||
|
||||
**[VMware Tanzu Mission Control (TMC)](https://tanzu.vmware.com/mission-control)**
|
||||
|
||||
TMC uses Pinniped to provide a uniform authentication experience across all attached clusters.
|
||||
|
||||
## Adding your organization to the list of adopters
|
||||
|
||||
If you are using Pinniped and would like to be included in the list of Pinniped Adopters, add an SVG version of your logo that is less than 150 KB to
|
||||
the [img directory](https://github.com/vmware-tanzu/pinniped/tree/main/site/themes/pinniped/static/img) in this repo and submit a pull request with your change including 1-2 sentences describing how your organization is using Pinniped. Name the image file something that
|
||||
reflects your company (e.g., if your company is called Acme, name the image acme.svg). Please feel free to send us a message in [#pinniped](https://kubernetes.slack.com/archives/C01BW364RJA) with any questions you may have.
|
||||
|
||||
@@ -8,24 +8,17 @@ Please see the [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||
|
||||
## Project Scope
|
||||
|
||||
Learn about the [scope](doc/scope.md) of the project.
|
||||
See [SCOPE.md](./SCOPE.md) for some guidelines about what we consider in and out of scope for Pinniped.
|
||||
|
||||
## Meeting with the Maintainers
|
||||
## Community Meetings
|
||||
|
||||
The maintainers aspire to hold a video conference every other week with the Pinniped community.
|
||||
Any community member may request to add topics to the agenda by contacting a [maintainer](MAINTAINERS.md)
|
||||
in advance, or by attending and raising the topic during time remaining after the agenda is covered.
|
||||
Typical agenda items include topics regarding the roadmap, feature requests, bug reports, pull requests, etc.
|
||||
A [public document](https://docs.google.com/document/d/1qYA35wZV-6bxcH5375vOnIGkNBo7e4OROgsV4Sj8WjQ)
|
||||
tracks the agendas and notes for these meetings.
|
||||
Pinniped is better because of our contributors and maintainers. It is because of you that we can bring great software to the community. Please join us during our online community meetings, occuring every first and third Thursday of the month at 9AM PT / 12PM PT. Use [this Zoom Link](https://vmware.zoom.us/j/93798188973?pwd=T3pIMWxReEQvcWljNm1admRoZTFSZz09) to attend and add any agenda items you wish to discuss to [the notes document](https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ?view). Join our [Google Group](https://groups.google.com/u/1/g/project-pinniped) to receive invites to this meeting.
|
||||
|
||||
These meetings are currently scheduled for the first and third Thursday mornings of each month
|
||||
at 9 AM Pacific Time, using this [Zoom meeting](https://VMware.zoom.us/j/94638309756?pwd=V3NvRXJIdDg5QVc0TUdFM2dYRzgrUT09).
|
||||
If the meeting day falls on a US holiday, please consider that occurrence of the meeting to be canceled.
|
||||
|
||||
## Discussion
|
||||
|
||||
Got a question, comment, or idea? Please don't hesitate to reach out via the GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions) tab at the top of this page.
|
||||
Got a question, comment, or idea? Please don't hesitate to reach out via the GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions) tab at the top of this page or reach out in Kubernetes Slack Workspace within the [#pinniped channel](https://kubernetes.slack.com/archives/C01BW364RJA).
|
||||
|
||||
## Issues
|
||||
|
||||
@@ -97,39 +90,35 @@ docker build .
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
- [`kind`](https://kind.sigs.k8s.io/docs/user/quick-start)
|
||||
- [`tilt`](https://docs.tilt.dev/install.html)
|
||||
- [`ytt`](https://carvel.dev/#getting-started)
|
||||
- [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
- [`chromedriver`](https://chromedriver.chromium.org/) (and [Chrome](https://www.google.com/chrome/))
|
||||
- [`docker`](https://www.docker.com/)
|
||||
- `htpasswd` (installed by default on MacOS, usually found in `apache2-utils` package for linux)
|
||||
- [`kapp`](https://carvel.dev/#getting-started)
|
||||
- [`kind`](https://kind.sigs.k8s.io/docs/user/quick-start)
|
||||
- [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
- [`ytt`](https://carvel.dev/#getting-started)
|
||||
|
||||
On macOS, these tools can be installed with [Homebrew](https://brew.sh/) (assuming you have Chrome installed already):
|
||||
|
||||
```bash
|
||||
brew install kind tilt-dev/tap/tilt k14s/tap/ytt kubectl chromedriver
|
||||
brew install kind k14s/tap/ytt k14s/tap/kapp kubectl chromedriver && brew cask install docker
|
||||
```
|
||||
|
||||
1. Create a local Kubernetes cluster using `kind`:
|
||||
1. Create a kind cluster, compile, create container images, and install Pinniped and supporting dependencies using:
|
||||
|
||||
```bash
|
||||
./hack/kind-up.sh
|
||||
./hack/prepare-for-integration-tests.sh
|
||||
```
|
||||
|
||||
1. Install Pinniped and supporting dependencies using `tilt`:
|
||||
|
||||
```bash
|
||||
./hack/tilt-up.sh
|
||||
```
|
||||
|
||||
Tilt will continue running and live-updating the Pinniped deployment whenever the code changes.
|
||||
|
||||
1. Run the Pinniped integration tests:
|
||||
|
||||
```bash
|
||||
source /tmp/integration-test-env && go test -v -count 1 ./test/integration
|
||||
source /tmp/integration-test-env && go test -v -count 1 -timeout 0 ./test/integration
|
||||
```
|
||||
|
||||
To uninstall the test environment, run `./hack/tilt-down.sh`.
|
||||
1. After making production code changes, recompile, redeploy, and run tests again by repeating the same
|
||||
commands described above. If there are only test code changes, then simply run the tests again.
|
||||
|
||||
To destroy the local Kubernetes cluster, run `./hack/kind-down.sh`.
|
||||
|
||||
### Observing Tests on the Continuous Integration Environment
|
||||
|
||||
54
Dockerfile
54
Dockerfile
@@ -1,35 +1,41 @@
|
||||
# Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
# syntax = docker/dockerfile:1.0-experimental
|
||||
|
||||
# Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM golang:1.15.3 as build-env
|
||||
FROM golang:1.16.2 as build-env
|
||||
|
||||
WORKDIR /work
|
||||
# Get dependencies first so they can be cached as a layer
|
||||
COPY go.* ./
|
||||
COPY generated/1.19/apis/go.* ./generated/1.19/apis/
|
||||
COPY generated/1.19/client/go.* ./generated/1.19/client/
|
||||
RUN go mod download
|
||||
|
||||
# Copy only the production source code to avoid cache misses when editing other files
|
||||
COPY generated ./generated
|
||||
COPY cmd ./cmd
|
||||
COPY internal ./internal
|
||||
COPY tools ./tools
|
||||
COPY hack ./hack
|
||||
COPY . .
|
||||
ARG GOPROXY
|
||||
|
||||
# Build the executable binary (CGO_ENABLED=0 means static linking)
|
||||
RUN mkdir out \
|
||||
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/pinniped-concierge/... \
|
||||
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/pinniped-supervisor/... \
|
||||
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out ./cmd/local-user-authenticator/...
|
||||
# Pass in GOCACHE (build cache) and GOMODCACHE (module cache) so they
|
||||
# can be re-used between image builds.
|
||||
RUN \
|
||||
--mount=type=cache,target=/cache/gocache \
|
||||
--mount=type=cache,target=/cache/gomodcache \
|
||||
mkdir out && \
|
||||
GOCACHE=/cache/gocache \
|
||||
GOMODCACHE=/cache/gomodcache \
|
||||
CGO_ENABLED=0 \
|
||||
GOOS=linux \
|
||||
GOARCH=amd64 \
|
||||
go build -v -ldflags "$(hack/get-ldflags.sh)" -o out \
|
||||
./cmd/pinniped-concierge/... \
|
||||
./cmd/pinniped-supervisor/... \
|
||||
./cmd/local-user-authenticator/...
|
||||
|
||||
# Use a runtime image based on Debian slim
|
||||
FROM debian:10.6-slim
|
||||
# Use a Debian slim image to grab a reasonable default CA bundle.
|
||||
FROM debian:10.9-slim AS get-ca-bundle-env
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* /var/cache/debconf/*
|
||||
|
||||
# Copy the binaries from the build-env stage
|
||||
COPY --from=build-env /work/out/pinniped-concierge /usr/local/bin/pinniped-concierge
|
||||
COPY --from=build-env /work/out/pinniped-supervisor /usr/local/bin/pinniped-supervisor
|
||||
COPY --from=build-env /work/out/local-user-authenticator /usr/local/bin/local-user-authenticator
|
||||
# Use a runtime image based on Debian slim.
|
||||
FROM debian:10.9-slim
|
||||
COPY --from=get-ca-bundle-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Copy the binaries from the build-env stage.
|
||||
COPY --from=build-env /work/out/ /usr/local/bin/
|
||||
|
||||
# Document the ports
|
||||
EXPOSE 8080 8443
|
||||
|
||||
@@ -5,7 +5,9 @@ This is the current list of maintainers for the Pinniped project.
|
||||
| Maintainer | GitHub ID | Affiliation |
|
||||
| --------------- | --------- | ----------- |
|
||||
| Andrew Keesler | [ankeesler](https://github.com/ankeesler) | [VMware](https://www.github.com/vmware/) |
|
||||
| Margo Crawford | [margocrawf](https://github.com/margocrawf) | [VMware](https://www.github.com/vmware/) |
|
||||
| Matt Moyer | [mattmoyer](https://github.com/mattmoyer) | [VMware](https://www.github.com/vmware/) |
|
||||
| Mo Khan | [enj](https://github.com/enj) | [VMware](https://www.github.com/vmware/) |
|
||||
| Pablo Schuhmacher | [pabloschuhmacher](https://github.com/pabloschuhmacher) | [VMware](https://www.github.com/vmware/) |
|
||||
| Ryan Richard | [cfryanr](https://github.com/cfryanr) | [VMware](https://www.github.com/vmware/) |
|
||||
|
||||
|
||||
37
README.md
37
README.md
@@ -1,4 +1,4 @@
|
||||
<img src="doc/img/pinniped_logo.svg" alt="Pinniped Logo" width="100%"/>
|
||||
<img src="site/content/docs/img/pinniped_logo.svg" alt="Pinniped Logo" width="100%"/>
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -10,7 +10,7 @@ install procedure across all types and origins of Kubernetes clusters,
|
||||
declarative configuration via Kubernetes APIs, enterprise-grade integrations
|
||||
with IDPs, and distribution-specific integration strategies.
|
||||
|
||||
### Example Use Cases
|
||||
### Example use cases
|
||||
|
||||
* Your team uses a large enterprise IDP, and has many clusters that they
|
||||
manage. Pinniped provides:
|
||||
@@ -23,28 +23,43 @@ with IDPs, and distribution-specific integration strategies.
|
||||
|
||||
### Architecture
|
||||
|
||||
Pinniped offers credential exchange to enable a user to exchange an external IDP
|
||||
credential for a short-lived, cluster-specific credential. Pinniped supports various
|
||||
IDP types and implements different integration strategies for various Kubernetes
|
||||
The Pinniped Supervisor component offers identity federation to enable a user to
|
||||
access multiple clusters with a single daily login to their external IDP. The
|
||||
Pinniped Supervisor supports various external [IDP
|
||||
types](https://github.com/vmware-tanzu/pinniped/tree/main/generated/1.20#k8s-api-idp-supervisor-pinniped-dev-v1alpha1).
|
||||
|
||||
The Pinniped Concierge component offers credential exchange to enable a user to
|
||||
exchange an external credential for a short-lived, cluster-specific
|
||||
credential. Pinniped supports various [authentication
|
||||
methods](https://github.com/vmware-tanzu/pinniped/tree/main/generated/1.20#authenticationconciergepinnipeddevv1alpha1)
|
||||
and implements different integration strategies for various Kubernetes
|
||||
distributions to make authentication possible.
|
||||
|
||||
To learn more, see [doc/architecture.md](doc/architecture.md).
|
||||
The Pinniped Concierge can be configured to hook into the Pinniped Supervisor's
|
||||
federated credentials, or it can authenticate users directly via external IDP
|
||||
credentials.
|
||||
|
||||
<img src="doc/img/pinniped_architecture.svg" alt="Pinniped Architecture Sketch" width="300px"/>
|
||||
To learn more, see [architecture](https://pinniped.dev/docs/background/architecture/).
|
||||
|
||||
## Trying Pinniped
|
||||
## Getting started with Pinniped
|
||||
|
||||
Care to kick the tires? It's easy to [install and try Pinniped](doc/demo.md).
|
||||
Care to kick the tires? It's easy to [install and try Pinniped](https://pinniped.dev/docs/demo/).
|
||||
|
||||
## Community meetings
|
||||
|
||||
Pinniped is better because of our contributors and maintainers. It is because of you that we can bring great software to the community. Please join us during our online community meetings, occurring every first and third Thursday of the month at 9 AM PT / 12 PM PT. Use [this Zoom Link](https://vmware.zoom.us/j/93798188973?pwd=T3pIMWxReEQvcWljNm1admRoZTFSZz09) to attend and add any agenda items you wish to discuss to [the notes document](https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ?view). Join our [Google Group](https://groups.google.com/g/project-pinniped) to receive invites to this meeting.
|
||||
|
||||
If the meeting day falls on a US holiday, please consider that occurrence of the meeting to be canceled.
|
||||
|
||||
## Discussion
|
||||
|
||||
Got a question, comment, or idea? Please don't hesitate to reach out via the GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions) tab at the top of this page.
|
||||
Got a question, comment, or idea? Please don't hesitate to reach out via the GitHub [Discussions](https://github.com/vmware-tanzu/pinniped/discussions) tab at the top of this page or reach out in Kubernetes Slack Workspace within the [#pinniped channel](https://kubernetes.slack.com/archives/C01BW364RJA).
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are welcome. Before contributing, please see the [contributing guide](CONTRIBUTING.md).
|
||||
|
||||
## Reporting Security Vulnerabilities
|
||||
## Reporting security vulnerabilities
|
||||
|
||||
Please follow the procedure described in [SECURITY.md](SECURITY.md).
|
||||
|
||||
|
||||
50
ROADMAP.md
Normal file
50
ROADMAP.md
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
## **Pinniped Project Roadmap**
|
||||
|
||||
|
||||
###
|
||||
**About this document**
|
||||
|
||||
This document provides a link to the[ Pinniped Project issues](https://github.com/vmware-tanzu/pinniped/issues) list that serves as the up to date description of items that are in the Pinniped release pipeline. Most items are gathered from the community or include a feedback loop with the community. This should serve as a reference point for Pinniped users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan.
|
||||
|
||||
|
||||
###
|
||||
**How to help?**
|
||||
|
||||
Discussion on the roadmap can take place in threads under [Issues](https://github.com/vmware-tanzu/pinniped/issues) or in [community meetings](https://github.com/vmware-tanzu/pinniped/blob/main/CONTRIBUTING.md#meeting-with-the-maintainers). Please open and comment on an issue if you want to provide suggestions and feedback to an item in the roadmap. Please review the roadmap to avoid potential duplicated effort.
|
||||
|
||||
|
||||
###
|
||||
**Need an idea for a contribution?**
|
||||
|
||||
We’ve created an [Opportunity Areas](https://github.com/vmware-tanzu/pinniped/discussions/483) discussion thread that outlines some areas we believe are excellent starting points for the community to get involved. In that discussion we’ve included specific work items that one might consider that also support the high-level items presented in our roadmap.
|
||||
|
||||
|
||||
###
|
||||
**How to add an item to the roadmap?**
|
||||
|
||||
Please open an issue to track any initiative on the roadmap of Pinniped (usually driven by new feature requests). We will work with and rely on our community to focus our efforts to improve Pinniped.
|
||||
|
||||
|
||||
###
|
||||
**Current Roadmap**
|
||||
|
||||
The following table includes the current roadmap for Pinniped. If you have any questions or would like to contribute to Pinniped, please attend a [community meeting](https://github.com/vmware-tanzu/pinniped/blob/main/CONTRIBUTING.md#meeting-with-the-maintainers) to discuss with our team. If you don't know where to start, we are always looking for contributors that will help us reduce technical, automation, and documentation debt. Please take the timelines & dates as proposals and goals. Priorities and requirements change based on community feedback, roadblocks encountered, community contributions, etc. If you depend on a specific item, we encourage you to attend community meetings to get updated status information, or help us deliver that feature by contributing to Pinniped.
|
||||
|
||||
|
||||
|
||||
Last Updated: March 2021
|
||||
Theme|Description|Timeline|
|
||||
|--|--|--|
|
||||
|Impersonation Proxy|Adds support for more types of clusters (managed services)|Mar 2021|
|
||||
|LDAP Support|Extends upstream IDP protocols|Apr 2021|
|
||||
|Device Code Flow|Add support for OAuth 2.0 Device Authorization Grant in the Pinniped CLI and Supervisor|Apr 2021|
|
||||
|Improved Documentation|Reorganizing and improving Pinniped docs; new how-to guides and tutorials|May 2021|
|
||||
|CLI Improvements|Improving CLI UX for setting up Supervisor IDPs|May 2021|
|
||||
|Multiple IDPs|Support for multiple upstream IDPs to be configured simultaneously|Jun 2021|
|
||||
|Improving Security Posture|Offer the best security posture for Kubernetes cluster authentication|Exploring/Ongoing|
|
||||
|Improve our CI/CD systems|Upgrade tests; make Kind more efficient and reliable for CI ; Windows tests; performance tests; scale tests; soak tests|Exploring/Ongoing|
|
||||
|Telemetry|Adding some useful phone home metrics as well as some vanity metrics|Exploring/Ongoing|
|
||||
|Observability|Expose Pinniped metrics through Prometheus Integration|Exploring/Ongoing|
|
||||
|
||||
|
||||
32
SCOPE.md
Normal file
32
SCOPE.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Project Scope
|
||||
|
||||
The Pinniped project is guided by the following principles.
|
||||
|
||||
- Pinniped lets you plug any external identity providers into Kubernetes.
|
||||
These integrations follow enterprise-grade security principles.
|
||||
- Pinniped is easy to install and use on any Kubernetes cluster via distribution-specific integration mechanisms.
|
||||
- Pinniped uses a declarative configuration via Kubernetes APIs.
|
||||
- Pinniped provides optimal user experience when authenticating to many clusters at one time.
|
||||
- Pinniped provides enterprise-grade security posture via secure defaults and revocable or very short-lived credentials.
|
||||
- Where possible, Pinniped will contribute ideas and code to upstream Kubernetes.
|
||||
|
||||
When contributing to Pinniped, please consider whether your contribution follows
|
||||
these guiding principles.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
The following items are out of scope for the Pinniped project.
|
||||
|
||||
- Authorization.
|
||||
- Standalone identity provider for general use.
|
||||
- Machine-to-machine (service) identity.
|
||||
- Running outside of Kubernetes.
|
||||
|
||||
## Roadmap
|
||||
|
||||
See our [open milestones][milestones] and the [`priority/backlog` label][backlog] for an idea about what's next on our roadmap.
|
||||
|
||||
For more details on proposing features and bugs, check out our [contributing](./CONTRIBUTING.md) doc.
|
||||
|
||||
[milestones]: https://github.com/vmware-tanzu/pinniped/milestones
|
||||
[backlog]: https://github.com/vmware-tanzu/pinniped/labels/priority%2Fbacklog
|
||||
100
SECURITY.md
100
SECURITY.md
@@ -1,12 +1,92 @@
|
||||
# Reporting a Vulnerability
|
||||
# Security Release Process
|
||||
|
||||
Pinniped development is sponsored by VMware, and the Pinniped team encourages users
|
||||
who become aware of a security vulnerability in Pinniped to report any potential
|
||||
vulnerabilities found to security@vmware.com. If possible, please include a description
|
||||
of the effects of the vulnerability, reproduction steps, and a description of in which
|
||||
version of Pinniped or its dependencies the vulnerability was discovered.
|
||||
The use of encrypted email is encouraged. The public PGP key can be found at https://kb.vmware.com/kb/1055.
|
||||
Pinniped provides identity services for Kubernetes clusters. The community has adopted this security disclosure and response policy to ensure we responsibly handle critical issues.
|
||||
|
||||
The Pinniped team hopes that users encountering a new vulnerability will contact
|
||||
us privately as it is in the best interests of our users that the Pinniped team has
|
||||
an opportunity to investigate and confirm a suspected vulnerability before it becomes public knowledge.
|
||||
## Supported Versions
|
||||
|
||||
As of right now, only the latest version of Pinniped is supported.
|
||||
|
||||
## Reporting a Vulnerability - Private Disclosure Process
|
||||
|
||||
Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to Pinniped privately, to minimize attacks against current users of Pinniped before they are fixed. Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible. This information could be kept entirely internal to the project.
|
||||
|
||||
If you know of a publicly disclosed security vulnerability for Pinniped, please **IMMEDIATELY** contact the VMware Security Team (security@vmware.com). The use of encrypted email is encouraged. The public PGP key can be found at https://kb.vmware.com/kb/1055.
|
||||
|
||||
**IMPORTANT: Do not file public issues on GitHub for security vulnerabilities**
|
||||
|
||||
To report a vulnerability or a security-related issue, please contact the VMware email address with the details of the vulnerability. The email will be fielded by the VMware Security Team and then shared with the Pinniped maintainers who have committer and release permissions. Emails will be addressed within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime. Do not report non-security-impacting bugs through this channel. Use [GitHub issues](https://github.com/vmware-tanzu/pinniped/issues/new/choose) instead.
|
||||
|
||||
## Proposed Email Content
|
||||
|
||||
Provide a descriptive subject line and in the body of the email include the following information:
|
||||
|
||||
* Basic identity information, such as your name and your affiliation or company.
|
||||
* Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and logs are all helpful to us).
|
||||
* Description of the effects of the vulnerability on Pinniped and the related hardware and software configurations, so that the VMware Security Team can reproduce it.
|
||||
* How the vulnerability affects Pinniped usage and an estimation of the attack surface, if there is one.
|
||||
* List other projects or dependencies that were used in conjunction with Pinniped to produce the vulnerability.
|
||||
|
||||
## When to report a vulnerability
|
||||
|
||||
* When you think Pinniped has a potential security vulnerability.
|
||||
* When you suspect a potential vulnerability but you are unsure that it impacts Pinniped.
|
||||
* When you know of or suspect a potential vulnerability on another project that is used by Pinniped.
|
||||
|
||||
## Patch, Release, and Disclosure
|
||||
|
||||
The VMware Security Team will respond to vulnerability reports as follows:
|
||||
|
||||
1. The Security Team will investigate the vulnerability and determine its effects and criticality.
|
||||
2. If the issue is not deemed to be a vulnerability, the Security Team will follow up with a detailed reason for rejection.
|
||||
3. The Security Team will initiate a conversation with the reporter within 3 business days.
|
||||
4. If a vulnerability is acknowledged and the timeline for a fix is determined, the Security Team will work on a plan to communicate with the appropriate community, including identifying mitigating steps that affected users can take to protect themselves until the fix is rolled out.
|
||||
5. The Security Team will also create a [CVSS](https://www.first.org/cvss/specification-document) using the [CVSS Calculator](https://www.first.org/cvss/calculator/3.0). The Security Team makes the final call on the calculated CVSS; it is better to move quickly than making the CVSS perfect. Issues may also be reported to [Mitre](https://cve.mitre.org/) using this [scoring calculator](https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator). The CVE will initially be set to private.
|
||||
6. The Security Team will work on fixing the vulnerability and perform internal testing before preparing to roll out the fix.
|
||||
7. The Security Team will provide early disclosure of the vulnerability by emailing the [Pinniped Distributors](https://groups.google.com/g/project-pinniped-distributors) mailing list. Distributors can initially plan for the vulnerability patch ahead of the fix, and later can test the fix and provide feedback to the Pinniped team. See the section **Early Disclosure to Pinniped Distributors List** for details about how to join this mailing list.
|
||||
8. A public disclosure date is negotiated by the VMware SecurityTeam, the bug submitter, and the distributors list. We prefer to fully disclose the bug as soon as possible once a user mitigation or patch is available. It is reasonable to delay disclosure when the bug or the fix is not yet fully understood, the solution is not well-tested, or for distributor coordination. The timeframe for disclosure is from immediate (especially if it’s already publicly known) to a few weeks. For a critical vulnerability with a straightforward mitigation, we expect the report date for the public disclosure date to be on the order of 14 business days. The VMware Security Team holds the final say when setting a public disclosure date.
|
||||
9. Once the fix is confirmed, the Security Team will patch the vulnerability in the next patch or minor release, and backport a patch release into all earlier supported releases. Upon release of the patched version of Pinniped, we will follow the **Public Disclosure Process**.
|
||||
|
||||
## Public Disclosure Process
|
||||
|
||||
The Security Team publishes a [public advisory](https://github.com/vmware-tanzu/pinniped/security/advisories) to the Pinniped community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog and other channels will assist in educating Pinniped users and rolling out the patched release to affected users.
|
||||
|
||||
The Security Team will also publish any mitigating steps users can take until the fix can be applied to their Pinniped instances. Pinniped distributors will handle creating and publishing their own security advisories.
|
||||
|
||||
## Mailing lists
|
||||
|
||||
* Use security@vmware.com to report security concerns to the VMware Security Team, who uses the list to privately discuss security issues and fixes prior to disclosure. The use of encrypted email is encouraged. The public PGP key can be found at https://kb.vmware.com/kb/1055.
|
||||
* Join the [Pinniped Distributors](https://groups.google.com/g/project-pinniped-distributors) mailing list for early private information and vulnerability disclosure. Early disclosure may include mitigating steps and additional information on security patch releases. See below for information on how Pinniped distributors or vendors can apply to join this list.
|
||||
|
||||
## Early Disclosure to Pinniped Distributors List
|
||||
|
||||
The private list is intended to be used primarily to provide actionable information to multiple distributor projects at once. This list is not intended to inform individuals about security issues.
|
||||
|
||||
## Membership Criteria
|
||||
|
||||
To be eligible to join the [Pinniped Distributors](https://groups.google.com/g/project-pinniped-distributors) mailing list, you should:
|
||||
|
||||
1. Be an active distributor of Pinniped.
|
||||
2. Have a user base that is not limited to your own organization.
|
||||
3. Have a publicly verifiable track record up to the present day of fixing security issues.
|
||||
4. Not be a downstream or rebuild of another distributor.
|
||||
5. Be a participant and active contributor in the Pinniped community.
|
||||
6. Accept the Embargo Policy that is outlined below.
|
||||
7. Have someone who is already on the list vouch for the person requesting membership on behalf of your distribution.
|
||||
|
||||
**The terms and conditions of the Embargo Policy apply to all members of this mailing list. A request for membership represents your acceptance to the terms and conditions of the Embargo Policy.**
|
||||
|
||||
## Embargo Policy
|
||||
|
||||
The information that members receive on the Pinniped Distributors mailing list must not be made public, shared, or even hinted at anywhere beyond those who need to know within your specific team, unless you receive explicit approval to do so from the VMware Security Team. This remains true until the public disclosure date/time agreed upon by the list. Members of the list and others cannot use the information for any reason other than to get the issue fixed for your respective distribution's users.
|
||||
|
||||
Before you share any information from the list with members of your team who are required to fix the issue, these team members must agree to the same terms, and only be provided with information on a need-to-know basis.
|
||||
|
||||
In the unfortunate event that you share information beyond what is permitted by this policy, you must urgently inform the VMware Security Team (security@vmware.com) of exactly what information was leaked and to whom. If you continue to leak information and break the policy outlined here, you will be permanently removed from the list.
|
||||
|
||||
## Requesting to Join
|
||||
|
||||
Send new membership requests to https://groups.google.com/g/project-pinniped-distributors. In the body of your request please specify how you qualify for membership and fulfill each criterion listed in the Membership Criteria section above.
|
||||
|
||||
## Confidentiality, integrity and availability
|
||||
|
||||
We consider vulnerabilities leading to the compromise of data confidentiality, elevation of privilege, or integrity to be our highest priority concerns. Availability, in particular in areas relating to DoS and resource exhaustion, is also a serious security concern. The VMware Security Team takes all vulnerabilities, potential vulnerabilities, and suspected vulnerabilities seriously and will investigate them in an urgent and expeditious manner.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=authentication.concierge.pinniped.dev
|
||||
|
||||
// Package authentication is the internal version of the Pinniped concierge authentication API.
|
||||
package authentication
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||
return RegisterDefaults(scheme)
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:conversion-gen=go.pinniped.dev/GENERATED_PKG/apis/concierge/authentication
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=authentication.concierge.pinniped.dev
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
localSchemeBuilder.Register(addKnownTypes)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
@@ -32,6 +32,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&WebhookAuthenticator{},
|
||||
&WebhookAuthenticatorList{},
|
||||
&JWTAuthenticator{},
|
||||
&JWTAuthenticatorList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
|
||||
83
apis/concierge/authentication/v1alpha1/types_jwt.go.tmpl
Normal file
83
apis/concierge/authentication/v1alpha1/types_jwt.go.tmpl
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// Status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
}
|
||||
|
||||
// Spec for configuring a JWT authenticator.
|
||||
type JWTAuthenticatorSpec struct {
|
||||
// Issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is
|
||||
// also used to validate the "iss" JWT claim.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^https://`
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// Audience is the required value of the "aud" JWT claim.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Audience string `json:"audience"`
|
||||
|
||||
// Claims allows customization of the claims that will be mapped to user identity
|
||||
// for Kubernetes access.
|
||||
// +optional
|
||||
Claims JWTTokenClaims `json:"claims"`
|
||||
|
||||
// TLS configuration for communicating with the OIDC provider.
|
||||
// +optional
|
||||
TLS *TLSSpec `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// JWTTokenClaims allows customization of the claims that will be mapped to user identity
|
||||
// for Kubernetes access.
|
||||
type JWTTokenClaims struct {
|
||||
// Groups is the name of the claim which should be read to extract the user's
|
||||
// group membership from the JWT token. When not specified, it will default to "groups".
|
||||
// +optional
|
||||
Groups string `json:"groups"`
|
||||
|
||||
// Username is the name of the claim which should be read to extract the
|
||||
// username from the JWT token. When not specified, it will default to "username".
|
||||
// +optional
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// JWTAuthenticator describes the configuration of a JWT authenticator.
|
||||
//
|
||||
// Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid
|
||||
// signature, existence of claims, etc.) and extract the username and groups from the token.
|
||||
//
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster
|
||||
// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer`
|
||||
// +kubebuilder:subresource:status
|
||||
type JWTAuthenticator struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec for configuring the authenticator.
|
||||
Spec JWTAuthenticatorSpec `json:"spec"`
|
||||
|
||||
// Status of the authenticator.
|
||||
Status JWTAuthenticatorStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// List of JWTAuthenticator objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type JWTAuthenticatorList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []JWTAuthenticator `json:"items"`
|
||||
}
|
||||
@@ -29,9 +29,11 @@ type WebhookAuthenticatorSpec struct {
|
||||
|
||||
// WebhookAuthenticator describes the configuration of a webhook authenticator.
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=all;authenticator;authenticators
|
||||
// +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators,scope=Cluster
|
||||
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint`
|
||||
// +kubebuilder:subresource:status
|
||||
type WebhookAuthenticator struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=config.concierge.pinniped.dev
|
||||
|
||||
// Package config is the internal version of the Pinniped concierge configuration API.
|
||||
package config
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package config
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:conversion-gen=go.pinniped.dev/GENERATED_PKG/apis/concierge/config
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=config.concierge.pinniped.dev
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
localSchemeBuilder.Register(addKnownTypes)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
|
||||
@@ -1,27 +1,39 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy
|
||||
type StrategyType string
|
||||
|
||||
// +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy
|
||||
type FrontendType string
|
||||
|
||||
// +kubebuilder:validation:Enum=Success;Error
|
||||
type StrategyStatus string
|
||||
|
||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
||||
// +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey
|
||||
type StrategyReason string
|
||||
|
||||
const (
|
||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||
ImpersonationProxyStrategyType = StrategyType("ImpersonationProxy")
|
||||
|
||||
TokenCredentialRequestAPIFrontendType = FrontendType("TokenCredentialRequestAPI")
|
||||
ImpersonationProxyFrontendType = FrontendType("ImpersonationProxy")
|
||||
|
||||
SuccessStrategyStatus = StrategyStatus("Success")
|
||||
ErrorStrategyStatus = StrategyStatus("Error")
|
||||
|
||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||
ListeningStrategyReason = StrategyReason("Listening")
|
||||
PendingStrategyReason = StrategyReason("Pending")
|
||||
DisabledStrategyReason = StrategyReason("Disabled")
|
||||
ErrorDuringSetupStrategyReason = StrategyReason("ErrorDuringSetup")
|
||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||
CouldNotGetClusterInfoStrategyReason = StrategyReason("CouldNotGetClusterInfo")
|
||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||
)
|
||||
|
||||
// Status of a credential issuer.
|
||||
@@ -30,6 +42,7 @@ type CredentialIssuerStatus struct {
|
||||
Strategies []CredentialIssuerStrategy `json:"strategies"`
|
||||
|
||||
// Information needed to form a valid Pinniped-based kubeconfig using this credential issuer.
|
||||
// This field is deprecated and will be removed in a future version.
|
||||
// +optional
|
||||
KubeConfigInfo *CredentialIssuerKubeConfigInfo `json:"kubeConfigInfo,omitempty"`
|
||||
}
|
||||
@@ -63,23 +76,65 @@ type CredentialIssuerStrategy struct {
|
||||
|
||||
// When the status was last checked.
|
||||
LastUpdateTime metav1.Time `json:"lastUpdateTime"`
|
||||
|
||||
// Frontend describes how clients can connect using this strategy.
|
||||
Frontend *CredentialIssuerFrontend `json:"frontend,omitempty"`
|
||||
}
|
||||
|
||||
type CredentialIssuerFrontend struct {
|
||||
// Type describes which frontend mechanism clients can use with a strategy.
|
||||
Type FrontendType `json:"type"`
|
||||
|
||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||
// This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
TokenCredentialRequestAPIInfo *TokenCredentialRequestAPIInfo `json:"tokenCredentialRequestInfo,omitempty"`
|
||||
|
||||
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||
// This field is only set when Type is "ImpersonationProxy".
|
||||
ImpersonationProxyInfo *ImpersonationProxyInfo `json:"impersonationProxyInfo,omitempty"`
|
||||
}
|
||||
|
||||
// TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge.
|
||||
type TokenCredentialRequestAPIInfo struct {
|
||||
// Server is the Kubernetes API server URL.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||
Server string `json:"server"`
|
||||
|
||||
// CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
||||
// ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge.
|
||||
type ImpersonationProxyInfo struct {
|
||||
// Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^https://`
|
||||
Endpoint string `json:"endpoint"`
|
||||
|
||||
// CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
||||
// Describes the configuration status of a Pinniped credential issuer.
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// +kubebuilder:resource:categories=pinniped,scope=Cluster
|
||||
// +kubebuilder:subresource:status
|
||||
type CredentialIssuer struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Status of the credential issuer.
|
||||
// +optional
|
||||
Status CredentialIssuerStatus `json:"status"`
|
||||
}
|
||||
|
||||
// List of CredentialIssuer objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
type CredentialIssuerList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
8
apis/concierge/identity/doc.go.tmpl
Normal file
8
apis/concierge/identity/doc.go.tmpl
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=identity.concierge.pinniped.dev
|
||||
|
||||
// Package identity is the internal version of the Pinniped identity API.
|
||||
package identity
|
||||
38
apis/concierge/identity/register.go.tmpl
Normal file
38
apis/concierge/identity/register.go.tmpl
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identity
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "identity.concierge.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
|
||||
|
||||
// Kind takes an unqualified kind and returns back a Group qualified GroupKind.
|
||||
func Kind(kind string) schema.GroupKind {
|
||||
return SchemeGroupVersion.WithKind(kind).GroupKind()
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns back a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
|
||||
var (
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&WhoAmIRequest{},
|
||||
&WhoAmIRequestList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
37
apis/concierge/identity/types_userinfo.go.tmpl
Normal file
37
apis/concierge/identity/types_userinfo.go.tmpl
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identity
|
||||
|
||||
import "fmt"
|
||||
|
||||
// KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it.
|
||||
// Copied from the Kubernetes token review API.
|
||||
type KubernetesUserInfo struct {
|
||||
// User is the UserInfo associated with the current user.
|
||||
User UserInfo
|
||||
// Audiences are audience identifiers chosen by the authenticator.
|
||||
Audiences []string
|
||||
}
|
||||
|
||||
// UserInfo holds the information about the user needed to implement the
|
||||
// user.Info interface.
|
||||
type UserInfo struct {
|
||||
// The name that uniquely identifies this user among all active users.
|
||||
Username string
|
||||
// A unique value that identifies this user across time. If this user is
|
||||
// deleted and another user by the same name is added, they will have
|
||||
// different UIDs.
|
||||
UID string
|
||||
// The names of groups this user is a part of.
|
||||
Groups []string
|
||||
// Any additional information provided by the authenticator.
|
||||
Extra map[string]ExtraValue
|
||||
}
|
||||
|
||||
// ExtraValue masks the value so protobuf can generate
|
||||
type ExtraValue []string
|
||||
|
||||
func (t ExtraValue) String() string {
|
||||
return fmt.Sprintf("%v", []string(t))
|
||||
}
|
||||
40
apis/concierge/identity/types_whoami.go.tmpl
Normal file
40
apis/concierge/identity/types_whoami.go.tmpl
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package identity
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// WhoAmIRequest submits a request to echo back the current authenticated user.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequest struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ObjectMeta
|
||||
|
||||
Spec WhoAmIRequestSpec
|
||||
Status WhoAmIRequestStatus
|
||||
}
|
||||
|
||||
type WhoAmIRequestSpec struct {
|
||||
// empty for now but we may add some config here in the future
|
||||
// any such config must be safe in the context of an unauthenticated user
|
||||
}
|
||||
|
||||
type WhoAmIRequestStatus struct {
|
||||
// The current authenticated user, exactly as Kubernetes understands it.
|
||||
KubernetesUserInfo KubernetesUserInfo
|
||||
|
||||
// We may add concierge specific information here in the future.
|
||||
}
|
||||
|
||||
// WhoAmIRequestList is a list of WhoAmIRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequestList struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ListMeta
|
||||
|
||||
// Items is a list of WhoAmIRequest
|
||||
Items []WhoAmIRequest
|
||||
}
|
||||
4
apis/concierge/identity/v1alpha1/conversion.go.tmpl
Normal file
4
apis/concierge/identity/v1alpha1/conversion.go.tmpl
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
11
apis/concierge/identity/v1alpha1/doc.go.tmpl
Normal file
11
apis/concierge/identity/v1alpha1/doc.go.tmpl
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:conversion-gen=go.pinniped.dev/GENERATED_PKG/apis/concierge/identity
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=identity.concierge.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped identity API.
|
||||
package v1alpha1
|
||||
43
apis/concierge/identity/v1alpha1/register.go.tmpl
Normal file
43
apis/concierge/identity/v1alpha1/register.go.tmpl
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "identity.concierge.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&WhoAmIRequest{},
|
||||
&WhoAmIRequestList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
41
apis/concierge/identity/v1alpha1/types_userinfo.go.tmpl
Normal file
41
apis/concierge/identity/v1alpha1/types_userinfo.go.tmpl
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import "fmt"
|
||||
|
||||
// KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it.
|
||||
// Copied from the Kubernetes token review API.
|
||||
type KubernetesUserInfo struct {
|
||||
// User is the UserInfo associated with the current user.
|
||||
User UserInfo `json:"user"`
|
||||
// Audiences are audience identifiers chosen by the authenticator.
|
||||
// +optional
|
||||
Audiences []string `json:"audiences,omitempty"`
|
||||
}
|
||||
|
||||
// UserInfo holds the information about the user needed to implement the
|
||||
// user.Info interface.
|
||||
type UserInfo struct {
|
||||
// The name that uniquely identifies this user among all active users.
|
||||
Username string `json:"username"`
|
||||
// A unique value that identifies this user across time. If this user is
|
||||
// deleted and another user by the same name is added, they will have
|
||||
// different UIDs.
|
||||
// +optional
|
||||
UID string `json:"uid,omitempty"`
|
||||
// The names of groups this user is a part of.
|
||||
// +optional
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
// Any additional information provided by the authenticator.
|
||||
// +optional
|
||||
Extra map[string]ExtraValue `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraValue masks the value so protobuf can generate
|
||||
type ExtraValue []string
|
||||
|
||||
func (t ExtraValue) String() string {
|
||||
return fmt.Sprintf("%v", []string(t))
|
||||
}
|
||||
43
apis/concierge/identity/v1alpha1/types_whoami.go.tmpl
Normal file
43
apis/concierge/identity/v1alpha1/types_whoami.go.tmpl
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// WhoAmIRequest submits a request to echo back the current authenticated user.
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +genclient:onlyVerbs=create
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequest struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec WhoAmIRequestSpec `json:"spec,omitempty"`
|
||||
Status WhoAmIRequestStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type WhoAmIRequestSpec struct {
|
||||
// empty for now but we may add some config here in the future
|
||||
// any such config must be safe in the context of an unauthenticated user
|
||||
}
|
||||
|
||||
type WhoAmIRequestStatus struct {
|
||||
// The current authenticated user, exactly as Kubernetes understands it.
|
||||
KubernetesUserInfo KubernetesUserInfo `json:"kubernetesUserInfo"`
|
||||
|
||||
// We may add concierge specific information here in the future.
|
||||
}
|
||||
|
||||
// WhoAmIRequestList is a list of WhoAmIRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WhoAmIRequestList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Items is a list of WhoAmIRequest
|
||||
Items []WhoAmIRequest `json:"items"`
|
||||
}
|
||||
14
apis/concierge/identity/validation/validation.go.tmpl
Normal file
14
apis/concierge/identity/validation/validation.go.tmpl
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
identityapi "go.pinniped.dev/GENERATED_PKG/apis/concierge/identity"
|
||||
)
|
||||
|
||||
func ValidateWhoAmIRequest(whoAmIRequest *identityapi.WhoAmIRequest) field.ErrorList {
|
||||
return nil // add validation for spec here if we expand it
|
||||
}
|
||||
@@ -27,7 +27,6 @@ type TokenCredentialRequestStatus struct {
|
||||
}
|
||||
|
||||
// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type TokenCredentialRequest struct {
|
||||
metav1.TypeMeta
|
||||
|
||||
@@ -30,6 +30,8 @@ type TokenCredentialRequestStatus struct {
|
||||
|
||||
// TokenCredentialRequest submits an IDP-specific credential to Pinniped in exchange for a cluster-specific credential.
|
||||
// +genclient
|
||||
// +genclient:nonNamespaced
|
||||
// +genclient:onlyVerbs=create
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type TokenCredentialRequest struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=config.supervisor.pinniped.dev
|
||||
|
||||
// Package config is the internal version of the Pinniped configuration API.
|
||||
package config
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package config
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||
return RegisterDefaults(scheme)
|
||||
}
|
||||
@@ -24,14 +24,14 @@ func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
localSchemeBuilder.Register(addKnownTypes)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&OIDCProvider{},
|
||||
&OIDCProviderList{},
|
||||
&FederationDomain{},
|
||||
&FederationDomainList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
|
||||
@@ -8,20 +8,20 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +kubebuilder:validation:Enum=Success;Duplicate;Invalid
|
||||
type OIDCProviderStatusCondition string
|
||||
// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret
|
||||
type FederationDomainStatusCondition string
|
||||
|
||||
const (
|
||||
SuccessOIDCProviderStatusCondition = OIDCProviderStatusCondition("Success")
|
||||
DuplicateOIDCProviderStatusCondition = OIDCProviderStatusCondition("Duplicate")
|
||||
SameIssuerHostMustUseSameSecretOIDCProviderStatusCondition = OIDCProviderStatusCondition("SameIssuerHostMustUseSameSecret")
|
||||
InvalidOIDCProviderStatusCondition = OIDCProviderStatusCondition("Invalid")
|
||||
SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success")
|
||||
DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate")
|
||||
SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret")
|
||||
InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid")
|
||||
)
|
||||
|
||||
// OIDCProviderTLSSpec is a struct that describes the TLS configuration for an OIDC Provider.
|
||||
type OIDCProviderTLSSpec struct {
|
||||
// FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider.
|
||||
type FederationDomainTLSSpec struct {
|
||||
// SecretName is an optional name of a Secret in the same namespace, of type `kubernetes.io/tls`, which contains
|
||||
// the TLS serving certificate for the HTTPS endpoints served by this OIDCProvider. When provided, the TLS Secret
|
||||
// the TLS serving certificate for the HTTPS endpoints served by this FederationDomain. When provided, the TLS Secret
|
||||
// named here must contain keys named `tls.crt` and `tls.key` that contain the certificate and private key to use
|
||||
// for TLS.
|
||||
//
|
||||
@@ -41,8 +41,8 @@ type OIDCProviderTLSSpec struct {
|
||||
SecretName string `json:"secretName,omitempty"`
|
||||
}
|
||||
|
||||
// OIDCProviderSpec is a struct that describes an OIDC Provider.
|
||||
type OIDCProviderSpec struct {
|
||||
// FederationDomainSpec is a struct that describes an OIDC Provider.
|
||||
type FederationDomainSpec struct {
|
||||
// Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the
|
||||
// identifier that it will use for the iss claim in issued JWTs. This field will also be used as
|
||||
// the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is
|
||||
@@ -54,17 +54,41 @@ type OIDCProviderSpec struct {
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// TLS configures how this OIDCProvider is served over Transport Layer Security (TLS).
|
||||
// TLS configures how this FederationDomain is served over Transport Layer Security (TLS).
|
||||
// +optional
|
||||
TLS *OIDCProviderTLSSpec `json:"tls,omitempty"`
|
||||
TLS *FederationDomainTLSSpec `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// OIDCProviderStatus is a struct that describes the actual state of an OIDC Provider.
|
||||
type OIDCProviderStatus struct {
|
||||
// FederationDomainSecrets holds information about this OIDC Provider's secrets.
|
||||
type FederationDomainSecrets struct {
|
||||
// JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are
|
||||
// stored. If it is empty, then the signing/verification keys are either unknown or they don't
|
||||
// exist.
|
||||
// +optional
|
||||
JWKS corev1.LocalObjectReference `json:"jwks,omitempty"`
|
||||
|
||||
// TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for
|
||||
// signing tokens is stored.
|
||||
// +optional
|
||||
TokenSigningKey corev1.LocalObjectReference `json:"tokenSigningKey,omitempty"`
|
||||
|
||||
// StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for
|
||||
// signing state parameters is stored.
|
||||
// +optional
|
||||
StateSigningKey corev1.LocalObjectReference `json:"stateSigningKey,omitempty"`
|
||||
|
||||
// StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for
|
||||
// encrypting state parameters is stored.
|
||||
// +optional
|
||||
StateEncryptionKey corev1.LocalObjectReference `json:"stateEncryptionKey,omitempty"`
|
||||
}
|
||||
|
||||
// FederationDomainStatus is a struct that describes the actual state of an OIDC Provider.
|
||||
type FederationDomainStatus struct {
|
||||
// Status holds an enum that describes the state of this OIDC Provider. Note that this Status can
|
||||
// represent success or failure.
|
||||
// +optional
|
||||
Status OIDCProviderStatusCondition `json:"status,omitempty"`
|
||||
Status FederationDomainStatusCondition `json:"status,omitempty"`
|
||||
|
||||
// Message provides human-readable details about the Status.
|
||||
// +optional
|
||||
@@ -76,32 +100,32 @@ type OIDCProviderStatus struct {
|
||||
// +optional
|
||||
LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
|
||||
|
||||
// JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys
|
||||
// are stored. If it is empty, then the signing/verification keys are either unknown or they don't
|
||||
// exist.
|
||||
// Secrets contains information about this OIDC Provider's secrets.
|
||||
// +optional
|
||||
JWKSSecret corev1.LocalObjectReference `json:"jwksSecret,omitempty"`
|
||||
Secrets FederationDomainSecrets `json:"secrets,omitempty"`
|
||||
}
|
||||
|
||||
// OIDCProvider describes the configuration of an OIDC provider.
|
||||
// FederationDomain describes the configuration of an OIDC provider.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type OIDCProvider struct {
|
||||
// +kubebuilder:resource:categories=pinniped
|
||||
// +kubebuilder:subresource:status
|
||||
type FederationDomain struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec of the OIDC provider.
|
||||
Spec OIDCProviderSpec `json:"spec"`
|
||||
Spec FederationDomainSpec `json:"spec"`
|
||||
|
||||
// Status of the OIDC provider.
|
||||
Status OIDCProviderStatus `json:"status,omitempty"`
|
||||
Status FederationDomainStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// List of OIDCProvider objects.
|
||||
// List of FederationDomain objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type OIDCProviderList struct {
|
||||
type FederationDomainList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []OIDCProvider `json:"items"`
|
||||
Items []FederationDomain `json:"items"`
|
||||
}
|
||||
11
apis/supervisor/idp/v1alpha1/doc.go.tmpl
Normal file
11
apis/supervisor/idp/v1alpha1/doc.go.tmpl
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=idp.supervisor.pinniped.dev
|
||||
// +groupGoName=IDP
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity provider (IDP) API.
|
||||
package v1alpha1
|
||||
43
apis/supervisor/idp/v1alpha1/register.go.tmpl
Normal file
43
apis/supervisor/idp/v1alpha1/register.go.tmpl
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "idp.supervisor.pinniped.dev"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&OIDCIdentityProvider{},
|
||||
&OIDCIdentityProviderList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource.
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
75
apis/supervisor/idp/v1alpha1/types_meta.go.tmpl
Normal file
75
apis/supervisor/idp/v1alpha1/types_meta.go.tmpl
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// ConditionStatus is effectively an enum type for Condition.Status.
|
||||
type ConditionStatus string
|
||||
|
||||
// These are valid condition statuses. "ConditionTrue" means a resource is in the condition.
|
||||
// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes
|
||||
// can't decide if a resource is in the condition or not. In the future, we could add other
|
||||
// intermediate conditions, e.g. ConditionDegraded.
|
||||
const (
|
||||
ConditionTrue ConditionStatus = "True"
|
||||
ConditionFalse ConditionStatus = "False"
|
||||
ConditionUnknown ConditionStatus = "Unknown"
|
||||
)
|
||||
|
||||
// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API
|
||||
// version we can switch to using the upstream type.
|
||||
// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413.
|
||||
type Condition struct {
|
||||
// type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
// ---
|
||||
// Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
|
||||
// useful (see .node.status.conditions), the ability to deconflict is important.
|
||||
// The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
// +required
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$`
|
||||
// +kubebuilder:validation:MaxLength=316
|
||||
Type string `json:"type"`
|
||||
|
||||
// status of the condition, one of True, False, Unknown.
|
||||
// +required
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:Enum=True;False;Unknown
|
||||
Status ConditionStatus `json:"status"`
|
||||
|
||||
// observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
// For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
// with respect to the current state of the instance.
|
||||
// +optional
|
||||
// +kubebuilder:validation:Minimum=0
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
|
||||
// lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
// This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
// +required
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:Type=string
|
||||
// +kubebuilder:validation:Format=date-time
|
||||
LastTransitionTime metav1.Time `json:"lastTransitionTime"`
|
||||
|
||||
// reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
// Producers of specific condition types may define expected values and meanings for this field,
|
||||
// and whether the values are considered a guaranteed API.
|
||||
// The value should be a CamelCase string.
|
||||
// This field may not be empty.
|
||||
// +required
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:MaxLength=1024
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$`
|
||||
Reason string `json:"reason"`
|
||||
|
||||
// message is a human readable message indicating details about the transition.
|
||||
// This may be an empty string.
|
||||
// +required
|
||||
// +kubebuilder:validation:Required
|
||||
// +kubebuilder:validation:MaxLength=32768
|
||||
Message string `json:"message"`
|
||||
}
|
||||
123
apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl
Normal file
123
apis/supervisor/idp/v1alpha1/types_oidcidentityprovider.go.tmpl
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type OIDCIdentityProviderPhase string
|
||||
|
||||
const (
|
||||
// PhasePending is the default phase for newly-created OIDCIdentityProvider resources.
|
||||
PhasePending OIDCIdentityProviderPhase = "Pending"
|
||||
|
||||
// PhaseReady is the phase for an OIDCIdentityProvider resource in a healthy state.
|
||||
PhaseReady OIDCIdentityProviderPhase = "Ready"
|
||||
|
||||
// PhaseError is the phase for an OIDCIdentityProvider in an unhealthy state.
|
||||
PhaseError OIDCIdentityProviderPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of an OIDC identity provider.
|
||||
type OIDCIdentityProviderStatus struct {
|
||||
// Phase summarizes the overall status of the OIDCIdentityProvider.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase OIDCIdentityProviderPhase `json:"phase,omitempty"`
|
||||
|
||||
// Represents the observations of an identity provider's current state.
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
}
|
||||
|
||||
// OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization
|
||||
// request parameters.
|
||||
type OIDCAuthorizationConfig struct {
|
||||
// AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization
|
||||
// request flow with an OIDC identity provider. By default only the "openid" scope will be requested.
|
||||
// +optional
|
||||
AdditionalScopes []string `json:"additionalScopes,omitempty"`
|
||||
}
|
||||
|
||||
// OIDCClaims provides a mapping from upstream claims into identities.
|
||||
type OIDCClaims struct {
|
||||
// Groups provides the name of the token claim that will be used to ascertain the groups to which
|
||||
// an identity belongs.
|
||||
// +optional
|
||||
Groups string `json:"groups"`
|
||||
|
||||
// Username provides the name of the token claim that will be used to ascertain an identity's
|
||||
// username.
|
||||
// +optional
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// OIDCClient contains information about an OIDC client (e.g., client ID and client
|
||||
// secret).
|
||||
type OIDCClient struct {
|
||||
// SecretName contains the name of a namespace-local Secret object that provides the clientID and
|
||||
// clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient
|
||||
// struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys
|
||||
// "clientID" and "clientSecret".
|
||||
SecretName string `json:"secretName"`
|
||||
}
|
||||
|
||||
// Spec for configuring an OIDC identity provider.
|
||||
type OIDCIdentityProviderSpec struct {
|
||||
// Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch
|
||||
// /.well-known/openid-configuration.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^https://`
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// TLS configuration for discovery/JWKS requests to the issuer.
|
||||
// +optional
|
||||
TLS *TLSSpec `json:"tls,omitempty"`
|
||||
|
||||
// AuthorizationConfig holds information about how to form the OAuth2 authorization request
|
||||
// parameters to be used with this OIDC identity provider.
|
||||
// +optional
|
||||
AuthorizationConfig OIDCAuthorizationConfig `json:"authorizationConfig,omitempty"`
|
||||
|
||||
// Claims provides the names of token claims that will be used when inspecting an identity from
|
||||
// this OIDC identity provider.
|
||||
// +optional
|
||||
Claims OIDCClaims `json:"claims"`
|
||||
|
||||
// OIDCClient contains OIDC client information to be used used with this OIDC identity
|
||||
// provider.
|
||||
Client OIDCClient `json:"client"`
|
||||
}
|
||||
|
||||
// OIDCIdentityProvider describes the configuration of an upstream OpenID Connect identity provider.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps
|
||||
// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer`
|
||||
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase`
|
||||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
|
||||
// +kubebuilder:subresource:status
|
||||
type OIDCIdentityProvider struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec for configuring the identity provider.
|
||||
Spec OIDCIdentityProviderSpec `json:"spec"`
|
||||
|
||||
// Status of the identity provider.
|
||||
Status OIDCIdentityProviderStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// List of OIDCIdentityProvider objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type OIDCIdentityProviderList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []OIDCIdentityProvider `json:"items"`
|
||||
}
|
||||
11
apis/supervisor/idp/v1alpha1/types_tls.go.tmpl
Normal file
11
apis/supervisor/idp/v1alpha1/types_tls.go.tmpl
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
// Configuration for TLS parameters related to identity provider integration.
|
||||
type TLSSpec struct {
|
||||
// X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
|
||||
// +optional
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"`
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package main provides a authentication webhook program.
|
||||
@@ -31,13 +31,14 @@ import (
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/controller/apicerts"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/dynamiccert"
|
||||
"go.pinniped.dev/internal/kubeclient"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -53,12 +54,12 @@ const (
|
||||
)
|
||||
|
||||
type webhook struct {
|
||||
certProvider dynamiccert.Provider
|
||||
certProvider dynamiccert.Private
|
||||
secretInformer corev1informers.SecretInformer
|
||||
}
|
||||
|
||||
func newWebhook(
|
||||
certProvider dynamiccert.Provider,
|
||||
certProvider dynamiccert.Private,
|
||||
secretInformer corev1informers.SecretInformer,
|
||||
) *webhook {
|
||||
return &webhook{
|
||||
@@ -92,11 +93,11 @@ func (w *webhook) start(ctx context.Context, l net.Listener) error {
|
||||
go func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
klog.InfoS("server exited", "err", err)
|
||||
plog.Debug("server exited", "err", err)
|
||||
case <-ctx.Done():
|
||||
klog.InfoS("server context cancelled", "err", ctx.Err())
|
||||
plog.Debug("server context cancelled", "err", ctx.Err())
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
klog.InfoS("server shutdown failed", "err", err)
|
||||
plog.Debug("server shutdown failed", "err", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -114,13 +115,13 @@ func (w *webhook) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
|
||||
secret, err := w.secretInformer.Lister().Secrets(namespace).Get(username)
|
||||
notFound := k8serrors.IsNotFound(err)
|
||||
if err != nil && !notFound {
|
||||
klog.InfoS("could not get secret", "err", err)
|
||||
plog.Debug("could not get secret", "err", err)
|
||||
rsp.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if notFound {
|
||||
klog.InfoS("user not found")
|
||||
plog.Debug("user not found")
|
||||
respondWithUnauthenticated(rsp)
|
||||
return
|
||||
}
|
||||
@@ -130,7 +131,7 @@ func (w *webhook) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
|
||||
[]byte(password),
|
||||
) == nil
|
||||
if !passwordMatches {
|
||||
klog.InfoS("authentication failed: wrong password")
|
||||
plog.Debug("authentication failed: wrong password")
|
||||
respondWithUnauthenticated(rsp)
|
||||
return
|
||||
}
|
||||
@@ -141,32 +142,32 @@ func (w *webhook) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
|
||||
groupsCSVReader := csv.NewReader(groupsBuf)
|
||||
groups, err = groupsCSVReader.Read()
|
||||
if err != nil {
|
||||
klog.InfoS("could not read groups", "err", err)
|
||||
plog.Debug("could not read groups", "err", err)
|
||||
rsp.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
trimLeadingAndTrailingWhitespace(groups)
|
||||
}
|
||||
|
||||
klog.InfoS("successful authentication")
|
||||
plog.Debug("successful authentication")
|
||||
respondWithAuthenticated(rsp, secret.ObjectMeta.Name, string(secret.UID), groups)
|
||||
}
|
||||
|
||||
func getUsernameAndPasswordFromRequest(rsp http.ResponseWriter, req *http.Request) (string, string, error) {
|
||||
if req.URL.Path != "/authenticate" {
|
||||
klog.InfoS("received request path other than /authenticate", "path", req.URL.Path)
|
||||
plog.Debug("received request path other than /authenticate", "path", req.URL.Path)
|
||||
rsp.WriteHeader(http.StatusNotFound)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
if req.Method != http.MethodPost {
|
||||
klog.InfoS("received request method other than post", "method", req.Method)
|
||||
plog.Debug("received request method other than post", "method", req.Method)
|
||||
rsp.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
if !headerContains(req, "Content-Type", "application/json") {
|
||||
klog.InfoS("content type is not application/json", "Content-Type", req.Header.Values("Content-Type"))
|
||||
plog.Debug("content type is not application/json", "Content-Type", req.Header.Values("Content-Type"))
|
||||
rsp.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
@@ -174,39 +175,39 @@ func getUsernameAndPasswordFromRequest(rsp http.ResponseWriter, req *http.Reques
|
||||
if !headerContains(req, "Accept", "application/json") &&
|
||||
!headerContains(req, "Accept", "application/*") &&
|
||||
!headerContains(req, "Accept", "*/*") {
|
||||
klog.InfoS("client does not accept application/json", "Accept", req.Header.Values("Accept"))
|
||||
plog.Debug("client does not accept application/json", "Accept", req.Header.Values("Accept"))
|
||||
rsp.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
if req.Body == nil {
|
||||
klog.InfoS("invalid nil body")
|
||||
plog.Debug("invalid nil body")
|
||||
rsp.WriteHeader(http.StatusBadRequest)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
var body authenticationv1beta1.TokenReview
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
klog.InfoS("failed to decode body", "err", err)
|
||||
plog.Debug("failed to decode body", "err", err)
|
||||
rsp.WriteHeader(http.StatusBadRequest)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
if body.APIVersion != authenticationv1beta1.SchemeGroupVersion.String() {
|
||||
klog.InfoS("invalid TokenReview apiVersion", "apiVersion", body.APIVersion)
|
||||
plog.Debug("invalid TokenReview apiVersion", "apiVersion", body.APIVersion)
|
||||
rsp.WriteHeader(http.StatusBadRequest)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
if body.Kind != "TokenReview" {
|
||||
klog.InfoS("invalid TokenReview kind", "kind", body.Kind)
|
||||
plog.Debug("invalid TokenReview kind", "kind", body.Kind)
|
||||
rsp.WriteHeader(http.StatusBadRequest)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
tokenSegments := strings.SplitN(body.Spec.Token, ":", 2)
|
||||
if len(tokenSegments) != 2 {
|
||||
klog.InfoS("bad token format in request")
|
||||
plog.Debug("bad token format in request")
|
||||
rsp.WriteHeader(http.StatusBadRequest)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
@@ -247,7 +248,7 @@ func respondWithUnauthenticated(rsp http.ResponseWriter) {
|
||||
},
|
||||
}
|
||||
if err := json.NewEncoder(rsp).Encode(body); err != nil {
|
||||
klog.InfoS("could not encode response", "err", err)
|
||||
plog.Debug("could not encode response", "err", err)
|
||||
rsp.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
@@ -273,29 +274,14 @@ func respondWithAuthenticated(
|
||||
},
|
||||
}
|
||||
if err := json.NewEncoder(rsp).Encode(body); err != nil {
|
||||
klog.InfoS("could not encode response", "err", err)
|
||||
plog.Debug("could not encode response", "err", err)
|
||||
rsp.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func newK8sClient() (kubernetes.Interface, error) {
|
||||
kubeConfig, err := restclient.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load in-cluster configuration: %w", err)
|
||||
}
|
||||
|
||||
// Connect to the core Kubernetes API.
|
||||
kubeClient, err := kubernetes.NewForConfig(kubeConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load in-cluster configuration: %w", err)
|
||||
}
|
||||
|
||||
return kubeClient, nil
|
||||
}
|
||||
|
||||
func startControllers(
|
||||
ctx context.Context,
|
||||
dynamicCertProvider dynamiccert.Provider,
|
||||
dynamicCertProvider dynamiccert.Private,
|
||||
kubeClient kubernetes.Interface,
|
||||
kubeInformers kubeinformers.SharedInformerFactory,
|
||||
) {
|
||||
@@ -342,7 +328,7 @@ func startControllers(
|
||||
func startWebhook(
|
||||
ctx context.Context,
|
||||
l net.Listener,
|
||||
dynamicCertProvider dynamiccert.Provider,
|
||||
dynamicCertProvider dynamiccert.Private,
|
||||
secretInformer corev1informers.SecretInformer,
|
||||
) error {
|
||||
return newWebhook(dynamicCertProvider, secretInformer).start(ctx, l)
|
||||
@@ -358,21 +344,21 @@ func run() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
kubeClient, err := newK8sClient()
|
||||
client, err := kubeclient.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create k8s client: %w", err)
|
||||
}
|
||||
|
||||
kubeInformers := kubeinformers.NewSharedInformerFactoryWithOptions(
|
||||
kubeClient,
|
||||
client.Kubernetes,
|
||||
defaultResyncInterval,
|
||||
kubeinformers.WithNamespace(namespace),
|
||||
)
|
||||
|
||||
dynamicCertProvider := dynamiccert.New()
|
||||
dynamicCertProvider := dynamiccert.NewServingCert("local-user-authenticator-tls-serving-certificate")
|
||||
|
||||
startControllers(ctx, dynamicCertProvider, kubeClient, kubeInformers)
|
||||
klog.InfoS("controllers are ready")
|
||||
startControllers(ctx, dynamicCertProvider, client.Kubernetes, kubeInformers)
|
||||
plog.Debug("controllers are ready")
|
||||
|
||||
//nolint: gosec // Intentionally binding to all network interfaces.
|
||||
l, err := net.Listen("tcp", ":8443")
|
||||
@@ -385,15 +371,20 @@ func run() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start webhook: %w", err)
|
||||
}
|
||||
klog.InfoS("webhook is ready", "address", l.Addr().String())
|
||||
plog.Debug("webhook is ready", "address", l.Addr().String())
|
||||
|
||||
gotSignal := waitForSignal()
|
||||
klog.InfoS("webhook exiting", "signal", gotSignal)
|
||||
plog.Debug("webhook exiting", "signal", gotSignal)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Hardcode the logging level to debug, since this is a test app and it is very helpful to have
|
||||
// verbose logs to debug test failures.
|
||||
if err := plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug); err != nil {
|
||||
klog.Fatal(err)
|
||||
}
|
||||
if err := run(); err != nil {
|
||||
klog.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -99,7 +98,7 @@ func TestWebhook(t *testing.T) {
|
||||
},
|
||||
}))
|
||||
|
||||
secretInformer := createSecretInformer(t, kubeClient)
|
||||
secretInformer := createSecretInformer(ctx, t, kubeClient)
|
||||
|
||||
certProvider, caBundle, serverName := newCertProvider(t)
|
||||
w := newWebhook(certProvider, secretInformer)
|
||||
@@ -437,7 +436,7 @@ func TestWebhook(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func createSecretInformer(t *testing.T, kubeClient kubernetes.Interface) corev1informers.SecretInformer {
|
||||
func createSecretInformer(ctx context.Context, t *testing.T, kubeClient kubernetes.Interface) corev1informers.SecretInformer {
|
||||
t.Helper()
|
||||
|
||||
kubeInformers := kubeinformers.NewSharedInformerFactory(kubeClient, 0)
|
||||
@@ -448,9 +447,6 @@ func createSecretInformer(t *testing.T, kubeClient kubernetes.Interface) corev1i
|
||||
// informer factory before syncing it.
|
||||
secretInformer.Informer()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
defer cancel()
|
||||
|
||||
kubeInformers.Start(ctx.Done())
|
||||
|
||||
informerTypesSynced := kubeInformers.WaitForCacheSync(ctx.Done())
|
||||
@@ -462,22 +458,23 @@ func createSecretInformer(t *testing.T, kubeClient kubernetes.Interface) corev1i
|
||||
// newClientProvider returns a dynamiccert.Provider configured
|
||||
// with valid serving cert, the CA bundle that can be used to verify the serving
|
||||
// cert, and the server name that can be used to verify the TLS peer.
|
||||
func newCertProvider(t *testing.T) (dynamiccert.Provider, []byte, string) {
|
||||
func newCertProvider(t *testing.T) (dynamiccert.Private, []byte, string) {
|
||||
t.Helper()
|
||||
|
||||
serverName := "local-user-authenticator"
|
||||
|
||||
ca, err := certauthority.New(pkix.Name{CommonName: serverName + " CA"}, time.Hour*24)
|
||||
ca, err := certauthority.New(serverName+" CA", time.Hour*24)
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := ca.Issue(pkix.Name{CommonName: serverName}, []string{serverName}, nil, time.Hour*24)
|
||||
cert, err := ca.IssueServerCert([]string{serverName}, nil, time.Hour*24)
|
||||
require.NoError(t, err)
|
||||
|
||||
certPEM, keyPEM, err := certauthority.ToPEM(cert)
|
||||
require.NoError(t, err)
|
||||
|
||||
certProvider := dynamiccert.New()
|
||||
certProvider.Set(certPEM, keyPEM)
|
||||
certProvider := dynamiccert.NewServingCert(t.Name())
|
||||
err = certProvider.SetCertKeyContent(certPEM, keyPEM)
|
||||
require.NoError(t, err)
|
||||
|
||||
return certProvider, ca.Bundle(), serverName
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -14,24 +15,35 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/pkg/version"
|
||||
"k8s.io/client-go/rest"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/component-base/logs"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/klog/v2/klogr"
|
||||
|
||||
pinnipedclientset "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned"
|
||||
pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions"
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
||||
pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions"
|
||||
"go.pinniped.dev/internal/config/supervisor"
|
||||
"go.pinniped.dev/internal/controller/supervisorconfig"
|
||||
"go.pinniped.dev/internal/controller/supervisorconfig/generator"
|
||||
"go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatcher"
|
||||
"go.pinniped.dev/internal/controller/supervisorstorage"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/deploymentref"
|
||||
"go.pinniped.dev/internal/downward"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/kubeclient"
|
||||
"go.pinniped.dev/internal/oidc/jwks"
|
||||
"go.pinniped.dev/internal/oidc/provider"
|
||||
"go.pinniped.dev/internal/oidc/provider/manager"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/internal/secret"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -50,11 +62,11 @@ func start(ctx context.Context, l net.Listener, handler http.Handler) {
|
||||
go func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
klog.InfoS("server exited", "err", err)
|
||||
plog.Debug("server exited", "err", err)
|
||||
case <-ctx.Done():
|
||||
klog.InfoS("server context cancelled", "err", ctx.Err())
|
||||
plog.Debug("server context cancelled", "err", ctx.Err())
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
klog.InfoS("server shutdown failed", "err", err)
|
||||
plog.Debug("server shutdown failed", "err", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -66,26 +78,42 @@ func waitForSignal() os.Signal {
|
||||
return <-signalCh
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func startControllers(
|
||||
ctx context.Context,
|
||||
cfg *supervisor.Config,
|
||||
issuerManager *manager.Manager,
|
||||
dynamicJWKSProvider jwks.DynamicJWKSProvider,
|
||||
dynamicTLSCertProvider provider.DynamicTLSCertProvider,
|
||||
dynamicUpstreamIDPProvider provider.DynamicUpstreamIDPProvider,
|
||||
secretCache *secret.Cache,
|
||||
supervisorDeployment *appsv1.Deployment,
|
||||
kubeClient kubernetes.Interface,
|
||||
pinnipedClient pinnipedclientset.Interface,
|
||||
kubeInformers kubeinformers.SharedInformerFactory,
|
||||
pinnipedInformers pinnipedinformers.SharedInformerFactory,
|
||||
) {
|
||||
federationDomainInformer := pinnipedInformers.Config().V1alpha1().FederationDomains()
|
||||
secretInformer := kubeInformers.Core().V1().Secrets()
|
||||
|
||||
// Create controller manager.
|
||||
controllerManager := controllerlib.
|
||||
NewManager().
|
||||
WithController(
|
||||
supervisorconfig.NewOIDCProviderWatcherController(
|
||||
supervisorstorage.GarbageCollectorController(
|
||||
clock.RealClock{},
|
||||
kubeClient,
|
||||
secretInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
supervisorconfig.NewFederationDomainWatcherController(
|
||||
issuerManager,
|
||||
clock.RealClock{},
|
||||
pinnipedClient,
|
||||
pinnipedInformers.Config().V1alpha1().OIDCProviders(),
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
@@ -95,8 +123,8 @@ func startControllers(
|
||||
cfg.Labels,
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
kubeInformers.Core().V1().Secrets(),
|
||||
pinnipedInformers.Config().V1alpha1().OIDCProviders(),
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
@@ -104,8 +132,8 @@ func startControllers(
|
||||
WithController(
|
||||
supervisorconfig.NewJWKSObserverController(
|
||||
dynamicJWKSProvider,
|
||||
kubeInformers.Core().V1().Secrets(),
|
||||
pinnipedInformers.Config().V1alpha1().OIDCProviders(),
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
@@ -114,12 +142,106 @@ func startControllers(
|
||||
supervisorconfig.NewTLSCertObserverController(
|
||||
dynamicTLSCertProvider,
|
||||
cfg.NamesConfig.DefaultTLSCertificateSecret,
|
||||
kubeInformers.Core().V1().Secrets(),
|
||||
pinnipedInformers.Config().V1alpha1().OIDCProviders(),
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
)
|
||||
).
|
||||
WithController(
|
||||
generator.NewSupervisorSecretsController(
|
||||
supervisorDeployment,
|
||||
cfg.Labels,
|
||||
kubeClient,
|
||||
secretInformer,
|
||||
func(secret []byte) {
|
||||
plog.Debug("setting csrf cookie secret")
|
||||
secretCache.SetCSRFCookieEncoderHashKey(secret)
|
||||
},
|
||||
controllerlib.WithInformer,
|
||||
controllerlib.WithInitialEvent,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
generator.NewFederationDomainSecretsController(
|
||||
generator.NewSymmetricSecretHelper(
|
||||
"pinniped-oidc-provider-hmac-key-",
|
||||
cfg.Labels,
|
||||
rand.Reader,
|
||||
generator.SecretUsageTokenSigningKey,
|
||||
func(federationDomainIssuer string, symmetricKey []byte) {
|
||||
plog.Debug("setting hmac secret", "issuer", federationDomainIssuer)
|
||||
secretCache.SetTokenHMACKey(federationDomainIssuer, symmetricKey)
|
||||
},
|
||||
),
|
||||
func(fd *configv1alpha1.FederationDomainStatus) *corev1.LocalObjectReference {
|
||||
return &fd.Secrets.TokenSigningKey
|
||||
},
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
generator.NewFederationDomainSecretsController(
|
||||
generator.NewSymmetricSecretHelper(
|
||||
"pinniped-oidc-provider-upstream-state-signature-key-",
|
||||
cfg.Labels,
|
||||
rand.Reader,
|
||||
generator.SecretUsageStateSigningKey,
|
||||
func(federationDomainIssuer string, symmetricKey []byte) {
|
||||
plog.Debug("setting state signature key", "issuer", federationDomainIssuer)
|
||||
secretCache.SetStateEncoderHashKey(federationDomainIssuer, symmetricKey)
|
||||
},
|
||||
),
|
||||
func(fd *configv1alpha1.FederationDomainStatus) *corev1.LocalObjectReference {
|
||||
return &fd.Secrets.StateSigningKey
|
||||
},
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
generator.NewFederationDomainSecretsController(
|
||||
generator.NewSymmetricSecretHelper(
|
||||
"pinniped-oidc-provider-upstream-state-encryption-key-",
|
||||
cfg.Labels,
|
||||
rand.Reader,
|
||||
generator.SecretUsageStateEncryptionKey,
|
||||
func(federationDomainIssuer string, symmetricKey []byte) {
|
||||
plog.Debug("setting state encryption key", "issuer", federationDomainIssuer)
|
||||
secretCache.SetStateEncoderBlockKey(federationDomainIssuer, symmetricKey)
|
||||
},
|
||||
),
|
||||
func(fd *configv1alpha1.FederationDomainStatus) *corev1.LocalObjectReference {
|
||||
return &fd.Secrets.StateEncryptionKey
|
||||
},
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
upstreamwatcher.New(
|
||||
dynamicUpstreamIDPProvider,
|
||||
pinnipedClient,
|
||||
pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
|
||||
secretInformer,
|
||||
klogr.New(),
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker)
|
||||
|
||||
kubeInformers.Start(ctx.Done())
|
||||
pinnipedInformers.Start(ctx.Done())
|
||||
@@ -131,44 +253,33 @@ func startControllers(
|
||||
go controllerManager.Start(ctx)
|
||||
}
|
||||
|
||||
func newClients() (kubernetes.Interface, pinnipedclientset.Interface, error) {
|
||||
kubeConfig, err := restclient.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not load in-cluster configuration: %w", err)
|
||||
}
|
||||
func run(podInfo *downward.PodInfo, cfg *supervisor.Config) error {
|
||||
serverInstallationNamespace := podInfo.Namespace
|
||||
|
||||
// Connect to the core Kubernetes API.
|
||||
kubeClient, err := kubernetes.NewForConfig(kubeConfig)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not create kube client: %w", err)
|
||||
}
|
||||
|
||||
// Connect to the Pinniped API.
|
||||
pinnipedClient, err := pinnipedclientset.NewForConfig(kubeConfig)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not create pinniped client: %w", err)
|
||||
}
|
||||
|
||||
return kubeClient, pinnipedClient, nil
|
||||
}
|
||||
|
||||
func run(serverInstallationNamespace string, cfg *supervisor.Config) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
kubeClient, pinnipedClient, err := newClients()
|
||||
dref, supervisorDeployment, err := deploymentref.New(podInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create deployment ref: %w", err)
|
||||
}
|
||||
|
||||
client, err := kubeclient.New(
|
||||
dref,
|
||||
kubeclient.WithMiddleware(groupsuffix.New(*cfg.APIGroupSuffix)),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create k8s client: %w", err)
|
||||
}
|
||||
|
||||
kubeInformers := kubeinformers.NewSharedInformerFactoryWithOptions(
|
||||
kubeClient,
|
||||
client.Kubernetes,
|
||||
defaultResyncInterval,
|
||||
kubeinformers.WithNamespace(serverInstallationNamespace),
|
||||
)
|
||||
|
||||
pinnipedInformers := pinnipedinformers.NewSharedInformerFactoryWithOptions(
|
||||
pinnipedClient,
|
||||
client.PinnipedSupervisor,
|
||||
defaultResyncInterval,
|
||||
pinnipedinformers.WithNamespace(serverInstallationNamespace),
|
||||
)
|
||||
@@ -181,9 +292,17 @@ func run(serverInstallationNamespace string, cfg *supervisor.Config) error {
|
||||
|
||||
dynamicJWKSProvider := jwks.NewDynamicJWKSProvider()
|
||||
dynamicTLSCertProvider := provider.NewDynamicTLSCertProvider()
|
||||
dynamicUpstreamIDPProvider := provider.NewDynamicUpstreamIDPProvider()
|
||||
secretCache := secret.Cache{}
|
||||
|
||||
// OIDC endpoints will be served by the oidProvidersManager, and any non-OIDC paths will fallback to the healthMux.
|
||||
oidProvidersManager := manager.NewManager(healthMux, dynamicJWKSProvider)
|
||||
oidProvidersManager := manager.NewManager(
|
||||
healthMux,
|
||||
dynamicJWKSProvider,
|
||||
dynamicUpstreamIDPProvider,
|
||||
&secretCache,
|
||||
client.Kubernetes.CoreV1().Secrets(serverInstallationNamespace),
|
||||
)
|
||||
|
||||
startControllers(
|
||||
ctx,
|
||||
@@ -191,8 +310,11 @@ func run(serverInstallationNamespace string, cfg *supervisor.Config) error {
|
||||
oidProvidersManager,
|
||||
dynamicJWKSProvider,
|
||||
dynamicTLSCertProvider,
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
dynamicUpstreamIDPProvider,
|
||||
&secretCache,
|
||||
supervisorDeployment,
|
||||
client.Kubernetes,
|
||||
client.PinnipedSupervisor,
|
||||
kubeInformers,
|
||||
pinnipedInformers,
|
||||
)
|
||||
@@ -211,7 +333,7 @@ func run(serverInstallationNamespace string, cfg *supervisor.Config) error {
|
||||
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert := dynamicTLSCertProvider.GetTLSCert(strings.ToLower(info.ServerName))
|
||||
defaultCert := dynamicTLSCertProvider.GetDefaultTLSCert()
|
||||
klog.InfoS("GetCertificate called for port 8443",
|
||||
plog.Debug("GetCertificate called for port 8443",
|
||||
"info.ServerName", info.ServerName,
|
||||
"foundSNICert", cert != nil,
|
||||
"foundDefaultCert", defaultCert != nil,
|
||||
@@ -228,13 +350,13 @@ func run(serverInstallationNamespace string, cfg *supervisor.Config) error {
|
||||
defer func() { _ = httpsListener.Close() }()
|
||||
start(ctx, httpsListener, oidProvidersManager)
|
||||
|
||||
klog.InfoS("supervisor is ready",
|
||||
plog.Debug("supervisor is ready",
|
||||
"httpAddress", httpListener.Addr().String(),
|
||||
"httpsAddress", httpsListener.Addr().String(),
|
||||
)
|
||||
|
||||
gotSignal := waitForSignal()
|
||||
klog.InfoS("supervisor exiting", "signal", gotSignal)
|
||||
plog.Debug("supervisor exiting", "signal", gotSignal)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -242,6 +364,7 @@ func run(serverInstallationNamespace string, cfg *supervisor.Config) error {
|
||||
func main() {
|
||||
logs.InitLogs()
|
||||
defer logs.FlushLogs()
|
||||
plog.RemoveKlogGlobalFlags() // move this whenever the below code gets refactored to use cobra
|
||||
|
||||
klog.Infof("Running %s at %#v", rest.DefaultKubernetesUserAgent(), version.Get())
|
||||
klog.Infof("Command-line arguments were: %s %s %s", os.Args[0], os.Args[1], os.Args[2])
|
||||
@@ -258,7 +381,7 @@ func main() {
|
||||
klog.Fatal(fmt.Errorf("could not load config: %w", err))
|
||||
}
|
||||
|
||||
if err := run(podInfo.Namespace, cfg); err != nil {
|
||||
if err := run(podInfo, cfg); err != nil {
|
||||
klog.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,3 +22,9 @@ func mustMarkHidden(cmd *cobra.Command, flags ...string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarkDeprecated(cmd *cobra.Command, flag, usageMessage string) {
|
||||
if err := cmd.Flags().MarkDeprecated(flag, usageMessage); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
|
||||
auth1alpha1 "go.pinniped.dev/generated/1.19/apis/concierge/authentication/v1alpha1"
|
||||
"go.pinniped.dev/internal/client"
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/here"
|
||||
)
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(newExchangeCredentialCmd(os.Args, os.Stdout, os.Stderr).cmd)
|
||||
}
|
||||
|
||||
type exchangeCredentialCommand struct {
|
||||
// runFunc is called by the cobra.Command.Run hook. It is included here for
|
||||
// testability.
|
||||
runFunc func(stdout, stderr io.Writer)
|
||||
|
||||
// cmd is the cobra.Command for this CLI command. It is included here for
|
||||
// testability.
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func newExchangeCredentialCmd(args []string, stdout, stderr io.Writer) *exchangeCredentialCommand {
|
||||
c := &exchangeCredentialCommand{
|
||||
runFunc: runExchangeCredential,
|
||||
}
|
||||
|
||||
c.cmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
c.runFunc(stdout, stderr)
|
||||
},
|
||||
Args: cobra.NoArgs, // do not accept positional arguments for this command
|
||||
Use: "exchange-credential",
|
||||
Short: "Exchange a credential for a cluster-specific access credential",
|
||||
Long: here.Doc(`
|
||||
Exchange a credential which proves your identity for a time-limited,
|
||||
cluster-specific access credential.
|
||||
|
||||
Designed to be conveniently used as an credential plugin for kubectl.
|
||||
See the help message for 'pinniped get-kubeconfig' for more
|
||||
information about setting up a kubeconfig file using Pinniped.
|
||||
|
||||
Requires all of the following environment variables, which are
|
||||
typically set in the kubeconfig:
|
||||
- PINNIPED_TOKEN: the token to send to Pinniped for exchange
|
||||
- PINNIPED_NAMESPACE: the namespace of the authenticator to authenticate
|
||||
against
|
||||
- PINNIPED_AUTHENTICATOR_TYPE: the type of authenticator to authenticate
|
||||
against (e.g., "webhook")
|
||||
- PINNIPED_AUTHENTICATOR_NAME: the name of the authenticator to authenticate
|
||||
against
|
||||
- PINNIPED_CA_BUNDLE: the CA bundle to trust when calling
|
||||
Pinniped's HTTPS endpoint
|
||||
- PINNIPED_K8S_API_ENDPOINT: the URL for the Pinniped credential
|
||||
exchange API
|
||||
|
||||
For more information about credential plugins in general, see
|
||||
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
|
||||
`),
|
||||
}
|
||||
|
||||
c.cmd.SetArgs(args)
|
||||
c.cmd.SetOut(stdout)
|
||||
c.cmd.SetErr(stderr)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
type envGetter func(string) (string, bool)
|
||||
type tokenExchanger func(
|
||||
ctx context.Context,
|
||||
namespace string,
|
||||
authenticator corev1.TypedLocalObjectReference,
|
||||
token string,
|
||||
caBundle string,
|
||||
apiEndpoint string,
|
||||
) (*clientauthenticationv1beta1.ExecCredential, error)
|
||||
|
||||
const (
|
||||
ErrMissingEnvVar = constable.Error("failed to get credential: environment variable not set")
|
||||
ErrInvalidAuthenticatorType = constable.Error("invalid authenticator type")
|
||||
)
|
||||
|
||||
func runExchangeCredential(stdout, _ io.Writer) {
|
||||
err := exchangeCredential(os.LookupEnv, client.ExchangeToken, stdout, 30*time.Second)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func exchangeCredential(envGetter envGetter, tokenExchanger tokenExchanger, outputWriter io.Writer, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
namespace, varExists := envGetter("PINNIPED_NAMESPACE")
|
||||
if !varExists {
|
||||
return envVarNotSetError("PINNIPED_NAMESPACE")
|
||||
}
|
||||
|
||||
authenticatorType, varExists := envGetter("PINNIPED_AUTHENTICATOR_TYPE")
|
||||
if !varExists {
|
||||
return envVarNotSetError("PINNIPED_AUTHENTICATOR_TYPE")
|
||||
}
|
||||
|
||||
authenticatorName, varExists := envGetter("PINNIPED_AUTHENTICATOR_NAME")
|
||||
if !varExists {
|
||||
return envVarNotSetError("PINNIPED_AUTHENTICATOR_NAME")
|
||||
}
|
||||
|
||||
token, varExists := envGetter("PINNIPED_TOKEN")
|
||||
if !varExists {
|
||||
return envVarNotSetError("PINNIPED_TOKEN")
|
||||
}
|
||||
|
||||
caBundle, varExists := envGetter("PINNIPED_CA_BUNDLE")
|
||||
if !varExists {
|
||||
return envVarNotSetError("PINNIPED_CA_BUNDLE")
|
||||
}
|
||||
|
||||
apiEndpoint, varExists := envGetter("PINNIPED_K8S_API_ENDPOINT")
|
||||
if !varExists {
|
||||
return envVarNotSetError("PINNIPED_K8S_API_ENDPOINT")
|
||||
}
|
||||
|
||||
authenticator := corev1.TypedLocalObjectReference{Name: authenticatorName}
|
||||
switch strings.ToLower(authenticatorType) {
|
||||
case "webhook":
|
||||
authenticator.APIGroup = &auth1alpha1.SchemeGroupVersion.Group
|
||||
authenticator.Kind = "WebhookAuthenticator"
|
||||
default:
|
||||
return fmt.Errorf(`%w: %q, supported values are "webhook"`, ErrInvalidAuthenticatorType, authenticatorType)
|
||||
}
|
||||
|
||||
cred, err := tokenExchanger(ctx, namespace, authenticator, token, caBundle, apiEndpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get credential: %w", err)
|
||||
}
|
||||
|
||||
err = json.NewEncoder(outputWriter).Encode(cred)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal response to stdout: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func envVarNotSetError(varName string) error {
|
||||
return fmt.Errorf("%w: %s", ErrMissingEnvVar, varName)
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sclevine/spec"
|
||||
"github.com/sclevine/spec/report"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
var (
|
||||
knownGoodUsageForExchangeCredential = here.Doc(`
|
||||
Usage:
|
||||
exchange-credential [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for exchange-credential
|
||||
|
||||
`)
|
||||
|
||||
knownGoodHelpForExchangeCredential = here.Doc(`
|
||||
Exchange a credential which proves your identity for a time-limited,
|
||||
cluster-specific access credential.
|
||||
|
||||
Designed to be conveniently used as an credential plugin for kubectl.
|
||||
See the help message for 'pinniped get-kubeconfig' for more
|
||||
information about setting up a kubeconfig file using Pinniped.
|
||||
|
||||
Requires all of the following environment variables, which are
|
||||
typically set in the kubeconfig:
|
||||
- PINNIPED_TOKEN: the token to send to Pinniped for exchange
|
||||
- PINNIPED_NAMESPACE: the namespace of the authenticator to authenticate
|
||||
against
|
||||
- PINNIPED_AUTHENTICATOR_TYPE: the type of authenticator to authenticate
|
||||
against (e.g., "webhook")
|
||||
- PINNIPED_AUTHENTICATOR_NAME: the name of the authenticator to authenticate
|
||||
against
|
||||
- PINNIPED_CA_BUNDLE: the CA bundle to trust when calling
|
||||
Pinniped's HTTPS endpoint
|
||||
- PINNIPED_K8S_API_ENDPOINT: the URL for the Pinniped credential
|
||||
exchange API
|
||||
|
||||
For more information about credential plugins in general, see
|
||||
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
|
||||
|
||||
Usage:
|
||||
exchange-credential [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for exchange-credential
|
||||
`)
|
||||
)
|
||||
|
||||
func TestNewCredentialExchangeCmd(t *testing.T) {
|
||||
spec.Run(t, "newCredentialExchangeCmd", func(t *testing.T, when spec.G, it spec.S) {
|
||||
var r *require.Assertions
|
||||
var stdout, stderr *bytes.Buffer
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
|
||||
stdout, stderr = bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
|
||||
})
|
||||
|
||||
it("calls runFunc and does not print usage or help when correct arguments and flags are used", func() {
|
||||
c := newExchangeCredentialCmd([]string{}, stdout, stderr)
|
||||
|
||||
runFuncCalled := false
|
||||
c.runFunc = func(out, err io.Writer) {
|
||||
runFuncCalled = true
|
||||
}
|
||||
|
||||
r.NoError(c.cmd.Execute())
|
||||
r.True(runFuncCalled)
|
||||
r.Empty(stdout.String())
|
||||
r.Empty(stderr.String())
|
||||
})
|
||||
|
||||
it("fails when args are passed", func() {
|
||||
c := newExchangeCredentialCmd([]string{"some-arg"}, stdout, stderr)
|
||||
|
||||
runFuncCalled := false
|
||||
c.runFunc = func(out, err io.Writer) {
|
||||
runFuncCalled = true
|
||||
}
|
||||
|
||||
errorMessage := `unknown command "some-arg" for "exchange-credential"`
|
||||
r.EqualError(c.cmd.Execute(), errorMessage)
|
||||
r.False(runFuncCalled)
|
||||
|
||||
output := "Error: " + errorMessage + "\n" + knownGoodUsageForExchangeCredential
|
||||
r.Equal(output, stdout.String())
|
||||
r.Empty(stderr.String())
|
||||
})
|
||||
|
||||
it("prints a nice help message", func() {
|
||||
c := newExchangeCredentialCmd([]string{"--help"}, stdout, stderr)
|
||||
|
||||
runFuncCalled := false
|
||||
c.runFunc = func(out, err io.Writer) {
|
||||
runFuncCalled = true
|
||||
}
|
||||
|
||||
r.NoError(c.cmd.Execute())
|
||||
r.False(runFuncCalled)
|
||||
r.Equal(knownGoodHelpForExchangeCredential, stdout.String())
|
||||
r.Empty(stderr.String())
|
||||
})
|
||||
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||
}
|
||||
|
||||
func TestExchangeCredential(t *testing.T) {
|
||||
spec.Run(t, "cmd.exchangeCredential", func(t *testing.T, when spec.G, it spec.S) {
|
||||
var r *require.Assertions
|
||||
var buffer *bytes.Buffer
|
||||
var tokenExchanger tokenExchanger
|
||||
var fakeEnv map[string]string
|
||||
|
||||
var envGetter envGetter = func(envVarName string) (string, bool) {
|
||||
value, present := fakeEnv[envVarName]
|
||||
if !present {
|
||||
return "", false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
it.Before(func() {
|
||||
r = require.New(t)
|
||||
buffer = new(bytes.Buffer)
|
||||
fakeEnv = map[string]string{
|
||||
"PINNIPED_NAMESPACE": "namespace from env",
|
||||
"PINNIPED_AUTHENTICATOR_TYPE": "Webhook",
|
||||
"PINNIPED_AUTHENTICATOR_NAME": "webhook name from env",
|
||||
"PINNIPED_TOKEN": "token from env",
|
||||
"PINNIPED_CA_BUNDLE": "ca bundle from env",
|
||||
"PINNIPED_K8S_API_ENDPOINT": "k8s api from env",
|
||||
}
|
||||
})
|
||||
|
||||
when("env vars are missing", func() {
|
||||
it("returns an error when PINNIPED_NAMESPACE is missing", func() {
|
||||
delete(fakeEnv, "PINNIPED_NAMESPACE")
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_NAMESPACE")
|
||||
})
|
||||
|
||||
it("returns an error when PINNIPED_AUTHENTICATOR_TYPE is missing", func() {
|
||||
delete(fakeEnv, "PINNIPED_AUTHENTICATOR_TYPE")
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_AUTHENTICATOR_TYPE")
|
||||
})
|
||||
|
||||
it("returns an error when PINNIPED_AUTHENTICATOR_NAME is missing", func() {
|
||||
delete(fakeEnv, "PINNIPED_AUTHENTICATOR_NAME")
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_AUTHENTICATOR_NAME")
|
||||
})
|
||||
|
||||
it("returns an error when PINNIPED_TOKEN is missing", func() {
|
||||
delete(fakeEnv, "PINNIPED_TOKEN")
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_TOKEN")
|
||||
})
|
||||
|
||||
it("returns an error when PINNIPED_CA_BUNDLE is missing", func() {
|
||||
delete(fakeEnv, "PINNIPED_CA_BUNDLE")
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_CA_BUNDLE")
|
||||
})
|
||||
|
||||
it("returns an error when PINNIPED_K8S_API_ENDPOINT is missing", func() {
|
||||
delete(fakeEnv, "PINNIPED_K8S_API_ENDPOINT")
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_K8S_API_ENDPOINT")
|
||||
})
|
||||
})
|
||||
|
||||
when("env vars are invalid", func() {
|
||||
it("returns an error when PINNIPED_AUTHENTICATOR_TYPE is missing", func() {
|
||||
fakeEnv["PINNIPED_AUTHENTICATOR_TYPE"] = "invalid"
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.EqualError(err, `invalid authenticator type: "invalid", supported values are "webhook"`)
|
||||
})
|
||||
})
|
||||
|
||||
when("the token exchange fails", func() {
|
||||
it.Before(func() {
|
||||
tokenExchanger = func(ctx context.Context, namespace string, authenticator corev1.TypedLocalObjectReference, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
return nil, fmt.Errorf("some error")
|
||||
}
|
||||
})
|
||||
|
||||
it("returns an error", func() {
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.EqualError(err, "failed to get credential: some error")
|
||||
})
|
||||
})
|
||||
|
||||
when("the JSON encoder fails", func() {
|
||||
it.Before(func() {
|
||||
tokenExchanger = func(ctx context.Context, namespace string, authenticator corev1.TypedLocalObjectReference, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
return &clientauthenticationv1beta1.ExecCredential{
|
||||
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||
Token: "some token",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
})
|
||||
|
||||
it("returns an error", func() {
|
||||
err := exchangeCredential(envGetter, tokenExchanger, &testutil.ErrorWriter{ReturnError: fmt.Errorf("some IO error")}, 30*time.Second)
|
||||
r.EqualError(err, "failed to marshal response to stdout: some IO error")
|
||||
})
|
||||
})
|
||||
|
||||
when("the token exchange times out", func() {
|
||||
it.Before(func() {
|
||||
tokenExchanger = func(ctx context.Context, namespace string, authenticator corev1.TypedLocalObjectReference, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return &clientauthenticationv1beta1.ExecCredential{
|
||||
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||
Token: "some token",
|
||||
},
|
||||
}, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("returns an error", func() {
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 1*time.Millisecond)
|
||||
r.EqualError(err, "failed to get credential: context deadline exceeded")
|
||||
})
|
||||
})
|
||||
|
||||
when("the token exchange succeeds", func() {
|
||||
var actualNamespace, actualToken, actualCaBundle, actualAPIEndpoint string
|
||||
|
||||
it.Before(func() {
|
||||
tokenExchanger = func(ctx context.Context, namespace string, authenticator corev1.TypedLocalObjectReference, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||
actualNamespace, actualToken, actualCaBundle, actualAPIEndpoint = namespace, token, caBundle, apiEndpoint
|
||||
now := metav1.NewTime(time.Date(2020, 7, 29, 1, 2, 3, 0, time.UTC))
|
||||
return &clientauthenticationv1beta1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ExecCredential",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||
ExpirationTimestamp: &now,
|
||||
ClientCertificateData: "some certificate",
|
||||
ClientKeyData: "some key",
|
||||
Token: "some token",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
})
|
||||
|
||||
it("writes the execCredential to the given writer", func() {
|
||||
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||
r.NoError(err)
|
||||
r.Equal(fakeEnv["PINNIPED_NAMESPACE"], actualNamespace)
|
||||
r.Equal(fakeEnv["PINNIPED_TOKEN"], actualToken)
|
||||
r.Equal(fakeEnv["PINNIPED_CA_BUNDLE"], actualCaBundle)
|
||||
r.Equal(fakeEnv["PINNIPED_K8S_API_ENDPOINT"], actualAPIEndpoint)
|
||||
expected := `{
|
||||
"kind": "ExecCredential",
|
||||
"apiVersion": "client.authentication.k8s.io/v1beta1",
|
||||
"spec": {},
|
||||
"status": {
|
||||
"expirationTimestamp":"2020-07-29T01:02:03Z",
|
||||
"clientCertificateData": "some certificate",
|
||||
"clientKeyData":"some key",
|
||||
"token": "some token"
|
||||
}
|
||||
}`
|
||||
r.JSONEq(expected, buffer.String())
|
||||
})
|
||||
})
|
||||
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||
}
|
||||
106
cmd/pinniped/cmd/flag_types.go
Normal file
106
cmd/pinniped/cmd/flag_types.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||
)
|
||||
|
||||
// conciergeModeFlag represents the method by which we should connect to the Concierge on a cluster during login.
|
||||
// this is meant to be a valid flag.Value implementation.
|
||||
type conciergeModeFlag int
|
||||
|
||||
var _ flag.Value = new(conciergeModeFlag)
|
||||
|
||||
const (
|
||||
modeUnknown conciergeModeFlag = iota
|
||||
modeTokenCredentialRequestAPI
|
||||
modeImpersonationProxy
|
||||
)
|
||||
|
||||
func (f *conciergeModeFlag) String() string {
|
||||
switch *f {
|
||||
case modeImpersonationProxy:
|
||||
return "ImpersonationProxy"
|
||||
case modeTokenCredentialRequestAPI:
|
||||
return "TokenCredentialRequestAPI"
|
||||
case modeUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
return "TokenCredentialRequestAPI"
|
||||
}
|
||||
}
|
||||
|
||||
func (f *conciergeModeFlag) Set(s string) error {
|
||||
if strings.EqualFold(s, "") {
|
||||
*f = modeUnknown
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(s, "TokenCredentialRequestAPI") {
|
||||
*f = modeTokenCredentialRequestAPI
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(s, "ImpersonationProxy") {
|
||||
*f = modeImpersonationProxy
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid mode %q, valid modes are TokenCredentialRequestAPI and ImpersonationProxy", s)
|
||||
}
|
||||
|
||||
func (f *conciergeModeFlag) Type() string {
|
||||
return "mode"
|
||||
}
|
||||
|
||||
// MatchesFrontend returns true iff the flag matches the type of the provided frontend.
|
||||
func (f *conciergeModeFlag) MatchesFrontend(frontend *configv1alpha1.CredentialIssuerFrontend) bool {
|
||||
switch *f {
|
||||
case modeImpersonationProxy:
|
||||
return frontend.Type == configv1alpha1.ImpersonationProxyFrontendType
|
||||
case modeTokenCredentialRequestAPI:
|
||||
return frontend.Type == configv1alpha1.TokenCredentialRequestAPIFrontendType
|
||||
case modeUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// caBundlePathsVar represents a list of CA bundle paths, which load from disk when the flag is populated.
|
||||
type caBundleFlag []byte
|
||||
|
||||
var _ pflag.Value = new(caBundleFlag)
|
||||
|
||||
func (f *caBundleFlag) String() string {
|
||||
return string(*f)
|
||||
}
|
||||
|
||||
func (f *caBundleFlag) Set(path string) error {
|
||||
pem, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read CA bundle path: %w", err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(pem) {
|
||||
return fmt.Errorf("failed to load any CA certificates from %q", path)
|
||||
}
|
||||
if len(*f) == 0 {
|
||||
*f = pem
|
||||
return nil
|
||||
}
|
||||
*f = bytes.Join([][]byte{*f, pem}, []byte("\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *caBundleFlag) Type() string {
|
||||
return "path"
|
||||
}
|
||||
73
cmd/pinniped/cmd/flag_types_test.go
Normal file
73
cmd/pinniped/cmd/flag_types_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
func TestConciergeModeFlag(t *testing.T) {
|
||||
var f conciergeModeFlag
|
||||
require.Equal(t, "mode", f.Type())
|
||||
require.Equal(t, modeUnknown, f)
|
||||
require.NoError(t, f.Set(""))
|
||||
require.Equal(t, modeUnknown, f)
|
||||
require.EqualError(t, f.Set("foo"), `invalid mode "foo", valid modes are TokenCredentialRequestAPI and ImpersonationProxy`)
|
||||
require.True(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||
require.True(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.ImpersonationProxyFrontendType}))
|
||||
|
||||
require.NoError(t, f.Set("TokenCredentialRequestAPI"))
|
||||
require.Equal(t, modeTokenCredentialRequestAPI, f)
|
||||
require.Equal(t, "TokenCredentialRequestAPI", f.String())
|
||||
require.True(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||
require.False(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.ImpersonationProxyFrontendType}))
|
||||
|
||||
require.NoError(t, f.Set("tokencredentialrequestapi"))
|
||||
require.Equal(t, modeTokenCredentialRequestAPI, f)
|
||||
require.Equal(t, "TokenCredentialRequestAPI", f.String())
|
||||
|
||||
require.NoError(t, f.Set("ImpersonationProxy"))
|
||||
require.Equal(t, modeImpersonationProxy, f)
|
||||
require.Equal(t, "ImpersonationProxy", f.String())
|
||||
require.False(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.TokenCredentialRequestAPIFrontendType}))
|
||||
require.True(t, f.MatchesFrontend(&configv1alpha1.CredentialIssuerFrontend{Type: configv1alpha1.ImpersonationProxyFrontendType}))
|
||||
|
||||
require.NoError(t, f.Set("impersonationproxy"))
|
||||
require.Equal(t, modeImpersonationProxy, f)
|
||||
require.Equal(t, "ImpersonationProxy", f.String())
|
||||
}
|
||||
|
||||
func TestCABundleFlag(t *testing.T) {
|
||||
testCA, err := certauthority.New("Test CA", 1*time.Hour)
|
||||
require.NoError(t, err)
|
||||
tmpdir := testutil.TempDir(t)
|
||||
emptyFilePath := filepath.Join(tmpdir, "empty")
|
||||
require.NoError(t, ioutil.WriteFile(emptyFilePath, []byte{}, 0600))
|
||||
|
||||
testCAPath := filepath.Join(tmpdir, "testca.pem")
|
||||
require.NoError(t, ioutil.WriteFile(testCAPath, testCA.Bundle(), 0600))
|
||||
|
||||
f := caBundleFlag{}
|
||||
require.Equal(t, "path", f.Type())
|
||||
require.Equal(t, "", f.String())
|
||||
require.EqualError(t, f.Set("./does/not/exist"), "could not read CA bundle path: open ./does/not/exist: no such file or directory")
|
||||
require.EqualError(t, f.Set(emptyFilePath), fmt.Sprintf("failed to load any CA certificates from %q", emptyFilePath))
|
||||
|
||||
require.NoError(t, f.Set(testCAPath))
|
||||
require.Equal(t, 1, bytes.Count(f, []byte("BEGIN CERTIFICATE")))
|
||||
|
||||
require.NoError(t, f.Set(testCAPath))
|
||||
require.Equal(t, 2, bytes.Count(f, []byte("BEGIN CERTIFICATE")))
|
||||
}
|
||||
114
cmd/pinniped/cmd/generate_markdown_help.go
Normal file
114
cmd/pinniped/cmd/generate_markdown_help.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(generateMarkdownHelpCommand())
|
||||
}
|
||||
|
||||
func generateMarkdownHelpCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
Use: "generate-markdown-help",
|
||||
Short: "Generate markdown help for the current set of non-hidden CLI commands",
|
||||
SilenceUsage: true,
|
||||
Hidden: true,
|
||||
RunE: runGenerateMarkdownHelp,
|
||||
}
|
||||
}
|
||||
|
||||
func runGenerateMarkdownHelp(cmd *cobra.Command, _ []string) error {
|
||||
var generated bytes.Buffer
|
||||
if err := generate(&generated); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := write(cmd.OutOrStdout(), &generated, "###### Auto generated by spf13/cobra"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generate(w io.Writer) error {
|
||||
if err := generateHeader(w); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := generateCommand(w, rootCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateHeader(w io.Writer) error {
|
||||
_, err := fmt.Fprintf(w, `---
|
||||
title: Command-Line Options Reference
|
||||
description: Reference for the `+"`pinniped`"+` command-line tool
|
||||
cascade:
|
||||
layout: docs
|
||||
menu:
|
||||
docs:
|
||||
name: Command-Line Options
|
||||
weight: 30
|
||||
parent: reference
|
||||
---
|
||||
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
func generateCommand(w io.Writer, command *cobra.Command) error {
|
||||
for _, command := range command.Commands() {
|
||||
// if this node is hidden, don't traverse it or its descendents
|
||||
if command.Hidden {
|
||||
continue
|
||||
}
|
||||
|
||||
// generate children
|
||||
if err := generateCommand(w, command); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// generate self, but only if we are a command that people would run to do something interesting
|
||||
if command.Run != nil || command.RunE != nil {
|
||||
if err := doc.GenMarkdownCustom(command, w, func(_ string) string { return "" }); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func write(w io.Writer, r io.Reader, unwantedPrefixes ...string) error {
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
if !containsPrefix(line, unwantedPrefixes) {
|
||||
if _, err := fmt.Fprintln(w, line); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return s.Err()
|
||||
}
|
||||
|
||||
func containsPrefix(s string, prefixes []string) bool {
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(s, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
16
cmd/pinniped/cmd/get.go
Normal file
16
cmd/pinniped/cmd/get.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
var getCmd = &cobra.Command{Use: "get", Short: "get"}
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(getCmd)
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/1.19/apis/concierge/config/v1alpha1"
|
||||
pinnipedclientset "go.pinniped.dev/generated/1.19/client/concierge/clientset/versioned"
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/here"
|
||||
)
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(newGetKubeConfigCommand().Command())
|
||||
}
|
||||
|
||||
type getKubeConfigFlags struct {
|
||||
token string
|
||||
kubeconfig string
|
||||
contextOverride string
|
||||
namespace string
|
||||
authenticatorName string
|
||||
authenticatorType string
|
||||
}
|
||||
|
||||
type getKubeConfigCommand struct {
|
||||
flags getKubeConfigFlags
|
||||
// Test mocking points
|
||||
getPathToSelf func() (string, error)
|
||||
kubeClientCreator func(restConfig *rest.Config) (pinnipedclientset.Interface, error)
|
||||
}
|
||||
|
||||
func newGetKubeConfigCommand() *getKubeConfigCommand {
|
||||
return &getKubeConfigCommand{
|
||||
flags: getKubeConfigFlags{
|
||||
namespace: "pinniped",
|
||||
},
|
||||
getPathToSelf: os.Executable,
|
||||
kubeClientCreator: func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return pinnipedclientset.NewForConfig(restConfig)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *getKubeConfigCommand) Command() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
RunE: c.run,
|
||||
Args: cobra.NoArgs, // do not accept positional arguments for this command
|
||||
Use: "get-kubeconfig",
|
||||
Short: "Print a kubeconfig for authenticating into a cluster via Pinniped",
|
||||
Long: here.Doc(`
|
||||
Print a kubeconfig for authenticating into a cluster via Pinniped.
|
||||
|
||||
Requires admin-like access to the cluster using the current
|
||||
kubeconfig context in order to access Pinniped's metadata.
|
||||
The current kubeconfig is found similar to how kubectl finds it:
|
||||
using the value of the --kubeconfig option, or if that is not
|
||||
specified then from the value of the KUBECONFIG environment
|
||||
variable, or if that is not specified then it defaults to
|
||||
.kube/config in your home directory.
|
||||
|
||||
Prints a kubeconfig which is suitable to access the cluster using
|
||||
Pinniped as the authentication mechanism. This kubeconfig output
|
||||
can be saved to a file and used with future kubectl commands, e.g.:
|
||||
pinniped get-kubeconfig --token $MY_TOKEN > $HOME/mycluster-kubeconfig
|
||||
kubectl --kubeconfig $HOME/mycluster-kubeconfig get pods
|
||||
`),
|
||||
}
|
||||
cmd.Flags().StringVar(&c.flags.token, "token", "", "Credential to include in the resulting kubeconfig output (Required)")
|
||||
cmd.Flags().StringVar(&c.flags.kubeconfig, "kubeconfig", c.flags.kubeconfig, "Path to the kubeconfig file")
|
||||
cmd.Flags().StringVar(&c.flags.contextOverride, "kubeconfig-context", c.flags.contextOverride, "Kubeconfig context override")
|
||||
cmd.Flags().StringVar(&c.flags.namespace, "pinniped-namespace", c.flags.namespace, "Namespace in which Pinniped was installed")
|
||||
cmd.Flags().StringVar(&c.flags.authenticatorType, "authenticator-type", c.flags.authenticatorType, "Authenticator type (e.g., 'webhook')")
|
||||
cmd.Flags().StringVar(&c.flags.authenticatorName, "authenticator-name", c.flags.authenticatorType, "Authenticator name")
|
||||
mustMarkRequired(cmd, "token")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *getKubeConfigCommand) run(cmd *cobra.Command, args []string) error {
|
||||
fullPathToSelf, err := c.getPathToSelf()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not find path to self: %w", err)
|
||||
}
|
||||
|
||||
clientConfig := newClientConfig(c.flags.kubeconfig, c.flags.contextOverride)
|
||||
|
||||
currentKubeConfig, err := clientConfig.RawConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
restConfig, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientset, err := c.kubeClientCreator(restConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authenticatorType, authenticatorName := c.flags.authenticatorType, c.flags.authenticatorName
|
||||
if authenticatorType == "" || authenticatorName == "" {
|
||||
authenticatorType, authenticatorName, err = getDefaultAuthenticator(clientset, c.flags.namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
credentialIssuer, err := fetchPinnipedCredentialIssuer(clientset, c.flags.namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if credentialIssuer.Status.KubeConfigInfo == nil {
|
||||
return constable.Error(`CredentialIssuer "pinniped-config" was missing KubeConfigInfo`)
|
||||
}
|
||||
|
||||
v1Cluster, err := copyCurrentClusterFromExistingKubeConfig(currentKubeConfig, c.flags.contextOverride)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = issueWarningForNonMatchingServerOrCA(v1Cluster, credentialIssuer, cmd.ErrOrStderr())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := newPinnipedKubeconfig(v1Cluster, fullPathToSelf, c.flags.token, c.flags.namespace, authenticatorType, authenticatorName)
|
||||
|
||||
err = writeConfigAsYAML(cmd.OutOrStdout(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func issueWarningForNonMatchingServerOrCA(v1Cluster v1.Cluster, credentialIssuer *configv1alpha1.CredentialIssuer, warningsWriter io.Writer) error {
|
||||
credentialIssuerCA, err := base64.StdEncoding.DecodeString(credentialIssuer.Status.KubeConfigInfo.CertificateAuthorityData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v1Cluster.Server != credentialIssuer.Status.KubeConfigInfo.Server ||
|
||||
!bytes.Equal(v1Cluster.CertificateAuthorityData, credentialIssuerCA) {
|
||||
_, err := warningsWriter.Write([]byte("WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuer on the cluster. Using local kubeconfig values.\n"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("output write error: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type noAuthenticatorError struct{ Namespace string }
|
||||
|
||||
func (e noAuthenticatorError) Error() string {
|
||||
return fmt.Sprintf(`no authenticators were found in namespace %q`, e.Namespace)
|
||||
}
|
||||
|
||||
type indeterminateAuthenticatorError struct{ Namespace string }
|
||||
|
||||
func (e indeterminateAuthenticatorError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
`multiple authenticators were found in namespace %q, so --authenticator-name/--authenticator-type must be specified`,
|
||||
e.Namespace,
|
||||
)
|
||||
}
|
||||
|
||||
func getDefaultAuthenticator(clientset pinnipedclientset.Interface, namespace string) (string, string, error) {
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
||||
defer cancelFunc()
|
||||
|
||||
webhooks, err := clientset.AuthenticationV1alpha1().WebhookAuthenticators(namespace).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
type ref struct{ authenticatorType, authenticatorName string }
|
||||
authenticators := make([]ref, 0, len(webhooks.Items))
|
||||
for _, webhook := range webhooks.Items {
|
||||
authenticators = append(authenticators, ref{authenticatorType: "webhook", authenticatorName: webhook.Name})
|
||||
}
|
||||
|
||||
if len(authenticators) == 0 {
|
||||
return "", "", noAuthenticatorError{namespace}
|
||||
}
|
||||
if len(authenticators) > 1 {
|
||||
return "", "", indeterminateAuthenticatorError{namespace}
|
||||
}
|
||||
return authenticators[0].authenticatorType, authenticators[0].authenticatorName, nil
|
||||
}
|
||||
|
||||
func fetchPinnipedCredentialIssuer(clientset pinnipedclientset.Interface, pinnipedInstallationNamespace string) (*configv1alpha1.CredentialIssuer, error) {
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
||||
defer cancelFunc()
|
||||
|
||||
credentialIssuers, err := clientset.ConfigV1alpha1().CredentialIssuers(pinnipedInstallationNamespace).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(credentialIssuers.Items) == 0 {
|
||||
return nil, constable.Error(fmt.Sprintf(
|
||||
`No CredentialIssuer was found in namespace "%s". Is Pinniped installed on this cluster in namespace "%s"?`,
|
||||
pinnipedInstallationNamespace,
|
||||
pinnipedInstallationNamespace,
|
||||
))
|
||||
}
|
||||
|
||||
if len(credentialIssuers.Items) > 1 {
|
||||
return nil, constable.Error(fmt.Sprintf(
|
||||
`More than one CredentialIssuer was found in namespace "%s"`,
|
||||
pinnipedInstallationNamespace,
|
||||
))
|
||||
}
|
||||
|
||||
return &credentialIssuers.Items[0], nil
|
||||
}
|
||||
|
||||
func newClientConfig(kubeconfigPathOverride string, currentContextName string) clientcmd.ClientConfig {
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
loadingRules.ExplicitPath = kubeconfigPathOverride
|
||||
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{
|
||||
CurrentContext: currentContextName,
|
||||
})
|
||||
return clientConfig
|
||||
}
|
||||
|
||||
func writeConfigAsYAML(outputWriter io.Writer, config v1.Config) error {
|
||||
output, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("YAML serialization error: %w", err)
|
||||
}
|
||||
|
||||
_, err = outputWriter.Write(output)
|
||||
if err != nil {
|
||||
return fmt.Errorf("output write error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyCurrentClusterFromExistingKubeConfig(currentKubeConfig clientcmdapi.Config, currentContextNameOverride string) (v1.Cluster, error) {
|
||||
v1Cluster := v1.Cluster{}
|
||||
|
||||
contextName := currentKubeConfig.CurrentContext
|
||||
if currentContextNameOverride != "" {
|
||||
contextName = currentContextNameOverride
|
||||
}
|
||||
|
||||
err := v1.Convert_api_Cluster_To_v1_Cluster(
|
||||
currentKubeConfig.Clusters[currentKubeConfig.Contexts[contextName].Cluster],
|
||||
&v1Cluster,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return v1.Cluster{}, err
|
||||
}
|
||||
|
||||
return v1Cluster, nil
|
||||
}
|
||||
|
||||
func newPinnipedKubeconfig(v1Cluster v1.Cluster, fullPathToSelf string, token string, namespace string, authenticatorType string, authenticatorName string) v1.Config {
|
||||
clusterName := "pinniped-cluster"
|
||||
userName := "pinniped-user"
|
||||
|
||||
return v1.Config{
|
||||
Kind: "Config",
|
||||
APIVersion: v1.SchemeGroupVersion.Version,
|
||||
Preferences: v1.Preferences{},
|
||||
Clusters: []v1.NamedCluster{
|
||||
{
|
||||
Name: clusterName,
|
||||
Cluster: v1Cluster,
|
||||
},
|
||||
},
|
||||
Contexts: []v1.NamedContext{
|
||||
{
|
||||
Name: clusterName,
|
||||
Context: v1.Context{
|
||||
Cluster: clusterName,
|
||||
AuthInfo: userName,
|
||||
},
|
||||
},
|
||||
},
|
||||
AuthInfos: []v1.NamedAuthInfo{
|
||||
{
|
||||
Name: userName,
|
||||
AuthInfo: v1.AuthInfo{
|
||||
Exec: &v1.ExecConfig{
|
||||
Command: fullPathToSelf,
|
||||
Args: []string{"exchange-credential"},
|
||||
Env: []v1.ExecEnvVar{
|
||||
{
|
||||
Name: "PINNIPED_K8S_API_ENDPOINT",
|
||||
Value: v1Cluster.Server,
|
||||
},
|
||||
{
|
||||
Name: "PINNIPED_CA_BUNDLE",
|
||||
Value: string(v1Cluster.CertificateAuthorityData)},
|
||||
{
|
||||
Name: "PINNIPED_NAMESPACE",
|
||||
Value: namespace,
|
||||
},
|
||||
{
|
||||
Name: "PINNIPED_TOKEN",
|
||||
Value: token,
|
||||
},
|
||||
{
|
||||
Name: "PINNIPED_AUTHENTICATOR_TYPE",
|
||||
Value: authenticatorType,
|
||||
},
|
||||
{
|
||||
Name: "PINNIPED_AUTHENTICATOR_NAME",
|
||||
Value: authenticatorName,
|
||||
},
|
||||
},
|
||||
APIVersion: clientauthenticationv1beta1.SchemeGroupVersion.String(),
|
||||
InstallHint: "The Pinniped CLI is required to authenticate to the current cluster.\n" +
|
||||
"For more information, please visit https://pinniped.dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
CurrentContext: clusterName,
|
||||
}
|
||||
}
|
||||
@@ -1,401 +0,0 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/rest"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
|
||||
authv1alpha "go.pinniped.dev/generated/1.19/apis/concierge/authentication/v1alpha1"
|
||||
configv1alpha1 "go.pinniped.dev/generated/1.19/apis/concierge/config/v1alpha1"
|
||||
pinnipedclientset "go.pinniped.dev/generated/1.19/client/concierge/clientset/versioned"
|
||||
pinnipedfake "go.pinniped.dev/generated/1.19/client/concierge/clientset/versioned/fake"
|
||||
"go.pinniped.dev/internal/here"
|
||||
)
|
||||
|
||||
var (
|
||||
knownGoodUsageForGetKubeConfig = here.Doc(`
|
||||
Usage:
|
||||
get-kubeconfig [flags]
|
||||
|
||||
Flags:
|
||||
--authenticator-name string Authenticator name
|
||||
--authenticator-type string Authenticator type (e.g., 'webhook')
|
||||
-h, --help help for get-kubeconfig
|
||||
--kubeconfig string Path to the kubeconfig file
|
||||
--kubeconfig-context string Kubeconfig context override
|
||||
--pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped")
|
||||
--token string Credential to include in the resulting kubeconfig output (Required)
|
||||
|
||||
`)
|
||||
|
||||
knownGoodHelpForGetKubeConfig = here.Doc(`
|
||||
Print a kubeconfig for authenticating into a cluster via Pinniped.
|
||||
|
||||
Requires admin-like access to the cluster using the current
|
||||
kubeconfig context in order to access Pinniped's metadata.
|
||||
The current kubeconfig is found similar to how kubectl finds it:
|
||||
using the value of the --kubeconfig option, or if that is not
|
||||
specified then from the value of the KUBECONFIG environment
|
||||
variable, or if that is not specified then it defaults to
|
||||
.kube/config in your home directory.
|
||||
|
||||
Prints a kubeconfig which is suitable to access the cluster using
|
||||
Pinniped as the authentication mechanism. This kubeconfig output
|
||||
can be saved to a file and used with future kubectl commands, e.g.:
|
||||
pinniped get-kubeconfig --token $MY_TOKEN > $HOME/mycluster-kubeconfig
|
||||
kubectl --kubeconfig $HOME/mycluster-kubeconfig get pods
|
||||
|
||||
Usage:
|
||||
get-kubeconfig [flags]
|
||||
|
||||
Flags:
|
||||
--authenticator-name string Authenticator name
|
||||
--authenticator-type string Authenticator type (e.g., 'webhook')
|
||||
-h, --help help for get-kubeconfig
|
||||
--kubeconfig string Path to the kubeconfig file
|
||||
--kubeconfig-context string Kubeconfig context override
|
||||
--pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped")
|
||||
--token string Credential to include in the resulting kubeconfig output (Required)
|
||||
`)
|
||||
)
|
||||
|
||||
func TestNewGetKubeConfigCmd(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantError bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "help flag passed",
|
||||
args: []string{"--help"},
|
||||
wantStdout: knownGoodHelpForGetKubeConfig,
|
||||
},
|
||||
{
|
||||
name: "missing required flag",
|
||||
args: []string{},
|
||||
wantError: true,
|
||||
wantStdout: `Error: required flag(s) "token" not set` + "\n" + knownGoodUsageForGetKubeConfig,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cmd := newGetKubeConfigCommand().Command()
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.SetOut(&stdout)
|
||||
cmd.SetErr(&stderr)
|
||||
cmd.SetArgs(tt.args)
|
||||
err := cmd.Execute()
|
||||
if tt.wantError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
||||
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type expectedKubeconfigYAML struct {
|
||||
clusterCAData string
|
||||
clusterServer string
|
||||
command string
|
||||
token string
|
||||
pinnipedEndpoint string
|
||||
pinnipedCABundle string
|
||||
namespace string
|
||||
authenticatorType string
|
||||
authenticatorName string
|
||||
}
|
||||
|
||||
func (e expectedKubeconfigYAML) String() string {
|
||||
return here.Docf(`
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: %s
|
||||
server: %s
|
||||
name: pinniped-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: pinniped-cluster
|
||||
user: pinniped-user
|
||||
name: pinniped-cluster
|
||||
current-context: pinniped-cluster
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: pinniped-user
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
args:
|
||||
- exchange-credential
|
||||
command: %s
|
||||
env:
|
||||
- name: PINNIPED_K8S_API_ENDPOINT
|
||||
value: %s
|
||||
- name: PINNIPED_CA_BUNDLE
|
||||
value: %s
|
||||
- name: PINNIPED_NAMESPACE
|
||||
value: %s
|
||||
- name: PINNIPED_TOKEN
|
||||
value: %s
|
||||
- name: PINNIPED_AUTHENTICATOR_TYPE
|
||||
value: %s
|
||||
- name: PINNIPED_AUTHENTICATOR_NAME
|
||||
value: %s
|
||||
installHint: |-
|
||||
The Pinniped CLI is required to authenticate to the current cluster.
|
||||
For more information, please visit https://pinniped.dev
|
||||
`, e.clusterCAData, e.clusterServer, e.command, e.pinnipedEndpoint, e.pinnipedCABundle, e.namespace, e.token, e.authenticatorType, e.authenticatorName)
|
||||
}
|
||||
|
||||
func newCredentialIssuer(name, namespace, server, certificateAuthorityData string) *configv1alpha1.CredentialIssuer {
|
||||
return &configv1alpha1.CredentialIssuer{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "CredentialIssuer",
|
||||
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Status: configv1alpha1.CredentialIssuerStatus{
|
||||
KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{
|
||||
Server: server,
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(certificateAuthorityData)),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
mocks func(*getKubeConfigCommand)
|
||||
wantError string
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "failure to get path to self",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
cmd.getPathToSelf = func() (string, error) {
|
||||
return "", fmt.Errorf("some error getting path to self")
|
||||
}
|
||||
},
|
||||
wantError: "could not find path to self: some error getting path to self",
|
||||
},
|
||||
{
|
||||
name: "kubeconfig does not exist",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
cmd.flags.kubeconfig = "./testdata/does-not-exist.yaml"
|
||||
},
|
||||
wantError: "stat ./testdata/does-not-exist.yaml: no such file or directory",
|
||||
},
|
||||
{
|
||||
name: "fail to get client",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return nil, fmt.Errorf("some error configuring clientset")
|
||||
}
|
||||
},
|
||||
wantError: "some error configuring clientset",
|
||||
},
|
||||
{
|
||||
name: "fail to get authenticators",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
cmd.flags.authenticatorName = ""
|
||||
cmd.flags.authenticatorType = ""
|
||||
clientset := pinnipedfake.NewSimpleClientset()
|
||||
clientset.PrependReactor("*", "*", func(_ coretesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, fmt.Errorf("some error getting authenticators")
|
||||
})
|
||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return clientset, nil
|
||||
}
|
||||
},
|
||||
wantError: "some error getting authenticators",
|
||||
},
|
||||
{
|
||||
name: "zero authenticators",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
cmd.flags.authenticatorName = ""
|
||||
cmd.flags.authenticatorType = ""
|
||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return pinnipedfake.NewSimpleClientset(), nil
|
||||
}
|
||||
},
|
||||
wantError: `no authenticators were found in namespace "test-namespace"`,
|
||||
},
|
||||
{
|
||||
name: "multiple authenticators",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
cmd.flags.authenticatorName = ""
|
||||
cmd.flags.authenticatorType = ""
|
||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return pinnipedfake.NewSimpleClientset(
|
||||
&authv1alpha.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Namespace: "test-namespace", Name: "webhook-one"}},
|
||||
&authv1alpha.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Namespace: "test-namespace", Name: "webhook-two"}},
|
||||
), nil
|
||||
}
|
||||
},
|
||||
wantError: `multiple authenticators were found in namespace "test-namespace", so --authenticator-name/--authenticator-type must be specified`,
|
||||
},
|
||||
{
|
||||
name: "fail to get CredentialIssuers",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
clientset := pinnipedfake.NewSimpleClientset()
|
||||
clientset.PrependReactor("*", "*", func(_ coretesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, fmt.Errorf("some error getting CredentialIssuers")
|
||||
})
|
||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return clientset, nil
|
||||
}
|
||||
},
|
||||
wantError: "some error getting CredentialIssuers",
|
||||
},
|
||||
{
|
||||
name: "zero CredentialIssuers found",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return pinnipedfake.NewSimpleClientset(
|
||||
newCredentialIssuer("pinniped-config-1", "not-the-test-namespace", "", ""),
|
||||
), nil
|
||||
}
|
||||
},
|
||||
wantError: `No CredentialIssuer was found in namespace "test-namespace". Is Pinniped installed on this cluster in namespace "test-namespace"?`,
|
||||
},
|
||||
{
|
||||
name: "multiple CredentialIssuers found",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return pinnipedfake.NewSimpleClientset(
|
||||
newCredentialIssuer("pinniped-config-1", "test-namespace", "", ""),
|
||||
newCredentialIssuer("pinniped-config-2", "test-namespace", "", ""),
|
||||
), nil
|
||||
}
|
||||
},
|
||||
wantError: `More than one CredentialIssuer was found in namespace "test-namespace"`,
|
||||
},
|
||||
{
|
||||
name: "CredentialIssuer missing KubeConfigInfo",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
ci := newCredentialIssuer("pinniped-config", "test-namespace", "", "")
|
||||
ci.Status.KubeConfigInfo = nil
|
||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return pinnipedfake.NewSimpleClientset(ci), nil
|
||||
}
|
||||
},
|
||||
wantError: `CredentialIssuer "pinniped-config" was missing KubeConfigInfo`,
|
||||
},
|
||||
{
|
||||
name: "KubeConfigInfo has invalid base64",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
ci := newCredentialIssuer("pinniped-config", "test-namespace", "https://example.com", "")
|
||||
ci.Status.KubeConfigInfo.CertificateAuthorityData = "invalid-base64-test-ca"
|
||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return pinnipedfake.NewSimpleClientset(ci), nil
|
||||
}
|
||||
},
|
||||
wantError: `illegal base64 data at input byte 7`,
|
||||
},
|
||||
{
|
||||
name: "success using remote CA data",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
ci := newCredentialIssuer("pinniped-config", "test-namespace", "https://fake-server-url-value", "fake-certificate-authority-data-value")
|
||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return pinnipedfake.NewSimpleClientset(ci), nil
|
||||
}
|
||||
},
|
||||
wantStdout: expectedKubeconfigYAML{
|
||||
clusterCAData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==",
|
||||
clusterServer: "https://fake-server-url-value",
|
||||
command: "/path/to/pinniped",
|
||||
token: "test-token",
|
||||
pinnipedEndpoint: "https://fake-server-url-value",
|
||||
pinnipedCABundle: "fake-certificate-authority-data-value",
|
||||
namespace: "test-namespace",
|
||||
authenticatorType: "test-authenticator-type",
|
||||
authenticatorName: "test-authenticator-name",
|
||||
}.String(),
|
||||
},
|
||||
{
|
||||
name: "success using local CA data and discovered authenticator",
|
||||
mocks: func(cmd *getKubeConfigCommand) {
|
||||
cmd.flags.authenticatorName = ""
|
||||
cmd.flags.authenticatorType = ""
|
||||
|
||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
||||
return pinnipedfake.NewSimpleClientset(
|
||||
&authv1alpha.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Namespace: "test-namespace", Name: "discovered-authenticator"}},
|
||||
newCredentialIssuer("pinniped-config", "test-namespace", "https://example.com", "test-ca"),
|
||||
), nil
|
||||
}
|
||||
},
|
||||
wantStderr: `WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuer on the cluster. Using local kubeconfig values.`,
|
||||
wantStdout: expectedKubeconfigYAML{
|
||||
clusterCAData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==",
|
||||
clusterServer: "https://fake-server-url-value",
|
||||
command: "/path/to/pinniped",
|
||||
token: "test-token",
|
||||
pinnipedEndpoint: "https://fake-server-url-value",
|
||||
pinnipedCABundle: "fake-certificate-authority-data-value",
|
||||
namespace: "test-namespace",
|
||||
authenticatorType: "webhook",
|
||||
authenticatorName: "discovered-authenticator",
|
||||
}.String(),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Start with a default getKubeConfigCommand, set some defaults, then apply any mocks.
|
||||
c := newGetKubeConfigCommand()
|
||||
c.flags.token = "test-token"
|
||||
c.flags.namespace = "test-namespace"
|
||||
c.flags.authenticatorName = "test-authenticator-name"
|
||||
c.flags.authenticatorType = "test-authenticator-type"
|
||||
c.getPathToSelf = func() (string, error) { return "/path/to/pinniped", nil }
|
||||
c.flags.kubeconfig = "./testdata/kubeconfig.yaml"
|
||||
tt.mocks(c)
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.SetOut(&stdout)
|
||||
cmd.SetErr(&stderr)
|
||||
cmd.SetArgs([]string{})
|
||||
err := c.run(cmd, []string{})
|
||||
if tt.wantError != "" {
|
||||
require.EqualError(t, err, tt.wantError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, strings.TrimSpace(tt.wantStdout), strings.TrimSpace(stdout.String()), "unexpected stdout")
|
||||
require.Equal(t, strings.TrimSpace(tt.wantStderr), strings.TrimSpace(stderr.String()), "unexpected stderr")
|
||||
})
|
||||
}
|
||||
}
|
||||
43
cmd/pinniped/cmd/kube_util.go
Normal file
43
cmd/pinniped/cmd/kube_util.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/kubeclient"
|
||||
)
|
||||
|
||||
// getConciergeClientsetFunc is a function that can return a clientset for the Concierge API given a
|
||||
// clientConfig and the apiGroupSuffix with which the API is running.
|
||||
type getConciergeClientsetFunc func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error)
|
||||
|
||||
// getRealConciergeClientset returns a real implementation of a conciergeclientset.Interface.
|
||||
func getRealConciergeClientset(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error) {
|
||||
restConfig, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := kubeclient.New(
|
||||
kubeclient.WithConfig(restConfig),
|
||||
kubeclient.WithMiddleware(groupsuffix.New(apiGroupSuffix)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.PinnipedConcierge, nil
|
||||
}
|
||||
|
||||
// newClientConfig returns a clientcmd.ClientConfig given an optional kubeconfig path override and
|
||||
// an optional context override.
|
||||
func newClientConfig(kubeconfigPathOverride string, currentContextName string) clientcmd.ClientConfig {
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
loadingRules.ExplicitPath = kubeconfigPathOverride
|
||||
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{
|
||||
CurrentContext: currentContextName,
|
||||
})
|
||||
return clientConfig
|
||||
}
|
||||
665
cmd/pinniped/cmd/kubeconfig.go
Normal file
665
cmd/pinniped/cmd/kubeconfig.go
Normal file
@@ -0,0 +1,665 @@
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/go-logr/stdr"
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
|
||||
conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
)
|
||||
|
||||
type kubeconfigDeps struct {
|
||||
getPathToSelf func() (string, error)
|
||||
getClientset getConciergeClientsetFunc
|
||||
log logr.Logger
|
||||
}
|
||||
|
||||
func kubeconfigRealDeps() kubeconfigDeps {
|
||||
return kubeconfigDeps{
|
||||
getPathToSelf: os.Executable,
|
||||
getClientset: getRealConciergeClientset,
|
||||
log: stdr.New(log.New(os.Stderr, "", 0)),
|
||||
}
|
||||
}
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
getCmd.AddCommand(kubeconfigCommand(kubeconfigRealDeps()))
|
||||
}
|
||||
|
||||
type getKubeconfigOIDCParams struct {
|
||||
issuer string
|
||||
clientID string
|
||||
listenPort uint16
|
||||
scopes []string
|
||||
skipBrowser bool
|
||||
sessionCachePath string
|
||||
debugSessionCache bool
|
||||
caBundle caBundleFlag
|
||||
requestAudience string
|
||||
}
|
||||
|
||||
type getKubeconfigConciergeParams struct {
|
||||
disabled bool
|
||||
credentialIssuer string
|
||||
authenticatorName string
|
||||
authenticatorType string
|
||||
apiGroupSuffix string
|
||||
caBundle caBundleFlag
|
||||
endpoint string
|
||||
mode conciergeModeFlag
|
||||
skipWait bool
|
||||
}
|
||||
|
||||
type getKubeconfigParams struct {
|
||||
kubeconfigPath string
|
||||
kubeconfigContextOverride string
|
||||
skipValidate bool
|
||||
timeout time.Duration
|
||||
outputPath string
|
||||
staticToken string
|
||||
staticTokenEnvName string
|
||||
oidc getKubeconfigOIDCParams
|
||||
concierge getKubeconfigConciergeParams
|
||||
}
|
||||
|
||||
func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
|
||||
var (
|
||||
cmd = &cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
Use: "kubeconfig",
|
||||
Short: "Generate a Pinniped-based kubeconfig for a cluster",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags getKubeconfigParams
|
||||
namespace string // unused now
|
||||
)
|
||||
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&flags.staticToken, "static-token", "", "Instead of doing an OIDC-based login, specify a static token")
|
||||
f.StringVar(&flags.staticTokenEnvName, "static-token-env", "", "Instead of doing an OIDC-based login, read a static token from the environment")
|
||||
|
||||
f.BoolVar(&flags.concierge.disabled, "no-concierge", false, "Generate a configuration which does not use the Concierge, but sends the credential to the cluster directly")
|
||||
f.StringVar(&namespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the Concierge was installed")
|
||||
f.StringVar(&flags.concierge.credentialIssuer, "concierge-credential-issuer", "", "Concierge CredentialIssuer object to use for autodiscovery (default: autodiscover)")
|
||||
f.StringVar(&flags.concierge.authenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover)")
|
||||
f.StringVar(&flags.concierge.authenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name (default: autodiscover)")
|
||||
f.StringVar(&flags.concierge.apiGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||
f.BoolVar(&flags.concierge.skipWait, "concierge-skip-wait", false, "Skip waiting for any pending Concierge strategies to become ready (default: false)")
|
||||
|
||||
f.Var(&flags.concierge.caBundle, "concierge-ca-bundle", "Path to TLS certificate authority bundle (PEM format, optional, can be repeated) to use when connecting to the Concierge")
|
||||
f.StringVar(&flags.concierge.endpoint, "concierge-endpoint", "", "API base for the Concierge endpoint")
|
||||
f.Var(&flags.concierge.mode, "concierge-mode", "Concierge mode of operation")
|
||||
|
||||
f.StringVar(&flags.oidc.issuer, "oidc-issuer", "", "OpenID Connect issuer URL (default: autodiscover)")
|
||||
f.StringVar(&flags.oidc.clientID, "oidc-client-id", "pinniped-cli", "OpenID Connect client ID (default: autodiscover)")
|
||||
f.Uint16Var(&flags.oidc.listenPort, "oidc-listen-port", 0, "TCP port for localhost listener (authorization code flow only)")
|
||||
f.StringSliceVar(&flags.oidc.scopes, "oidc-scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "OpenID Connect scopes to request during login")
|
||||
f.BoolVar(&flags.oidc.skipBrowser, "oidc-skip-browser", false, "During OpenID Connect login, skip opening the browser (just print the URL)")
|
||||
f.StringVar(&flags.oidc.sessionCachePath, "oidc-session-cache", "", "Path to OpenID Connect session cache file")
|
||||
f.Var(&flags.oidc.caBundle, "oidc-ca-bundle", "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)")
|
||||
f.BoolVar(&flags.oidc.debugSessionCache, "oidc-debug-session-cache", false, "Print debug logs related to the OpenID Connect session cache")
|
||||
f.StringVar(&flags.oidc.requestAudience, "oidc-request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange")
|
||||
f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file")
|
||||
f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)")
|
||||
f.BoolVar(&flags.skipValidate, "skip-validation", false, "Skip final validation of the kubeconfig (default: false)")
|
||||
f.DurationVar(&flags.timeout, "timeout", 10*time.Minute, "Timeout for autodiscovery and validation")
|
||||
f.StringVarP(&flags.outputPath, "output", "o", "", "Output file path (default: stdout)")
|
||||
|
||||
mustMarkHidden(cmd, "oidc-debug-session-cache")
|
||||
|
||||
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
||||
mustMarkHidden(cmd, "concierge-namespace")
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
if flags.outputPath != "" {
|
||||
out, err := os.Create(flags.outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open output file: %w", err)
|
||||
}
|
||||
defer func() { _ = out.Close() }()
|
||||
cmd.SetOut(out)
|
||||
}
|
||||
return runGetKubeconfig(cmd.Context(), cmd.OutOrStdout(), deps, flags)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, flags getKubeconfigParams) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, flags.timeout)
|
||||
defer cancel()
|
||||
|
||||
// Validate api group suffix and immediately return an error if it is invalid.
|
||||
if err := groupsuffix.Validate(flags.concierge.apiGroupSuffix); err != nil {
|
||||
return fmt.Errorf("invalid API group suffix: %w", err)
|
||||
}
|
||||
|
||||
execConfig := clientcmdapi.ExecConfig{
|
||||
APIVersion: clientauthenticationv1beta1.SchemeGroupVersion.String(),
|
||||
Args: []string{},
|
||||
Env: []clientcmdapi.ExecEnvVar{},
|
||||
}
|
||||
|
||||
var err error
|
||||
execConfig.Command, err = deps.getPathToSelf()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine the Pinniped executable path: %w", err)
|
||||
}
|
||||
execConfig.ProvideClusterInfo = true
|
||||
|
||||
clientConfig := newClientConfig(flags.kubeconfigPath, flags.kubeconfigContextOverride)
|
||||
currentKubeConfig, err := clientConfig.RawConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load --kubeconfig: %w", err)
|
||||
}
|
||||
cluster, err := copyCurrentClusterFromExistingKubeConfig(currentKubeConfig, flags.kubeconfigContextOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load --kubeconfig/--kubeconfig-context: %w", err)
|
||||
}
|
||||
clientset, err := deps.getClientset(clientConfig, flags.concierge.apiGroupSuffix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not configure Kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
if !flags.concierge.disabled {
|
||||
credentialIssuer, err := waitForCredentialIssuer(ctx, clientset, flags, deps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authenticator, err := lookupAuthenticator(
|
||||
clientset,
|
||||
flags.concierge.authenticatorType,
|
||||
flags.concierge.authenticatorName,
|
||||
deps.log,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := discoverConciergeParams(credentialIssuer, &flags, cluster, deps.log); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := discoverAuthenticatorParams(authenticator, &flags, deps.log); err != nil {
|
||||
return err
|
||||
}
|
||||
// Append the flags to configure the Concierge credential exchange at runtime.
|
||||
execConfig.Args = append(execConfig.Args,
|
||||
"--enable-concierge",
|
||||
"--concierge-api-group-suffix="+flags.concierge.apiGroupSuffix,
|
||||
"--concierge-authenticator-name="+flags.concierge.authenticatorName,
|
||||
"--concierge-authenticator-type="+flags.concierge.authenticatorType,
|
||||
"--concierge-endpoint="+flags.concierge.endpoint,
|
||||
"--concierge-ca-bundle-data="+base64.StdEncoding.EncodeToString(flags.concierge.caBundle),
|
||||
)
|
||||
|
||||
// Point kubectl at the concierge endpoint.
|
||||
cluster.Server = flags.concierge.endpoint
|
||||
cluster.CertificateAuthorityData = flags.concierge.caBundle
|
||||
}
|
||||
|
||||
// If one of the --static-* flags was passed, output a config that runs `pinniped login static`.
|
||||
if flags.staticToken != "" || flags.staticTokenEnvName != "" {
|
||||
if flags.staticToken != "" && flags.staticTokenEnvName != "" {
|
||||
return fmt.Errorf("only one of --static-token and --static-token-env can be specified")
|
||||
}
|
||||
execConfig.Args = append([]string{"login", "static"}, execConfig.Args...)
|
||||
if flags.staticToken != "" {
|
||||
execConfig.Args = append(execConfig.Args, "--token="+flags.staticToken)
|
||||
}
|
||||
if flags.staticTokenEnvName != "" {
|
||||
execConfig.Args = append(execConfig.Args, "--token-env="+flags.staticTokenEnvName)
|
||||
}
|
||||
|
||||
kubeconfig := newExecKubeconfig(cluster, &execConfig)
|
||||
if err := validateKubeconfig(ctx, flags, kubeconfig, deps.log); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeConfigAsYAML(out, kubeconfig)
|
||||
}
|
||||
|
||||
// Otherwise continue to parse the OIDC-related flags and output a config that runs `pinniped login oidc`.
|
||||
execConfig.Args = append([]string{"login", "oidc"}, execConfig.Args...)
|
||||
if flags.oidc.issuer == "" {
|
||||
return fmt.Errorf("could not autodiscover --oidc-issuer and none was provided")
|
||||
}
|
||||
execConfig.Args = append(execConfig.Args,
|
||||
"--issuer="+flags.oidc.issuer,
|
||||
"--client-id="+flags.oidc.clientID,
|
||||
"--scopes="+strings.Join(flags.oidc.scopes, ","),
|
||||
)
|
||||
if flags.oidc.skipBrowser {
|
||||
execConfig.Args = append(execConfig.Args, "--skip-browser")
|
||||
}
|
||||
if flags.oidc.listenPort != 0 {
|
||||
execConfig.Args = append(execConfig.Args, "--listen-port="+strconv.Itoa(int(flags.oidc.listenPort)))
|
||||
}
|
||||
if len(flags.oidc.caBundle) != 0 {
|
||||
execConfig.Args = append(execConfig.Args, "--ca-bundle-data="+base64.StdEncoding.EncodeToString(flags.oidc.caBundle))
|
||||
}
|
||||
if flags.oidc.sessionCachePath != "" {
|
||||
execConfig.Args = append(execConfig.Args, "--session-cache="+flags.oidc.sessionCachePath)
|
||||
}
|
||||
if flags.oidc.debugSessionCache {
|
||||
execConfig.Args = append(execConfig.Args, "--debug-session-cache")
|
||||
}
|
||||
if flags.oidc.requestAudience != "" {
|
||||
execConfig.Args = append(execConfig.Args, "--request-audience="+flags.oidc.requestAudience)
|
||||
}
|
||||
kubeconfig := newExecKubeconfig(cluster, &execConfig)
|
||||
if err := validateKubeconfig(ctx, flags, kubeconfig, deps.log); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeConfigAsYAML(out, kubeconfig)
|
||||
}
|
||||
|
||||
func waitForCredentialIssuer(ctx context.Context, clientset conciergeclientset.Interface, flags getKubeconfigParams, deps kubeconfigDeps) (*configv1alpha1.CredentialIssuer, error) {
|
||||
credentialIssuer, err := lookupCredentialIssuer(clientset, flags.concierge.credentialIssuer, deps.log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !flags.concierge.skipWait {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
deadline, _ := ctx.Deadline()
|
||||
attempts := 1
|
||||
|
||||
for {
|
||||
if !hasPendingStrategy(credentialIssuer) {
|
||||
break
|
||||
}
|
||||
logStrategies(credentialIssuer, deps.log)
|
||||
deps.log.Info("waiting for CredentialIssuer pending strategies to finish",
|
||||
"attempts", attempts,
|
||||
"remaining", time.Until(deadline).Round(time.Second).String(),
|
||||
)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-ticker.C:
|
||||
credentialIssuer, err = lookupCredentialIssuer(clientset, flags.concierge.credentialIssuer, deps.log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return credentialIssuer, nil
|
||||
}
|
||||
|
||||
func discoverConciergeParams(credentialIssuer *configv1alpha1.CredentialIssuer, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, log logr.Logger) error {
|
||||
// Autodiscover the --concierge-mode.
|
||||
frontend, err := getConciergeFrontend(credentialIssuer, flags.concierge.mode)
|
||||
if err != nil {
|
||||
logStrategies(credentialIssuer, log)
|
||||
return err
|
||||
}
|
||||
|
||||
// Auto-set --concierge-mode if it wasn't explicitly set.
|
||||
if flags.concierge.mode == modeUnknown {
|
||||
switch frontend.Type {
|
||||
case configv1alpha1.TokenCredentialRequestAPIFrontendType:
|
||||
log.Info("discovered Concierge operating in TokenCredentialRequest API mode")
|
||||
flags.concierge.mode = modeTokenCredentialRequestAPI
|
||||
case configv1alpha1.ImpersonationProxyFrontendType:
|
||||
log.Info("discovered Concierge operating in impersonation proxy mode")
|
||||
flags.concierge.mode = modeImpersonationProxy
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-set --concierge-endpoint if it wasn't explicitly set.
|
||||
if flags.concierge.endpoint == "" {
|
||||
switch frontend.Type {
|
||||
case configv1alpha1.TokenCredentialRequestAPIFrontendType:
|
||||
flags.concierge.endpoint = v1Cluster.Server
|
||||
case configv1alpha1.ImpersonationProxyFrontendType:
|
||||
flags.concierge.endpoint = frontend.ImpersonationProxyInfo.Endpoint
|
||||
}
|
||||
log.Info("discovered Concierge endpoint", "endpoint", flags.concierge.endpoint)
|
||||
}
|
||||
|
||||
// Auto-set --concierge-ca-bundle if it wasn't explicitly set..
|
||||
if len(flags.concierge.caBundle) == 0 {
|
||||
switch frontend.Type {
|
||||
case configv1alpha1.TokenCredentialRequestAPIFrontendType:
|
||||
flags.concierge.caBundle = v1Cluster.CertificateAuthorityData
|
||||
case configv1alpha1.ImpersonationProxyFrontendType:
|
||||
data, err := base64.StdEncoding.DecodeString(frontend.ImpersonationProxyInfo.CertificateAuthorityData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("autodiscovered Concierge CA bundle is invalid: %w", err)
|
||||
}
|
||||
flags.concierge.caBundle = data
|
||||
}
|
||||
log.Info("discovered Concierge certificate authority bundle", "roots", countCACerts(flags.concierge.caBundle))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log logr.Logger) {
|
||||
for _, strategy := range credentialIssuer.Status.Strategies {
|
||||
log.Info("found CredentialIssuer strategy",
|
||||
"type", strategy.Type,
|
||||
"status", strategy.Status,
|
||||
"reason", strategy.Reason,
|
||||
"message", strategy.Message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconfigParams, log logr.Logger) error {
|
||||
switch auth := authenticator.(type) {
|
||||
case *conciergev1alpha1.WebhookAuthenticator:
|
||||
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
|
||||
// them to point at the discovered WebhookAuthenticator.
|
||||
if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" {
|
||||
log.Info("discovered WebhookAuthenticator", "name", auth.Name)
|
||||
flags.concierge.authenticatorType = "webhook"
|
||||
flags.concierge.authenticatorName = auth.Name
|
||||
}
|
||||
case *conciergev1alpha1.JWTAuthenticator:
|
||||
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
|
||||
// them to point at the discovered JWTAuthenticator.
|
||||
if flags.concierge.authenticatorType == "" && flags.concierge.authenticatorName == "" {
|
||||
log.Info("discovered JWTAuthenticator", "name", auth.Name)
|
||||
flags.concierge.authenticatorType = "jwt"
|
||||
flags.concierge.authenticatorName = auth.Name
|
||||
}
|
||||
|
||||
// If the --oidc-issuer flag was not set explicitly, default it to the spec.issuer field of the JWTAuthenticator.
|
||||
if flags.oidc.issuer == "" {
|
||||
log.Info("discovered OIDC issuer", "issuer", auth.Spec.Issuer)
|
||||
flags.oidc.issuer = auth.Spec.Issuer
|
||||
}
|
||||
|
||||
// If the --oidc-request-audience flag was not set explicitly, default it to the spec.audience field of the JWTAuthenticator.
|
||||
if flags.oidc.requestAudience == "" {
|
||||
log.Info("discovered OIDC audience", "audience", auth.Spec.Audience)
|
||||
flags.oidc.requestAudience = auth.Spec.Audience
|
||||
}
|
||||
|
||||
// If the --oidc-ca-bundle flags was not set explicitly, default it to the
|
||||
// spec.tls.certificateAuthorityData field of the JWTAuthenticator.
|
||||
if len(flags.oidc.caBundle) == 0 && auth.Spec.TLS != nil && auth.Spec.TLS.CertificateAuthorityData != "" {
|
||||
decoded, err := base64.StdEncoding.DecodeString(auth.Spec.TLS.CertificateAuthorityData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator %s has invalid spec.tls.certificateAuthorityData: %w", auth.Name, err)
|
||||
}
|
||||
log.Info("discovered OIDC CA bundle", "roots", countCACerts(decoded))
|
||||
flags.oidc.caBundle = decoded
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConciergeFrontend(credentialIssuer *configv1alpha1.CredentialIssuer, mode conciergeModeFlag) (*configv1alpha1.CredentialIssuerFrontend, error) {
|
||||
for _, strategy := range credentialIssuer.Status.Strategies {
|
||||
// Skip unhealthy strategies.
|
||||
if strategy.Status != configv1alpha1.SuccessStrategyStatus {
|
||||
continue
|
||||
}
|
||||
|
||||
// Backfill the .status.strategies[].frontend field from .status.kubeConfigInfo for backwards compatibility.
|
||||
if strategy.Type == configv1alpha1.KubeClusterSigningCertificateStrategyType && strategy.Frontend == nil && credentialIssuer.Status.KubeConfigInfo != nil {
|
||||
strategy = *strategy.DeepCopy()
|
||||
strategy.Frontend = &configv1alpha1.CredentialIssuerFrontend{
|
||||
Type: configv1alpha1.TokenCredentialRequestAPIFrontendType,
|
||||
TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{
|
||||
Server: credentialIssuer.Status.KubeConfigInfo.Server,
|
||||
CertificateAuthorityData: credentialIssuer.Status.KubeConfigInfo.CertificateAuthorityData,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// If the strategy frontend is still nil, skip.
|
||||
if strategy.Frontend == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip any unknown frontend types.
|
||||
switch strategy.Frontend.Type {
|
||||
case configv1alpha1.TokenCredentialRequestAPIFrontendType, configv1alpha1.ImpersonationProxyFrontendType:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
// Skip strategies that don't match --concierge-mode.
|
||||
if !mode.MatchesFrontend(strategy.Frontend) {
|
||||
continue
|
||||
}
|
||||
return strategy.Frontend, nil
|
||||
}
|
||||
|
||||
if mode == modeUnknown {
|
||||
return nil, fmt.Errorf("could not autodiscover --concierge-mode")
|
||||
}
|
||||
return nil, fmt.Errorf("could not find successful Concierge strategy matching --concierge-mode=%s", mode.String())
|
||||
}
|
||||
|
||||
func newExecKubeconfig(cluster *clientcmdapi.Cluster, execConfig *clientcmdapi.ExecConfig) clientcmdapi.Config {
|
||||
const name = "pinniped"
|
||||
return clientcmdapi.Config{
|
||||
Kind: "Config",
|
||||
APIVersion: clientcmdapi.SchemeGroupVersion.Version,
|
||||
Clusters: map[string]*clientcmdapi.Cluster{name: cluster},
|
||||
AuthInfos: map[string]*clientcmdapi.AuthInfo{name: {Exec: execConfig}},
|
||||
Contexts: map[string]*clientcmdapi.Context{name: {Cluster: name, AuthInfo: name}},
|
||||
CurrentContext: name,
|
||||
}
|
||||
}
|
||||
|
||||
func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, log logr.Logger) (*configv1alpha1.CredentialIssuer, error) {
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
||||
defer cancelFunc()
|
||||
|
||||
// If the name is specified, get that object.
|
||||
if name != "" {
|
||||
return clientset.ConfigV1alpha1().CredentialIssuers().Get(ctx, name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
// Otherwise list all the available CredentialIssuers and hope there's just a single one
|
||||
results, err := clientset.ConfigV1alpha1().CredentialIssuers().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list CredentialIssuer objects for autodiscovery: %w", err)
|
||||
}
|
||||
if len(results.Items) == 0 {
|
||||
return nil, fmt.Errorf("no CredentialIssuers were found")
|
||||
}
|
||||
if len(results.Items) > 1 {
|
||||
return nil, fmt.Errorf("multiple CredentialIssuers were found, so the --concierge-credential-issuer flag must be specified")
|
||||
}
|
||||
|
||||
result := &results.Items[0]
|
||||
log.Info("discovered CredentialIssuer", "name", result.Name)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string, log logr.Logger) (metav1.Object, error) {
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
||||
defer cancelFunc()
|
||||
|
||||
// If one was specified, look it up or error.
|
||||
if authName != "" && authType != "" {
|
||||
switch strings.ToLower(authType) {
|
||||
case "webhook":
|
||||
return clientset.AuthenticationV1alpha1().WebhookAuthenticators().Get(ctx, authName, metav1.GetOptions{})
|
||||
case "jwt":
|
||||
return clientset.AuthenticationV1alpha1().JWTAuthenticators().Get(ctx, authName, metav1.GetOptions{})
|
||||
default:
|
||||
return nil, fmt.Errorf(`invalid authenticator type %q, supported values are "webhook" and "jwt"`, authType)
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise list all the available authenticators and hope there's just a single one.
|
||||
|
||||
jwtAuths, err := clientset.AuthenticationV1alpha1().JWTAuthenticators().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list JWTAuthenticator objects for autodiscovery: %w", err)
|
||||
}
|
||||
webhooks, err := clientset.AuthenticationV1alpha1().WebhookAuthenticators().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list WebhookAuthenticator objects for autodiscovery: %w", err)
|
||||
}
|
||||
|
||||
results := make([]metav1.Object, 0, len(jwtAuths.Items)+len(webhooks.Items))
|
||||
for i := range jwtAuths.Items {
|
||||
results = append(results, &jwtAuths.Items[i])
|
||||
}
|
||||
for i := range webhooks.Items {
|
||||
results = append(results, &webhooks.Items[i])
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return nil, fmt.Errorf("no authenticators were found")
|
||||
}
|
||||
if len(results) > 1 {
|
||||
for _, jwtAuth := range jwtAuths.Items {
|
||||
log.Info("found JWTAuthenticator", "name", jwtAuth.Name)
|
||||
}
|
||||
for _, webhook := range webhooks.Items {
|
||||
log.Info("found WebhookAuthenticator", "name", webhook.Name)
|
||||
}
|
||||
return nil, fmt.Errorf("multiple authenticators were found, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified")
|
||||
}
|
||||
return results[0], nil
|
||||
}
|
||||
|
||||
func writeConfigAsYAML(out io.Writer, config clientcmdapi.Config) error {
|
||||
output, err := clientcmd.Write(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = out.Write(output)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write output: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyCurrentClusterFromExistingKubeConfig(currentKubeConfig clientcmdapi.Config, currentContextNameOverride string) (*clientcmdapi.Cluster, error) {
|
||||
contextName := currentKubeConfig.CurrentContext
|
||||
if currentContextNameOverride != "" {
|
||||
contextName = currentContextNameOverride
|
||||
}
|
||||
ctx := currentKubeConfig.Contexts[contextName]
|
||||
if ctx == nil {
|
||||
return nil, fmt.Errorf("no such context %q", contextName)
|
||||
}
|
||||
return currentKubeConfig.Clusters[ctx.Cluster], nil
|
||||
}
|
||||
|
||||
func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconfig clientcmdapi.Config, log logr.Logger) error {
|
||||
if flags.skipValidate {
|
||||
return nil
|
||||
}
|
||||
|
||||
kubeContext := kubeconfig.Contexts[kubeconfig.CurrentContext]
|
||||
if kubeContext == nil {
|
||||
return fmt.Errorf("invalid kubeconfig (no context)")
|
||||
}
|
||||
cluster := kubeconfig.Clusters[kubeContext.Cluster]
|
||||
if cluster == nil {
|
||||
return fmt.Errorf("invalid kubeconfig (no cluster)")
|
||||
}
|
||||
|
||||
kubeconfigCA := x509.NewCertPool()
|
||||
if !kubeconfigCA.AppendCertsFromPEM(cluster.CertificateAuthorityData) {
|
||||
return fmt.Errorf("invalid kubeconfig (no certificateAuthorityData)")
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
RootCAs: kubeconfigCA,
|
||||
},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
},
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
pingCluster := func() error {
|
||||
reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, cluster.Server, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not form request to validate cluster: %w", err)
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
if resp.StatusCode >= 500 {
|
||||
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err := pingCluster()
|
||||
if err == nil {
|
||||
log.Info("validated connection to the cluster")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("could not immediately connect to the cluster but it may be initializing, will retry until timeout")
|
||||
deadline, _ := ctx.Deadline()
|
||||
attempts := 0
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
attempts++
|
||||
err := pingCluster()
|
||||
if err == nil {
|
||||
log.Info("validated connection to the cluster", "attempts", attempts)
|
||||
return nil
|
||||
}
|
||||
log.Error(err, "could not connect to cluster, retrying...", "attempts", attempts, "remaining", time.Until(deadline).Round(time.Second).String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func countCACerts(pemData []byte) int {
|
||||
pool := x509.NewCertPool()
|
||||
pool.AppendCertsFromPEM(pemData)
|
||||
return len(pool.Subjects())
|
||||
}
|
||||
|
||||
func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool {
|
||||
for _, strategy := range credentialIssuer.Status.Strategies {
|
||||
if strategy.Reason == configv1alpha1.PendingStrategyReason {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
1117
cmd/pinniped/cmd/kubeconfig_test.go
Normal file
1117
cmd/pinniped/cmd/kubeconfig_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,8 @@ var loginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "login",
|
||||
Long: "Login to a Pinniped server",
|
||||
SilenceUsage: true, // do not print usage message when commands fail
|
||||
SilenceUsage: true, // Do not print usage message when commands fail.
|
||||
Hidden: true, // These commands are not really meant to be used directly by users, so it's confusing to have them discoverable.
|
||||
}
|
||||
|
||||
//nolint: gochecknoinits
|
||||
|
||||
@@ -1,103 +1,230 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
"k8s.io/klog/v2/klogr"
|
||||
|
||||
"go.pinniped.dev/internal/oidcclient"
|
||||
"go.pinniped.dev/internal/oidcclient/filesession"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
"go.pinniped.dev/pkg/oidcclient"
|
||||
"go.pinniped.dev/pkg/oidcclient/filesession"
|
||||
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||
)
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
loginCmd.AddCommand(oidcLoginCommand(oidcclient.Login))
|
||||
loginCmd.AddCommand(oidcLoginCommand(oidcLoginCommandRealDeps()))
|
||||
}
|
||||
|
||||
func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oidcclient.Option) (*oidcclient.Token, error)) *cobra.Command {
|
||||
type oidcLoginCommandDeps struct {
|
||||
login func(string, string, ...oidcclient.Option) (*oidctypes.Token, error)
|
||||
exchangeToken func(context.Context, *conciergeclient.Client, string) (*clientauthv1beta1.ExecCredential, error)
|
||||
}
|
||||
|
||||
func oidcLoginCommandRealDeps() oidcLoginCommandDeps {
|
||||
return oidcLoginCommandDeps{
|
||||
login: oidcclient.Login,
|
||||
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
|
||||
return client.ExchangeToken(ctx, token)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type oidcLoginFlags struct {
|
||||
issuer string
|
||||
clientID string
|
||||
listenPort uint16
|
||||
scopes []string
|
||||
skipBrowser bool
|
||||
sessionCachePath string
|
||||
caBundlePaths []string
|
||||
caBundleData []string
|
||||
debugSessionCache bool
|
||||
requestAudience string
|
||||
conciergeEnabled bool
|
||||
conciergeAuthenticatorType string
|
||||
conciergeAuthenticatorName string
|
||||
conciergeEndpoint string
|
||||
conciergeCABundle string
|
||||
conciergeAPIGroupSuffix string
|
||||
}
|
||||
|
||||
func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
|
||||
var (
|
||||
cmd = cobra.Command{
|
||||
cmd = &cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
Use: "oidc --issuer ISSUER --client-id CLIENT_ID",
|
||||
Use: "oidc --issuer ISSUER",
|
||||
Short: "Login using an OpenID Connect provider",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
issuer string
|
||||
clientID string
|
||||
listenPort uint16
|
||||
scopes []string
|
||||
skipBrowser bool
|
||||
sessionCachePath string
|
||||
debugSessionCache bool
|
||||
flags oidcLoginFlags
|
||||
conciergeNamespace string // unused now
|
||||
)
|
||||
cmd.Flags().StringVar(&issuer, "issuer", "", "OpenID Connect issuer URL.")
|
||||
cmd.Flags().StringVar(&clientID, "client-id", "", "OpenID Connect client ID.")
|
||||
cmd.Flags().Uint16Var(&listenPort, "listen-port", 0, "TCP port for localhost listener (authorization code flow only).")
|
||||
cmd.Flags().StringSliceVar(&scopes, "scopes", []string{"offline_access", "openid", "email", "profile"}, "OIDC scopes to request during login.")
|
||||
cmd.Flags().BoolVar(&skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL).")
|
||||
cmd.Flags().StringVar(&sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file.")
|
||||
cmd.Flags().BoolVar(&debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache.")
|
||||
mustMarkHidden(&cmd, "debug-session-cache")
|
||||
mustMarkRequired(&cmd, "issuer", "client-id")
|
||||
cmd.Flags().StringVar(&flags.issuer, "issuer", "", "OpenID Connect issuer URL")
|
||||
cmd.Flags().StringVar(&flags.clientID, "client-id", "pinniped-cli", "OpenID Connect client ID")
|
||||
cmd.Flags().Uint16Var(&flags.listenPort, "listen-port", 0, "TCP port for localhost listener (authorization code flow only)")
|
||||
cmd.Flags().StringSliceVar(&flags.scopes, "scopes", []string{oidc.ScopeOfflineAccess, oidc.ScopeOpenID, "pinniped:request-audience"}, "OIDC scopes to request during login")
|
||||
cmd.Flags().BoolVar(&flags.skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL)")
|
||||
cmd.Flags().StringVar(&flags.sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file")
|
||||
cmd.Flags().StringSliceVar(&flags.caBundlePaths, "ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)")
|
||||
cmd.Flags().StringSliceVar(&flags.caBundleData, "ca-bundle-data", nil, "Base64 encoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)")
|
||||
cmd.Flags().BoolVar(&flags.debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache")
|
||||
cmd.Flags().StringVar(&flags.requestAudience, "request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange")
|
||||
cmd.Flags().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Use the Concierge to login")
|
||||
cmd.Flags().StringVar(&conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the Concierge was installed")
|
||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt')")
|
||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name")
|
||||
cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the Concierge")
|
||||
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
// Initialize the session cache.
|
||||
var sessionOptions []filesession.Option
|
||||
mustMarkHidden(cmd, "debug-session-cache")
|
||||
mustMarkRequired(cmd, "issuer")
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runOIDCLogin(cmd, deps, flags) }
|
||||
|
||||
// If the hidden --debug-session-cache option is passed, log all the errors from the session cache with klog.
|
||||
if debugSessionCache {
|
||||
logger := klogr.New().WithName("session")
|
||||
sessionOptions = append(sessionOptions, filesession.WithErrorReporter(func(err error) {
|
||||
logger.Error(err, "error during session cache operation")
|
||||
}))
|
||||
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
||||
mustMarkHidden(cmd, "concierge-namespace")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error {
|
||||
// Initialize the session cache.
|
||||
var sessionOptions []filesession.Option
|
||||
|
||||
// If the hidden --debug-session-cache option is passed, log all the errors from the session cache with klog.
|
||||
if flags.debugSessionCache {
|
||||
logger := klogr.New().WithName("session")
|
||||
sessionOptions = append(sessionOptions, filesession.WithErrorReporter(func(err error) {
|
||||
logger.Error(err, "error during session cache operation")
|
||||
}))
|
||||
}
|
||||
sessionCache := filesession.New(flags.sessionCachePath, sessionOptions...)
|
||||
|
||||
// Initialize the login handler.
|
||||
opts := []oidcclient.Option{
|
||||
oidcclient.WithContext(cmd.Context()),
|
||||
oidcclient.WithScopes(flags.scopes),
|
||||
oidcclient.WithSessionCache(sessionCache),
|
||||
}
|
||||
|
||||
if flags.listenPort != 0 {
|
||||
opts = append(opts, oidcclient.WithListenPort(flags.listenPort))
|
||||
}
|
||||
|
||||
if flags.requestAudience != "" {
|
||||
opts = append(opts, oidcclient.WithRequestAudience(flags.requestAudience))
|
||||
}
|
||||
|
||||
var concierge *conciergeclient.Client
|
||||
if flags.conciergeEnabled {
|
||||
var err error
|
||||
concierge, err = conciergeclient.New(
|
||||
conciergeclient.WithEndpoint(flags.conciergeEndpoint),
|
||||
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
|
||||
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
|
||||
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid Concierge parameters: %w", err)
|
||||
}
|
||||
sessionCache := filesession.New(sessionCachePath, sessionOptions...)
|
||||
}
|
||||
|
||||
// Initialize the login handler.
|
||||
opts := []oidcclient.Option{
|
||||
oidcclient.WithContext(cmd.Context()),
|
||||
oidcclient.WithScopes(scopes),
|
||||
oidcclient.WithSessionCache(sessionCache),
|
||||
}
|
||||
// --skip-browser replaces the default "browser open" function with one that prints to stderr.
|
||||
if flags.skipBrowser {
|
||||
opts = append(opts, oidcclient.WithBrowserOpen(func(url string) error {
|
||||
cmd.PrintErr("Please log in: ", url, "\n")
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
if listenPort != 0 {
|
||||
opts = append(opts, oidcclient.WithListenPort(listenPort))
|
||||
}
|
||||
|
||||
// --skip-browser replaces the default "browser open" function with one that prints to stderr.
|
||||
if skipBrowser {
|
||||
opts = append(opts, oidcclient.WithBrowserOpen(func(url string) error {
|
||||
cmd.PrintErr("Please log in: ", url, "\n")
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
tok, err := loginFunc(issuer, clientID, opts...)
|
||||
if len(flags.caBundlePaths) > 0 || len(flags.caBundleData) > 0 {
|
||||
client, err := makeClient(flags.caBundlePaths, flags.caBundleData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert the token out to Kubernetes ExecCredential JSON format for output.
|
||||
return json.NewEncoder(cmd.OutOrStdout()).Encode(&clientauthenticationv1beta1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ExecCredential",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||
ExpirationTimestamp: &tok.IDToken.Expiry,
|
||||
Token: tok.IDToken.Token,
|
||||
},
|
||||
})
|
||||
opts = append(opts, oidcclient.WithClient(client))
|
||||
}
|
||||
return &cmd
|
||||
|
||||
// Do the basic login to get an OIDC token.
|
||||
token, err := deps.login(flags.issuer, flags.clientID, opts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not complete Pinniped login: %w", err)
|
||||
}
|
||||
cred := tokenCredential(token)
|
||||
|
||||
// If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential.
|
||||
if concierge != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cred, err = deps.exchangeToken(ctx, concierge, token.IDToken.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not complete Concierge credential exchange: %w", err)
|
||||
}
|
||||
}
|
||||
return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
|
||||
}
|
||||
|
||||
func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) {
|
||||
pool := x509.NewCertPool()
|
||||
for _, p := range caBundlePaths {
|
||||
pem, err := ioutil.ReadFile(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read --ca-bundle: %w", err)
|
||||
}
|
||||
pool.AppendCertsFromPEM(pem)
|
||||
}
|
||||
for _, d := range caBundleData {
|
||||
pem, err := base64.StdEncoding.DecodeString(d)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read --ca-bundle-data: %w", err)
|
||||
}
|
||||
pool.AppendCertsFromPEM(pem)
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: pool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func tokenCredential(token *oidctypes.Token) *clientauthv1beta1.ExecCredential {
|
||||
cred := clientauthv1beta1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ExecCredential",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Status: &clientauthv1beta1.ExecCredentialStatus{
|
||||
Token: token.IDToken.Token,
|
||||
},
|
||||
}
|
||||
if !token.IDToken.Expiry.IsZero() {
|
||||
cred.Status.ExpirationTimestamp = &token.IDToken.Expiry
|
||||
}
|
||||
return &cred
|
||||
}
|
||||
|
||||
// mustGetConfigDir returns a directory that follows the XDG base directory convention:
|
||||
|
||||
@@ -1,35 +1,49 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/oidcclient"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
"go.pinniped.dev/pkg/oidcclient"
|
||||
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||
)
|
||||
|
||||
func TestLoginOIDCCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfgDir := mustGetConfigDir()
|
||||
|
||||
testCA, err := certauthority.New("Test CA", 1*time.Hour)
|
||||
require.NoError(t, err)
|
||||
tmpdir := testutil.TempDir(t)
|
||||
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
||||
require.NoError(t, ioutil.WriteFile(testCABundlePath, testCA.Bundle(), 0600))
|
||||
|
||||
time1 := time.Date(3020, 10, 12, 13, 14, 15, 16, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
loginErr error
|
||||
conciergeErr error
|
||||
wantError bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantIssuer string
|
||||
wantClientID string
|
||||
wantOptionsCount int
|
||||
}{
|
||||
{
|
||||
@@ -39,24 +53,114 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
Login using an OpenID Connect provider
|
||||
|
||||
Usage:
|
||||
oidc --issuer ISSUER --client-id CLIENT_ID [flags]
|
||||
oidc --issuer ISSUER [flags]
|
||||
|
||||
Flags:
|
||||
--client-id string OpenID Connect client ID.
|
||||
-h, --help help for oidc
|
||||
--issuer string OpenID Connect issuer URL.
|
||||
--listen-port uint16 TCP port for localhost listener (authorization code flow only).
|
||||
--scopes strings OIDC scopes to request during login. (default [offline_access,openid,email,profile])
|
||||
--session-cache string Path to session cache file. (default "` + cfgDir + `/sessions.yaml")
|
||||
--skip-browser Skip opening the browser (just print the URL).
|
||||
--ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated)
|
||||
--ca-bundle-data strings Base64 encoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)
|
||||
--client-id string OpenID Connect client ID (default "pinniped-cli")
|
||||
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
||||
--concierge-authenticator-name string Concierge authenticator name
|
||||
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt')
|
||||
--concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge
|
||||
--concierge-endpoint string API base for the Concierge endpoint
|
||||
--enable-concierge Use the Concierge to login
|
||||
-h, --help help for oidc
|
||||
--issuer string OpenID Connect issuer URL
|
||||
--listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
||||
--request-audience string Request a token with an alternate audience using RFC8693 token exchange
|
||||
--scopes strings OIDC scopes to request during login (default [offline_access,openid,pinniped:request-audience])
|
||||
--session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml")
|
||||
--skip-browser Skip opening the browser (just print the URL)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "missing required flags",
|
||||
args: []string{},
|
||||
wantError: true,
|
||||
wantStdout: here.Doc(`
|
||||
Error: required flag(s) "client-id", "issuer" not set
|
||||
wantStderr: here.Doc(`
|
||||
Error: required flag(s) "issuer" not set
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "missing concierge flags",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--enable-concierge",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: endpoint must not be empty
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid CA bundle path",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--ca-bundle", "./does/not/exist",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not read --ca-bundle: open ./does/not/exist: no such file or directory
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid CA bundle data",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--ca-bundle-data", "invalid-base64",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not read --ca-bundle-data: illegal base64 data at input byte 7
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid API group suffix",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--enable-concierge",
|
||||
"--concierge-api-group-suffix", ".starts.with.dot",
|
||||
"--concierge-authenticator-type", "jwt",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
"--concierge-endpoint", "https://127.0.0.1:1234/",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: invalid API group suffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "login error",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
},
|
||||
loginErr: fmt.Errorf("some login error"),
|
||||
wantOptionsCount: 3,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not complete Pinniped login: some login error
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "concierge token exchange error",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--enable-concierge",
|
||||
"--concierge-authenticator-type", "jwt",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
"--concierge-endpoint", "https://127.0.0.1:1234/",
|
||||
},
|
||||
conciergeErr: fmt.Errorf("some concierge error"),
|
||||
wantOptionsCount: 3,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not complete Concierge credential exchange: some concierge error
|
||||
`),
|
||||
},
|
||||
{
|
||||
@@ -65,8 +169,6 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
},
|
||||
wantIssuer: "test-issuer",
|
||||
wantClientID: "test-client-id",
|
||||
wantOptionsCount: 3,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
@@ -78,32 +180,56 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--skip-browser",
|
||||
"--listen-port", "1234",
|
||||
"--debug-session-cache",
|
||||
"--request-audience", "cluster-1234",
|
||||
"--ca-bundle-data", base64.StdEncoding.EncodeToString(testCA.Bundle()),
|
||||
"--ca-bundle", testCABundlePath,
|
||||
"--enable-concierge",
|
||||
"--concierge-authenticator-type", "webhook",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
"--concierge-endpoint", "https://127.0.0.1:1234/",
|
||||
"--concierge-ca-bundle-data", base64.StdEncoding.EncodeToString(testCA.Bundle()),
|
||||
"--concierge-api-group-suffix", "some.suffix.com",
|
||||
},
|
||||
wantIssuer: "test-issuer",
|
||||
wantClientID: "test-client-id",
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
wantOptionsCount: 7,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"exchanged-token"}}` + "\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
gotIssuer string
|
||||
gotClientID string
|
||||
gotOptions []oidcclient.Option
|
||||
gotOptions []oidcclient.Option
|
||||
)
|
||||
cmd := oidcLoginCommand(func(issuer string, clientID string, opts ...oidcclient.Option) (*oidcclient.Token, error) {
|
||||
gotIssuer = issuer
|
||||
gotClientID = clientID
|
||||
gotOptions = opts
|
||||
return &oidcclient.Token{
|
||||
IDToken: &oidcclient.IDToken{
|
||||
Token: "test-id-token",
|
||||
Expiry: metav1.NewTime(time1),
|
||||
},
|
||||
}, nil
|
||||
cmd := oidcLoginCommand(oidcLoginCommandDeps{
|
||||
login: func(issuer string, clientID string, opts ...oidcclient.Option) (*oidctypes.Token, error) {
|
||||
require.Equal(t, "test-issuer", issuer)
|
||||
require.Equal(t, "test-client-id", clientID)
|
||||
gotOptions = opts
|
||||
if tt.loginErr != nil {
|
||||
return nil, tt.loginErr
|
||||
}
|
||||
return &oidctypes.Token{
|
||||
IDToken: &oidctypes.IDToken{
|
||||
Token: "test-id-token",
|
||||
Expiry: metav1.NewTime(time1),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
|
||||
require.Equal(t, token, "test-id-token")
|
||||
if tt.conciergeErr != nil {
|
||||
return nil, tt.conciergeErr
|
||||
}
|
||||
return &clientauthv1beta1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ExecCredential",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Status: &clientauthv1beta1.ExecCredentialStatus{
|
||||
Token: "exchanged-token",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
@@ -119,8 +245,6 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
}
|
||||
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
||||
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
|
||||
require.Equal(t, tt.wantIssuer, gotIssuer, "unexpected issuer")
|
||||
require.Equal(t, tt.wantClientID, gotClientID, "unexpected client ID")
|
||||
require.Len(t, gotOptions, tt.wantOptionsCount)
|
||||
})
|
||||
}
|
||||
|
||||
128
cmd/pinniped/cmd/login_static.go
Normal file
128
cmd/pinniped/cmd/login_static.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||
)
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
loginCmd.AddCommand(staticLoginCommand(staticLoginRealDeps()))
|
||||
}
|
||||
|
||||
type staticLoginDeps struct {
|
||||
lookupEnv func(string) (string, bool)
|
||||
exchangeToken func(context.Context, *conciergeclient.Client, string) (*clientauthv1beta1.ExecCredential, error)
|
||||
}
|
||||
|
||||
func staticLoginRealDeps() staticLoginDeps {
|
||||
return staticLoginDeps{
|
||||
lookupEnv: os.LookupEnv,
|
||||
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
|
||||
return client.ExchangeToken(ctx, token)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type staticLoginParams struct {
|
||||
staticToken string
|
||||
staticTokenEnvName string
|
||||
conciergeEnabled bool
|
||||
conciergeAuthenticatorType string
|
||||
conciergeAuthenticatorName string
|
||||
conciergeEndpoint string
|
||||
conciergeCABundle string
|
||||
conciergeAPIGroupSuffix string
|
||||
}
|
||||
|
||||
func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
|
||||
var (
|
||||
cmd = &cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
Use: "static [--token TOKEN] [--token-env TOKEN_NAME]",
|
||||
Short: "Login using a static token",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags staticLoginParams
|
||||
conciergeNamespace string // unused now
|
||||
)
|
||||
cmd.Flags().StringVar(&flags.staticToken, "token", "", "Static token to present during login")
|
||||
cmd.Flags().StringVar(&flags.staticTokenEnvName, "token-env", "", "Environment variable containing a static token")
|
||||
cmd.Flags().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Use the Concierge to login")
|
||||
cmd.Flags().StringVar(&conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the Concierge was installed")
|
||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt')")
|
||||
cmd.Flags().StringVar(&flags.conciergeAuthenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name")
|
||||
cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the Concierge")
|
||||
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) }
|
||||
|
||||
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
|
||||
mustMarkHidden(cmd, "concierge-namespace")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams) error {
|
||||
if flags.staticToken == "" && flags.staticTokenEnvName == "" {
|
||||
return fmt.Errorf("one of --token or --token-env must be set")
|
||||
}
|
||||
|
||||
var concierge *conciergeclient.Client
|
||||
if flags.conciergeEnabled {
|
||||
var err error
|
||||
concierge, err = conciergeclient.New(
|
||||
conciergeclient.WithEndpoint(flags.conciergeEndpoint),
|
||||
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
|
||||
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
|
||||
conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid Concierge parameters: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var token string
|
||||
if flags.staticToken != "" {
|
||||
token = flags.staticToken
|
||||
}
|
||||
if flags.staticTokenEnvName != "" {
|
||||
var ok bool
|
||||
token, ok = deps.lookupEnv(flags.staticTokenEnvName)
|
||||
if !ok {
|
||||
return fmt.Errorf("--token-env variable %q is not set", flags.staticTokenEnvName)
|
||||
}
|
||||
if token == "" {
|
||||
return fmt.Errorf("--token-env variable %q is empty", flags.staticTokenEnvName)
|
||||
}
|
||||
}
|
||||
cred := tokenCredential(&oidctypes.Token{IDToken: &oidctypes.IDToken{Token: token}})
|
||||
|
||||
// If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential.
|
||||
if concierge != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
cred, err = deps.exchangeToken(ctx, concierge, token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not complete Concierge credential exchange: %w", err)
|
||||
}
|
||||
}
|
||||
return json.NewEncoder(out).Encode(cred)
|
||||
}
|
||||
194
cmd/pinniped/cmd/login_static_test.go
Normal file
194
cmd/pinniped/cmd/login_static_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
)
|
||||
|
||||
func TestLoginStaticCommand(t *testing.T) {
|
||||
testCA, err := certauthority.New("Test CA", 1*time.Hour)
|
||||
require.NoError(t, err)
|
||||
tmpdir := testutil.TempDir(t)
|
||||
testCABundlePath := filepath.Join(tmpdir, "testca.pem")
|
||||
require.NoError(t, ioutil.WriteFile(testCABundlePath, testCA.Bundle(), 0600))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
env map[string]string
|
||||
loginErr error
|
||||
conciergeErr error
|
||||
wantError bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantOptionsCount int
|
||||
}{
|
||||
{
|
||||
name: "help flag passed",
|
||||
args: []string{"--help"},
|
||||
wantStdout: here.Doc(`
|
||||
Login using a static token
|
||||
|
||||
Usage:
|
||||
static [--token TOKEN] [--token-env TOKEN_NAME] [flags]
|
||||
|
||||
Flags:
|
||||
--concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
||||
--concierge-authenticator-name string Concierge authenticator name
|
||||
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt')
|
||||
--concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge
|
||||
--concierge-endpoint string API base for the Concierge endpoint
|
||||
--enable-concierge Use the Concierge to login
|
||||
-h, --help help for static
|
||||
--token string Static token to present during login
|
||||
--token-env string Environment variable containing a static token
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "missing required flags",
|
||||
args: []string{},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: one of --token or --token-env must be set
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "missing concierge flags",
|
||||
args: []string{
|
||||
"--token", "test-token",
|
||||
"--enable-concierge",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: endpoint must not be empty
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "missing env var",
|
||||
args: []string{
|
||||
"--token-env", "TEST_TOKEN_ENV",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --token-env variable "TEST_TOKEN_ENV" is not set
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "empty env var",
|
||||
args: []string{
|
||||
"--token-env", "TEST_TOKEN_ENV",
|
||||
},
|
||||
env: map[string]string{
|
||||
"TEST_TOKEN_ENV": "",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --token-env variable "TEST_TOKEN_ENV" is empty
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "env var token success",
|
||||
args: []string{
|
||||
"--token-env", "TEST_TOKEN_ENV",
|
||||
},
|
||||
env: map[string]string{
|
||||
"TEST_TOKEN_ENV": "test-token",
|
||||
},
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"test-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "concierge failure",
|
||||
args: []string{
|
||||
"--token", "test-token",
|
||||
"--enable-concierge",
|
||||
"--concierge-endpoint", "https://127.0.0.1/",
|
||||
"--concierge-authenticator-type", "webhook",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
},
|
||||
conciergeErr: fmt.Errorf("some concierge error"),
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not complete Concierge credential exchange: some concierge error
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid API group suffix",
|
||||
args: []string{
|
||||
"--token", "test-token",
|
||||
"--enable-concierge",
|
||||
"--concierge-api-group-suffix", ".starts.with.dot",
|
||||
"--concierge-authenticator-type", "jwt",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
"--concierge-endpoint", "https://127.0.0.1:1234/",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: invalid API group suffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "static token success",
|
||||
args: []string{
|
||||
"--token", "test-token",
|
||||
},
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"test-token"}}` + "\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := staticLoginCommand(staticLoginDeps{
|
||||
lookupEnv: func(s string) (string, bool) {
|
||||
v, ok := tt.env[s]
|
||||
return v, ok
|
||||
},
|
||||
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
|
||||
require.Equal(t, token, "test-token")
|
||||
if tt.conciergeErr != nil {
|
||||
return nil, tt.conciergeErr
|
||||
}
|
||||
return &clientauthv1beta1.ExecCredential{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ExecCredential",
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Status: &clientauthv1beta1.ExecCredentialStatus{
|
||||
Token: "exchanged-token",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.SetOut(&stdout)
|
||||
cmd.SetErr(&stderr)
|
||||
cmd.SetArgs(tt.args)
|
||||
err := cmd.Execute()
|
||||
if tt.wantError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
||||
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
@@ -18,11 +19,16 @@ var rootCmd = &cobra.Command{
|
||||
SilenceUsage: true, // do not print usage message when commands fail
|
||||
}
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
// We don't want klog flags showing up in our CLI.
|
||||
plog.RemoveKlogGlobalFlags()
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/here"
|
||||
@@ -36,13 +37,12 @@ var (
|
||||
)
|
||||
|
||||
func TestNewVersionCmd(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantError bool
|
||||
wantStdoutRegexp string
|
||||
wantStderr string
|
||||
wantStderrRegexp string
|
||||
}{
|
||||
{
|
||||
name: "no flags",
|
||||
@@ -58,13 +58,13 @@ func TestNewVersionCmd(t *testing.T) {
|
||||
name: "arg passed",
|
||||
args: []string{"tuna"},
|
||||
wantError: true,
|
||||
wantStdoutRegexp: `Error: unknown command "tuna" for "version"` + "\n" + knownGoodUsageRegexpForVersion,
|
||||
wantStderrRegexp: `Error: unknown command "tuna" for "version"`,
|
||||
wantStdoutRegexp: knownGoodUsageRegexpForVersion,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cmd := newVersionCommand()
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
@@ -78,8 +78,8 @@ func TestNewVersionCmd(t *testing.T) {
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Regexp(t, tt.wantStdoutRegexp, stdout.String(), "unexpected stdout")
|
||||
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
|
||||
assert.Regexp(t, tt.wantStdoutRegexp, stdout.String(), "unexpected stdout")
|
||||
assert.Regexp(t, tt.wantStderrRegexp, stderr.String(), "unexpected stderr")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
191
cmd/pinniped/cmd/whoami.go
Normal file
191
cmd/pinniped/cmd/whoami.go
Normal file
@@ -0,0 +1,191 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
||||
conciergescheme "go.pinniped.dev/internal/concierge/scheme"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/here"
|
||||
)
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(newWhoamiCommand(getRealConciergeClientset))
|
||||
}
|
||||
|
||||
type whoamiFlags struct {
|
||||
outputFormat string // e.g., yaml, json, text
|
||||
|
||||
kubeconfigPath string
|
||||
kubeconfigContextOverride string
|
||||
|
||||
apiGroupSuffix string
|
||||
}
|
||||
|
||||
type clusterInfo struct {
|
||||
name string
|
||||
url string
|
||||
}
|
||||
|
||||
func newWhoamiCommand(getClientset getConciergeClientsetFunc) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Args: cobra.NoArgs, // do not accept positional arguments for this command
|
||||
Use: "whoami",
|
||||
Short: "Print information about the current user",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags := &whoamiFlags{}
|
||||
|
||||
// flags
|
||||
f := cmd.Flags()
|
||||
f.StringVarP(&flags.outputFormat, "output", "o", "text", "Output format (e.g., 'yaml', 'json', 'text')")
|
||||
f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file")
|
||||
f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)")
|
||||
f.StringVar(&flags.apiGroupSuffix, "api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, _ []string) error {
|
||||
return runWhoami(cmd.OutOrStdout(), getClientset, flags)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runWhoami(output io.Writer, getClientset getConciergeClientsetFunc, flags *whoamiFlags) error {
|
||||
clientConfig := newClientConfig(flags.kubeconfigPath, flags.kubeconfigContextOverride)
|
||||
clientset, err := getClientset(clientConfig, flags.apiGroupSuffix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not configure Kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
clusterInfo, err := getCurrentCluster(clientConfig, flags.kubeconfigContextOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get current cluster info: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
||||
defer cancelFunc()
|
||||
whoAmI, err := clientset.IdentityV1alpha1().WhoAmIRequests().Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
hint := ""
|
||||
if errors.IsNotFound(err) {
|
||||
hint = " (is the Pinniped WhoAmI API running and healthy?)"
|
||||
}
|
||||
return fmt.Errorf("could not complete WhoAmIRequest%s: %w", hint, err)
|
||||
}
|
||||
|
||||
if err := writeWhoamiOutput(output, flags, clusterInfo, whoAmI); err != nil {
|
||||
return fmt.Errorf("could not write output: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCurrentCluster(clientConfig clientcmd.ClientConfig, currentContextNameOverride string) (*clusterInfo, error) {
|
||||
currentKubeConfig, err := clientConfig.RawConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contextName := currentKubeConfig.CurrentContext
|
||||
if len(currentContextNameOverride) > 0 {
|
||||
contextName = currentContextNameOverride
|
||||
}
|
||||
|
||||
unknownClusterInfo := &clusterInfo{name: "???", url: "???"}
|
||||
ctx, ok := currentKubeConfig.Contexts[contextName]
|
||||
if !ok {
|
||||
return unknownClusterInfo, nil
|
||||
}
|
||||
|
||||
cluster, ok := currentKubeConfig.Clusters[ctx.Cluster]
|
||||
if !ok {
|
||||
return unknownClusterInfo, nil
|
||||
}
|
||||
|
||||
return &clusterInfo{name: ctx.Cluster, url: cluster.Server}, nil
|
||||
}
|
||||
|
||||
func writeWhoamiOutput(output io.Writer, flags *whoamiFlags, cInfo *clusterInfo, whoAmI *identityv1alpha1.WhoAmIRequest) error {
|
||||
switch flags.outputFormat {
|
||||
case "text":
|
||||
return writeWhoamiOutputText(output, cInfo, whoAmI)
|
||||
case "json":
|
||||
return writeWhoamiOutputJSON(output, flags.apiGroupSuffix, whoAmI)
|
||||
case "yaml":
|
||||
return writeWhoamiOutputYAML(output, flags.apiGroupSuffix, whoAmI)
|
||||
default:
|
||||
return fmt.Errorf("unknown output format: %q", flags.outputFormat)
|
||||
}
|
||||
}
|
||||
|
||||
func writeWhoamiOutputText(output io.Writer, clusterInfo *clusterInfo, whoAmI *identityv1alpha1.WhoAmIRequest) error {
|
||||
fmt.Fprint(output, here.Docf(`
|
||||
Current cluster info:
|
||||
|
||||
Name: %s
|
||||
URL: %s
|
||||
|
||||
Current user info:
|
||||
|
||||
Username: %s
|
||||
Groups: %s
|
||||
`, clusterInfo.name, clusterInfo.url, whoAmI.Status.KubernetesUserInfo.User.Username, prettyStrings(whoAmI.Status.KubernetesUserInfo.User.Groups)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeWhoamiOutputJSON(output io.Writer, apiGroupSuffix string, whoAmI *identityv1alpha1.WhoAmIRequest) error {
|
||||
return serialize(output, apiGroupSuffix, whoAmI, runtime.ContentTypeJSON)
|
||||
}
|
||||
|
||||
func writeWhoamiOutputYAML(output io.Writer, apiGroupSuffix string, whoAmI *identityv1alpha1.WhoAmIRequest) error {
|
||||
return serialize(output, apiGroupSuffix, whoAmI, runtime.ContentTypeYAML)
|
||||
}
|
||||
|
||||
func serialize(output io.Writer, apiGroupSuffix string, whoAmI *identityv1alpha1.WhoAmIRequest, contentType string) error {
|
||||
scheme, _, identityGV := conciergescheme.New(apiGroupSuffix)
|
||||
codecs := serializer.NewCodecFactory(scheme)
|
||||
respInfo, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), contentType)
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown content type: %q", contentType)
|
||||
}
|
||||
|
||||
// I have seen the pretty serializer be nil before, so this will hopefully protect against that
|
||||
// corner.
|
||||
serializer := respInfo.PrettySerializer
|
||||
if serializer == nil {
|
||||
serializer = respInfo.Serializer
|
||||
}
|
||||
|
||||
// Ensure that these fields are set so that the JSON/YAML output tells the full story.
|
||||
whoAmI.APIVersion = identityGV.String()
|
||||
whoAmI.Kind = "WhoAmIRequest"
|
||||
|
||||
return serializer.Encode(whoAmI, output)
|
||||
}
|
||||
|
||||
func prettyStrings(ss []string) string {
|
||||
b := &strings.Builder{}
|
||||
for i, s := range ss {
|
||||
if i != 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.WriteString(s)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
327
cmd/pinniped/cmd/whoami_test.go
Normal file
327
cmd/pinniped/cmd/whoami_test.go
Normal file
@@ -0,0 +1,327 @@
|
||||
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
kubetesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
||||
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||
fakeconciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
|
||||
"go.pinniped.dev/internal/constable"
|
||||
"go.pinniped.dev/internal/here"
|
||||
)
|
||||
|
||||
func TestWhoami(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
groupsOverride []string
|
||||
gettingClientsetErr error
|
||||
callingAPIErr error
|
||||
wantError bool
|
||||
wantStdout, wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "help flag",
|
||||
args: []string{"--help"},
|
||||
wantStdout: here.Doc(`
|
||||
Print information about the current user
|
||||
|
||||
Usage:
|
||||
whoami [flags]
|
||||
|
||||
Flags:
|
||||
--api-group-suffix string Concierge API group suffix (default "pinniped.dev")
|
||||
-h, --help help for whoami
|
||||
--kubeconfig string Path to kubeconfig file
|
||||
--kubeconfig-context string Kubeconfig context name (default: current active context)
|
||||
-o, --output string Output format (e.g., 'yaml', 'json', 'text') (default "text")
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "text output",
|
||||
args: []string{"--kubeconfig", "testdata/kubeconfig.yaml"},
|
||||
wantStdout: here.Doc(`
|
||||
Current cluster info:
|
||||
|
||||
Name: kind-kind
|
||||
URL: https://fake-server-url-value
|
||||
|
||||
Current user info:
|
||||
|
||||
Username: some-username
|
||||
Groups: some-group-0, some-group-1
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "text output with long output flag",
|
||||
args: []string{"--kubeconfig", "testdata/kubeconfig.yaml", "--output", "text"},
|
||||
wantStdout: here.Doc(`
|
||||
Current cluster info:
|
||||
|
||||
Name: kind-kind
|
||||
URL: https://fake-server-url-value
|
||||
|
||||
Current user info:
|
||||
|
||||
Username: some-username
|
||||
Groups: some-group-0, some-group-1
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "text output with 1 group",
|
||||
args: []string{"--kubeconfig", "testdata/kubeconfig.yaml", "--output", "text"},
|
||||
groupsOverride: []string{"some-group-0"},
|
||||
wantStdout: here.Doc(`
|
||||
Current cluster info:
|
||||
|
||||
Name: kind-kind
|
||||
URL: https://fake-server-url-value
|
||||
|
||||
Current user info:
|
||||
|
||||
Username: some-username
|
||||
Groups: some-group-0
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "text output with no groups",
|
||||
args: []string{"--kubeconfig", "testdata/kubeconfig.yaml", "--output", "text"},
|
||||
groupsOverride: []string{},
|
||||
wantStdout: here.Doc(`
|
||||
Current cluster info:
|
||||
|
||||
Name: kind-kind
|
||||
URL: https://fake-server-url-value
|
||||
|
||||
Current user info:
|
||||
|
||||
Username: some-username
|
||||
Groups:
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "json output",
|
||||
args: []string{"--kubeconfig", "testdata/kubeconfig.yaml", "-o", "json"},
|
||||
wantStdout: here.Doc(`
|
||||
{
|
||||
"kind": "WhoAmIRequest",
|
||||
"apiVersion": "identity.concierge.pinniped.dev/v1alpha1",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"spec": {},
|
||||
"status": {
|
||||
"kubernetesUserInfo": {
|
||||
"user": {
|
||||
"username": "some-username",
|
||||
"groups": [
|
||||
"some-group-0",
|
||||
"some-group-1"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
name: "json output with api group suffix flag",
|
||||
args: []string{"--kubeconfig", "testdata/kubeconfig.yaml", "-o", "json", "--api-group-suffix", "tuna.io"},
|
||||
wantStdout: here.Doc(`
|
||||
{
|
||||
"kind": "WhoAmIRequest",
|
||||
"apiVersion": "identity.concierge.tuna.io/v1alpha1",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"spec": {},
|
||||
"status": {
|
||||
"kubernetesUserInfo": {
|
||||
"user": {
|
||||
"username": "some-username",
|
||||
"groups": [
|
||||
"some-group-0",
|
||||
"some-group-1"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
name: "yaml output",
|
||||
args: []string{"--kubeconfig", "testdata/kubeconfig.yaml", "-o", "yaml"},
|
||||
wantStdout: here.Doc(`
|
||||
apiVersion: identity.concierge.pinniped.dev/v1alpha1
|
||||
kind: WhoAmIRequest
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
spec: {}
|
||||
status:
|
||||
kubernetesUserInfo:
|
||||
user:
|
||||
groups:
|
||||
- some-group-0
|
||||
- some-group-1
|
||||
username: some-username
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "yaml output with api group suffix",
|
||||
args: []string{"--kubeconfig", "testdata/kubeconfig.yaml", "-o", "yaml", "--api-group-suffix", "tuna.io"},
|
||||
wantStdout: here.Doc(`
|
||||
apiVersion: identity.concierge.tuna.io/v1alpha1
|
||||
kind: WhoAmIRequest
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
spec: {}
|
||||
status:
|
||||
kubernetesUserInfo:
|
||||
user:
|
||||
groups:
|
||||
- some-group-0
|
||||
- some-group-1
|
||||
username: some-username
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "extra args",
|
||||
args: []string{"extra-arg"},
|
||||
wantError: true,
|
||||
wantStderr: "Error: unknown command \"extra-arg\" for \"whoami\"\n",
|
||||
},
|
||||
{
|
||||
name: "cannot get cluster info",
|
||||
args: []string{"--kubeconfig", "this-file-does-not-exist"},
|
||||
wantError: true,
|
||||
wantStderr: "Error: could not get current cluster info: stat this-file-does-not-exist: no such file or directory\n",
|
||||
},
|
||||
{
|
||||
name: "different kubeconfig context, but same as current",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--kubeconfig-context", "kind-kind",
|
||||
},
|
||||
wantStdout: here.Doc(`
|
||||
Current cluster info:
|
||||
|
||||
Name: kind-kind
|
||||
URL: https://fake-server-url-value
|
||||
|
||||
Current user info:
|
||||
|
||||
Username: some-username
|
||||
Groups: some-group-0, some-group-1
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "different kubeconfig context, not current",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--kubeconfig-context", "some-other-context",
|
||||
},
|
||||
wantStdout: here.Doc(`
|
||||
Current cluster info:
|
||||
|
||||
Name: some-other-cluster
|
||||
URL: https://some-other-fake-server-url-value
|
||||
|
||||
Current user info:
|
||||
|
||||
Username: some-username
|
||||
Groups: some-group-0, some-group-1
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid kubeconfig context prints ???",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--kubeconfig-context", "invalid",
|
||||
},
|
||||
wantStdout: here.Doc(`
|
||||
Current cluster info:
|
||||
|
||||
Name: ???
|
||||
URL: ???
|
||||
|
||||
Current user info:
|
||||
|
||||
Username: some-username
|
||||
Groups: some-group-0, some-group-1
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "getting clientset fails",
|
||||
gettingClientsetErr: constable.Error("some get clientset error"),
|
||||
wantError: true,
|
||||
wantStderr: "Error: could not configure Kubernetes client: some get clientset error\n",
|
||||
},
|
||||
{
|
||||
name: "calling API fails",
|
||||
callingAPIErr: constable.Error("some API error"),
|
||||
wantError: true,
|
||||
wantStderr: "Error: could not complete WhoAmIRequest: some API error\n",
|
||||
},
|
||||
{
|
||||
name: "calling API fails because WhoAmI API is not installed",
|
||||
callingAPIErr: errors.NewNotFound(identityv1alpha1.SchemeGroupVersion.WithResource("whoamirequests").GroupResource(), "whatever"),
|
||||
wantError: true,
|
||||
wantStderr: "Error: could not complete WhoAmIRequest (is the Pinniped WhoAmI API running and healthy?): whoamirequests.identity.concierge.pinniped.dev \"whatever\" not found\n",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
getClientset := func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error) {
|
||||
if test.gettingClientsetErr != nil {
|
||||
return nil, test.gettingClientsetErr
|
||||
}
|
||||
clientset := fakeconciergeclientset.NewSimpleClientset()
|
||||
clientset.PrependReactor("create", "whoamirequests", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
||||
if test.callingAPIErr != nil {
|
||||
return true, nil, test.callingAPIErr
|
||||
}
|
||||
groups := []string{"some-group-0", "some-group-1"}
|
||||
if test.groupsOverride != nil {
|
||||
groups = test.groupsOverride
|
||||
}
|
||||
return true, &identityv1alpha1.WhoAmIRequest{
|
||||
Status: identityv1alpha1.WhoAmIRequestStatus{
|
||||
KubernetesUserInfo: identityv1alpha1.KubernetesUserInfo{
|
||||
User: identityv1alpha1.UserInfo{
|
||||
Username: "some-username",
|
||||
Groups: groups,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
return clientset, nil
|
||||
}
|
||||
cmd := newWhoamiCommand(getClientset)
|
||||
|
||||
stdout, stderr := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
|
||||
cmd.SetOut(stdout)
|
||||
cmd.SetErr(stderr)
|
||||
cmd.SetArgs(test.args)
|
||||
|
||||
err := cmd.Execute()
|
||||
if test.wantError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, test.wantStdout, stdout.String())
|
||||
require.Equal(t, test.wantStderr, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,3 @@
|
||||
# Deploying
|
||||
# Pinniped Concierge Deployment
|
||||
|
||||
## Connecting Pinniped to an Identity Provider
|
||||
|
||||
If you would like to try Pinniped, but you don't have a compatible identity provider,
|
||||
you can use Pinniped's test identity provider.
|
||||
See [deploy/local-user-authenticator/README.md](../../deploy/local-user-authenticator/README.md)
|
||||
for details.
|
||||
|
||||
## Installing the Latest Version with Default Options
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/download/$(curl https://api.github.com/repos/vmware-tanzu/pinniped/releases/latest -s | jq .name -r)/install-pinniped-concierge.yaml
|
||||
```
|
||||
|
||||
## Installing an Older Version with Default Options
|
||||
|
||||
Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number
|
||||
and use it to replace the version number in the URL below.
|
||||
|
||||
```bash
|
||||
# Replace v0.2.0 with your preferred version in the URL below
|
||||
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/download/v0.2.0/install-pinniped-concierge.yaml
|
||||
```
|
||||
|
||||
## Installing with Custom Options
|
||||
|
||||
Creating your own deployment YAML file requires `ytt` from [Carvel](https://carvel.dev/) to template the YAML files
|
||||
in the `deploy/concierge` directory.
|
||||
Either [install `ytt`](https://get-ytt.io/) or use the [container image from Dockerhub](https://hub.docker.com/r/k14s/image/tags).
|
||||
|
||||
1. `git clone` this repo and `git checkout` the release version tag of the release that you would like to deploy.
|
||||
1. The configuration options are in [deploy/concierge/values.yml](values.yaml).
|
||||
Fill in the values in that file, or override those values using additional `ytt` command-line options in
|
||||
the command below. Use the release version tag as the `image_tag` value.
|
||||
2. In a terminal, cd to this `deploy/concierge` directory
|
||||
3. To generate the final YAML files, run `ytt --file .`
|
||||
4. Deploy the generated YAML using your preferred deployment tool, such as `kubectl` or [`kapp`](https://get-kapp.io/).
|
||||
For example: `ytt --file . | kapp deploy --yes --app pinniped --diff-changes --file -`
|
||||
See [the how-to guide for details](https://pinniped.dev/docs/howto/install-concierge/).
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.4.0
|
||||
creationTimestamp: null
|
||||
name: jwtauthenticators.authentication.concierge.pinniped.dev
|
||||
spec:
|
||||
group: authentication.concierge.pinniped.dev
|
||||
names:
|
||||
categories:
|
||||
- pinniped
|
||||
- pinniped-authenticator
|
||||
- pinniped-authenticators
|
||||
kind: JWTAuthenticator
|
||||
listKind: JWTAuthenticatorList
|
||||
plural: jwtauthenticators
|
||||
singular: jwtauthenticator
|
||||
scope: Cluster
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.issuer
|
||||
name: Issuer
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: "JWTAuthenticator describes the configuration of a JWT authenticator.
|
||||
\n Upon receiving a signed JWT, a JWTAuthenticator will performs some validation
|
||||
on it (e.g., valid signature, existence of claims, etc.) and extract the
|
||||
username and groups from the token."
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: Spec for configuring the authenticator.
|
||||
properties:
|
||||
audience:
|
||||
description: Audience is the required value of the "aud" JWT claim.
|
||||
minLength: 1
|
||||
type: string
|
||||
claims:
|
||||
description: Claims allows customization of the claims that will be
|
||||
mapped to user identity for Kubernetes access.
|
||||
properties:
|
||||
groups:
|
||||
description: Groups is the name of the claim which should be read
|
||||
to extract the user's group membership from the JWT token. When
|
||||
not specified, it will default to "groups".
|
||||
type: string
|
||||
username:
|
||||
description: Username is the name of the claim which should be
|
||||
read to extract the username from the JWT token. When not specified,
|
||||
it will default to "username".
|
||||
type: string
|
||||
type: object
|
||||
issuer:
|
||||
description: Issuer is the OIDC issuer URL that will be used to discover
|
||||
public signing keys. Issuer is also used to validate the "iss" JWT
|
||||
claim.
|
||||
minLength: 1
|
||||
pattern: ^https://
|
||||
type: string
|
||||
tls:
|
||||
description: TLS configuration for communicating with the OIDC provider.
|
||||
properties:
|
||||
certificateAuthorityData:
|
||||
description: X.509 Certificate Authority (base64-encoded PEM bundle).
|
||||
If omitted, a default set of system roots will be trusted.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- audience
|
||||
- issuer
|
||||
type: object
|
||||
status:
|
||||
description: Status of the authenticator.
|
||||
properties:
|
||||
conditions:
|
||||
description: Represents the observations of the authenticator's current
|
||||
state.
|
||||
items:
|
||||
description: Condition status of a resource (mirrored from the metav1.Condition
|
||||
type added in Kubernetes 1.19). In a future API version we can
|
||||
switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: lastTransitionTime is the last time the condition
|
||||
transitioned from one status to another. This should be when
|
||||
the underlying condition changed. If that is not known, then
|
||||
using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: message is a human readable message indicating
|
||||
details about the transition. This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: observedGeneration represents the .metadata.generation
|
||||
that the condition was set based upon. For instance, if .metadata.generation
|
||||
is currently 12, but the .status.conditions[x].observedGeneration
|
||||
is 9, the condition is out of date with respect to the current
|
||||
state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: reason contains a programmatic identifier indicating
|
||||
the reason for the condition's last transition. Producers
|
||||
of specific condition types may define expected values and
|
||||
meanings for this field, and whether the values are considered
|
||||
a guaranteed API. The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
--- Many .condition.type values are consistent across resources
|
||||
like Available, but because arbitrary conditions can be useful
|
||||
(see .node.status.conditions), the ability to deconflict is
|
||||
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
@@ -11,14 +11,14 @@ spec:
|
||||
group: authentication.concierge.pinniped.dev
|
||||
names:
|
||||
categories:
|
||||
- all
|
||||
- authenticator
|
||||
- authenticators
|
||||
- pinniped
|
||||
- pinniped-authenticator
|
||||
- pinniped-authenticators
|
||||
kind: WebhookAuthenticator
|
||||
listKind: WebhookAuthenticatorList
|
||||
plural: webhookauthenticators
|
||||
singular: webhookauthenticator
|
||||
scope: Namespaced
|
||||
scope: Cluster
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.endpoint
|
||||
@@ -137,7 +137,8 @@ spec:
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources: {}
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
|
||||
@@ -10,15 +10,18 @@ metadata:
|
||||
spec:
|
||||
group: config.concierge.pinniped.dev
|
||||
names:
|
||||
categories:
|
||||
- pinniped
|
||||
kind: CredentialIssuer
|
||||
listKind: CredentialIssuerList
|
||||
plural: credentialissuers
|
||||
singular: credentialissuer
|
||||
scope: Namespaced
|
||||
scope: Cluster
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Describes the configuration status of a Pinniped credential issuer.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
@@ -37,7 +40,8 @@ spec:
|
||||
properties:
|
||||
kubeConfigInfo:
|
||||
description: Information needed to form a valid Pinniped-based kubeconfig
|
||||
using this credential issuer.
|
||||
using this credential issuer. This field is deprecated and will
|
||||
be removed in a future version.
|
||||
properties:
|
||||
certificateAuthorityData:
|
||||
description: The K8s API server CA bundle.
|
||||
@@ -59,6 +63,59 @@ spec:
|
||||
description: Status of an integration strategy that was attempted
|
||||
by Pinniped.
|
||||
properties:
|
||||
frontend:
|
||||
description: Frontend describes how clients can connect using
|
||||
this strategy.
|
||||
properties:
|
||||
impersonationProxyInfo:
|
||||
description: ImpersonationProxyInfo describes the parameters
|
||||
for the impersonation proxy on this Concierge. This field
|
||||
is only set when Type is "ImpersonationProxy".
|
||||
properties:
|
||||
certificateAuthorityData:
|
||||
description: CertificateAuthorityData is the base64-encoded
|
||||
PEM CA bundle of the impersonation proxy.
|
||||
minLength: 1
|
||||
type: string
|
||||
endpoint:
|
||||
description: Endpoint is the HTTPS endpoint of the impersonation
|
||||
proxy.
|
||||
minLength: 1
|
||||
pattern: ^https://
|
||||
type: string
|
||||
required:
|
||||
- certificateAuthorityData
|
||||
- endpoint
|
||||
type: object
|
||||
tokenCredentialRequestInfo:
|
||||
description: TokenCredentialRequestAPIInfo describes the
|
||||
parameters for the TokenCredentialRequest API on this
|
||||
Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
properties:
|
||||
certificateAuthorityData:
|
||||
description: CertificateAuthorityData is the base64-encoded
|
||||
Kubernetes API server CA bundle.
|
||||
minLength: 1
|
||||
type: string
|
||||
server:
|
||||
description: Server is the Kubernetes API server URL.
|
||||
minLength: 1
|
||||
pattern: ^https://|^http://
|
||||
type: string
|
||||
required:
|
||||
- certificateAuthorityData
|
||||
- server
|
||||
type: object
|
||||
type:
|
||||
description: Type describes which frontend mechanism clients
|
||||
can use with a strategy.
|
||||
enum:
|
||||
- TokenCredentialRequestAPI
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
type: object
|
||||
lastUpdateTime:
|
||||
description: When the status was last checked.
|
||||
format: date-time
|
||||
@@ -70,8 +127,13 @@ spec:
|
||||
reason:
|
||||
description: Reason for the current status.
|
||||
enum:
|
||||
- FetchedKey
|
||||
- Listening
|
||||
- Pending
|
||||
- Disabled
|
||||
- ErrorDuringSetup
|
||||
- CouldNotFetchKey
|
||||
- CouldNotGetClusterInfo
|
||||
- FetchedKey
|
||||
type: string
|
||||
status:
|
||||
description: Status of the attempted integration strategy.
|
||||
@@ -83,6 +145,7 @@ spec:
|
||||
description: Type of integration attempted.
|
||||
enum:
|
||||
- KubeClusterSigningCertificate
|
||||
- ImpersonationProxy
|
||||
type: string
|
||||
required:
|
||||
- lastUpdateTime
|
||||
@@ -95,11 +158,11 @@ spec:
|
||||
required:
|
||||
- strategies
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
#@ load("@ytt:json", "json")
|
||||
#@ load("helpers.lib.yaml", "defaultLabel", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix")
|
||||
#@ load("helpers.lib.yaml", "defaultLabel", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix", "getAndValidateLogLevel", "pinnipedDevAPIGroupWithPrefix")
|
||||
|
||||
#@ if not data.values.into_namespace:
|
||||
---
|
||||
@@ -37,10 +37,16 @@ data:
|
||||
servingCertificate:
|
||||
durationSeconds: (@= str(data.values.api_serving_certificate_duration_seconds) @)
|
||||
renewBeforeSeconds: (@= str(data.values.api_serving_certificate_renew_before_seconds) @)
|
||||
apiGroupSuffix: (@= data.values.api_group_suffix @)
|
||||
names:
|
||||
servingCertificateSecret: (@= defaultResourceNameWithSuffix("api-tls-serving-certificate") @)
|
||||
credentialIssuer: (@= defaultResourceNameWithSuffix("config") @)
|
||||
apiService: (@= defaultResourceNameWithSuffix("api") @)
|
||||
impersonationConfigMap: (@= defaultResourceNameWithSuffix("impersonation-proxy-config") @)
|
||||
impersonationLoadBalancerService: (@= defaultResourceNameWithSuffix("impersonation-proxy-load-balancer") @)
|
||||
impersonationTLSCertificateSecret: (@= defaultResourceNameWithSuffix("impersonation-proxy-tls-serving-certificate") @)
|
||||
impersonationCACertificateSecret: (@= defaultResourceNameWithSuffix("impersonation-proxy-ca-certificate") @)
|
||||
impersonationSignerSecret: (@= defaultResourceNameWithSuffix("impersonation-proxy-signer-ca-certificate") @)
|
||||
labels: (@= json.encode(labels()).rstrip() @)
|
||||
kubeCertAgent:
|
||||
namePrefix: (@= defaultResourceNameWithSuffix("kube-cert-agent-") @)
|
||||
@@ -57,6 +63,9 @@ data:
|
||||
imagePullSecrets:
|
||||
- image-pull-secret
|
||||
(@ end @)
|
||||
(@ if data.values.log_level: @)
|
||||
logLevel: (@= getAndValidateLogLevel() @)
|
||||
(@ end @)
|
||||
---
|
||||
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
|
||||
apiVersion: v1
|
||||
@@ -87,8 +96,8 @@ spec:
|
||||
scheduler.alpha.kubernetes.io/critical-pod: ""
|
||||
spec:
|
||||
securityContext:
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
runAsUser: #@ data.values.run_as_user
|
||||
runAsGroup: #@ data.values.run_as_group
|
||||
serviceAccountName: #@ defaultResourceName()
|
||||
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
|
||||
imagePullSecrets:
|
||||
@@ -145,6 +154,9 @@ spec:
|
||||
- path: "labels"
|
||||
fieldRef:
|
||||
fieldPath: metadata.labels
|
||||
- path: "name"
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- path: "namespace"
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
@@ -182,16 +194,46 @@ spec:
|
||||
port: 443
|
||||
targetPort: 8443
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("proxy")
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector: #@ defaultLabel()
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
targetPort: 8444
|
||||
---
|
||||
apiVersion: apiregistration.k8s.io/v1
|
||||
kind: APIService
|
||||
metadata:
|
||||
name: v1alpha1.login.concierge.pinniped.dev
|
||||
name: #@ pinnipedDevAPIGroupWithPrefix("v1alpha1.login.concierge")
|
||||
labels: #@ labels()
|
||||
spec:
|
||||
version: v1alpha1
|
||||
group: login.concierge.pinniped.dev
|
||||
groupPriorityMinimum: 2500
|
||||
versionPriority: 10
|
||||
group: #@ pinnipedDevAPIGroupWithPrefix("login.concierge")
|
||||
groupPriorityMinimum: 9900
|
||||
versionPriority: 15
|
||||
#! caBundle: Do not include this key here. Starts out null, will be updated/owned by the golang code.
|
||||
service:
|
||||
name: #@ defaultResourceNameWithSuffix("api")
|
||||
namespace: #@ namespace()
|
||||
port: 443
|
||||
---
|
||||
apiVersion: apiregistration.k8s.io/v1
|
||||
kind: APIService
|
||||
metadata:
|
||||
name: #@ pinnipedDevAPIGroupWithPrefix("v1alpha1.identity.concierge")
|
||||
labels: #@ labels()
|
||||
spec:
|
||||
version: v1alpha1
|
||||
group: #@ pinnipedDevAPIGroupWithPrefix("identity.concierge")
|
||||
groupPriorityMinimum: 9900
|
||||
versionPriority: 15
|
||||
#! caBundle: Do not include this key here. Starts out null, will be updated/owned by the golang code.
|
||||
service:
|
||||
name: #@ defaultResourceNameWithSuffix("api")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
@@ -12,6 +12,10 @@
|
||||
#@ return data.values.app_name + "-" + suffix
|
||||
#@ end
|
||||
|
||||
#@ def pinnipedDevAPIGroupWithPrefix(prefix):
|
||||
#@ return prefix + "." + data.values.api_group_suffix
|
||||
#@ end
|
||||
|
||||
#@ def namespace():
|
||||
#@ if data.values.into_namespace:
|
||||
#@ return data.values.into_namespace
|
||||
@@ -28,3 +32,11 @@ app: #@ data.values.app_name
|
||||
_: #@ template.replace(defaultLabel())
|
||||
_: #@ template.replace(data.values.custom_labels)
|
||||
#@ end
|
||||
|
||||
#@ def getAndValidateLogLevel():
|
||||
#@ log_level = data.values.log_level
|
||||
#@ if log_level != "info" and log_level != "debug" and log_level != "trace" and log_level != "all":
|
||||
#@ fail("log_level '" + log_level + "' is invalid")
|
||||
#@ end
|
||||
#@ return log_level
|
||||
#@ end
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix")
|
||||
#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix", "pinnipedDevAPIGroupWithPrefix")
|
||||
|
||||
#! Give permission to various cluster-scoped objects
|
||||
---
|
||||
@@ -17,13 +17,41 @@ rules:
|
||||
verbs: [ get, list, watch ]
|
||||
- apiGroups: [ apiregistration.k8s.io ]
|
||||
resources: [ apiservices ]
|
||||
verbs: [ create, get, list, patch, update, watch ]
|
||||
verbs: [ get, list, patch, update, watch ]
|
||||
- apiGroups: [ admissionregistration.k8s.io ]
|
||||
resources: [ validatingwebhookconfigurations, mutatingwebhookconfigurations ]
|
||||
verbs: [ get, list, watch ]
|
||||
- apiGroups: [ flowcontrol.apiserver.k8s.io ]
|
||||
resources: [ flowschemas, prioritylevelconfigurations ]
|
||||
verbs: [ get, list, watch ]
|
||||
- apiGroups: [ policy ]
|
||||
resources: [ podsecuritypolicies ]
|
||||
verbs: [ use ]
|
||||
- apiGroups: [ security.openshift.io ]
|
||||
resources: [ securitycontextconstraints ]
|
||||
verbs: [ use ]
|
||||
resourceNames: [ nonroot ]
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ "users", "groups", "serviceaccounts" ]
|
||||
verbs: [ "impersonate" ]
|
||||
- apiGroups: [ "authentication.k8s.io" ]
|
||||
resources: [ "*" ] #! What we really want is userextras/* but the RBAC authorizer only supports */subresource, not resource/*
|
||||
verbs: [ "impersonate" ]
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ nodes ]
|
||||
verbs: [ list ]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("config.concierge")
|
||||
resources: [ credentialissuers ]
|
||||
verbs: [ get, list, watch, create ]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("config.concierge")
|
||||
resources: [ credentialissuers/status ]
|
||||
verbs: [get, patch, update]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")
|
||||
resources: [ jwtauthenticators, webhookauthenticators ]
|
||||
verbs: [ get, list, watch ]
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
@@ -50,7 +78,7 @@ metadata:
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ services ]
|
||||
verbs: [ create, get, list, patch, update, watch ]
|
||||
verbs: [ create, get, list, patch, update, watch, delete ]
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ secrets ]
|
||||
verbs: [ create, get, list, patch, update, watch, delete ]
|
||||
@@ -62,9 +90,12 @@ rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ pods/exec ]
|
||||
verbs: [ create ]
|
||||
- apiGroups: [ config.concierge.pinniped.dev, authentication.concierge.pinniped.dev ]
|
||||
resources: [ "*" ]
|
||||
verbs: [ create, get, list, update, watch ]
|
||||
- apiGroups: [ apps ]
|
||||
resources: [ replicasets,deployments ]
|
||||
verbs: [ get ]
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ configmaps ]
|
||||
verbs: [ list, get, watch ]
|
||||
---
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
@@ -114,17 +145,22 @@ roleRef:
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("create-token-credential-requests")
|
||||
name: #@ defaultResourceNameWithSuffix("pre-authn-apis")
|
||||
labels: #@ labels()
|
||||
rules:
|
||||
- apiGroups: [ login.concierge.pinniped.dev ]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("login.concierge")
|
||||
resources: [ tokencredentialrequests ]
|
||||
verbs: [ create ]
|
||||
verbs: [ create, list ]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("identity.concierge")
|
||||
resources: [ whoamirequests ]
|
||||
verbs: [ create, list ]
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("create-token-credential-requests")
|
||||
name: #@ defaultResourceNameWithSuffix("pre-authn-apis")
|
||||
labels: #@ labels()
|
||||
subjects:
|
||||
- kind: Group
|
||||
@@ -135,7 +171,7 @@ subjects:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: #@ defaultResourceNameWithSuffix("create-token-credential-requests")
|
||||
name: #@ defaultResourceNameWithSuffix("pre-authn-apis")
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
#! Give permissions for subjectaccessreviews, tokenreview that is needed by aggregated api servers
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@data/values
|
||||
@@ -25,7 +25,7 @@ custom_labels: {} #! e.g. {myCustomLabelName: myCustomLabelValue, otherCustomLab
|
||||
replicas: 2
|
||||
|
||||
#! Specify either an image_digest or an image_tag. If both are given, only image_digest will be used.
|
||||
image_repo: docker.io/getpinniped/pinniped-server
|
||||
image_repo: projects.registry.vmware.com/pinniped/pinniped-server
|
||||
image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8
|
||||
image_tag: latest
|
||||
|
||||
@@ -50,3 +50,16 @@ discovery_url: #! e.g., https://example.com
|
||||
#! about every 25 days.
|
||||
api_serving_certificate_duration_seconds: 2592000
|
||||
api_serving_certificate_renew_before_seconds: 2160000
|
||||
|
||||
#! Specify the verbosity of logging: info ("nice to know" information), debug (developer
|
||||
#! information), trace (timing information), all (kitchen sink).
|
||||
log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs.
|
||||
|
||||
run_as_user: 1001 #! run_as_user specifies the user ID that will own the process
|
||||
run_as_group: 1001 #! run_as_group specifies the group ID that will own the process
|
||||
|
||||
#! Specify the API group suffix for all Pinniped API groups. By default, this is set to
|
||||
#! pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev,
|
||||
#! authentication.concierge.pinniped.dev, etc. As an example, if this is set to tuna.io, then
|
||||
#! Pinniped API groups will look like foo.tuna.io. authentication.concierge.tuna.io, etc.
|
||||
api_group_suffix: pinniped.dev
|
||||
|
||||
@@ -1,17 +1,33 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:overlay", "overlay")
|
||||
#@ load("helpers.lib.yaml", "labels")
|
||||
#@ load("helpers.lib.yaml", "labels", "pinnipedDevAPIGroupWithPrefix")
|
||||
#@ load("@ytt:data", "data")
|
||||
|
||||
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"credentialissuers.config.concierge.pinniped.dev"}}), expects=1
|
||||
---
|
||||
metadata:
|
||||
#@overlay/match missing_ok=True
|
||||
labels: #@ labels()
|
||||
name: #@ pinnipedDevAPIGroupWithPrefix("credentialissuers.config.concierge")
|
||||
spec:
|
||||
group: #@ pinnipedDevAPIGroupWithPrefix("config.concierge")
|
||||
|
||||
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"webhookauthenticators.authentication.concierge.pinniped.dev"}}), expects=1
|
||||
---
|
||||
metadata:
|
||||
#@overlay/match missing_ok=True
|
||||
labels: #@ labels()
|
||||
name: #@ pinnipedDevAPIGroupWithPrefix("webhookauthenticators.authentication.concierge")
|
||||
spec:
|
||||
group: #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")
|
||||
|
||||
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"jwtauthenticators.authentication.concierge.pinniped.dev"}}), expects=1
|
||||
---
|
||||
metadata:
|
||||
#@overlay/match missing_ok=True
|
||||
labels: #@ labels()
|
||||
name: #@ pinnipedDevAPIGroupWithPrefix("jwtauthenticators.authentication.concierge")
|
||||
spec:
|
||||
group: #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")
|
||||
|
||||
@@ -15,17 +15,17 @@ User accounts can be created and edited dynamically using `kubectl` commands (se
|
||||
## Installing the Latest Version with Default Options
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/latest/download/install-local-user-authenticator.yaml
|
||||
kubectl apply -f https://get.pinniped.dev/latest/install-local-user-authenticator.yaml
|
||||
```
|
||||
|
||||
## Installing an Older Version with Default Options
|
||||
## Installing a Specific Version with Default Options
|
||||
|
||||
Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number
|
||||
and use it to replace the version number in the URL below.
|
||||
|
||||
```bash
|
||||
# Replace v0.2.0 with your preferred version in the URL below
|
||||
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/download/v0.2.0/install-local-user-authenticator.yaml
|
||||
# Replace v0.4.1 with your preferred version in the URL below
|
||||
kubectl apply -f https://get.pinniped.dev/v0.4.1/install-local-user-authenticator.yaml
|
||||
```
|
||||
|
||||
## Installing with Custom Options
|
||||
@@ -79,7 +79,7 @@ kubectl get secret local-user-authenticator-tls-serving-certificate --namespace
|
||||
|
||||
When installing Pinniped on the same cluster, configure local-user-authenticator as an Identity Provider for Pinniped
|
||||
using the webhook URL `https://local-user-authenticator.local-user-authenticator.svc/authenticate`
|
||||
along with the CA bundle fetched by the above command. See [doc/demo.md](../../doc/demo.md) for an example.
|
||||
along with the CA bundle fetched by the above command. See [demo](https://pinniped.dev/docs/demo/) for an example.
|
||||
|
||||
## Optional: Manually Testing the Webhook Endpoint After Installing
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
@@ -48,8 +48,8 @@ spec:
|
||||
app: local-user-authenticator
|
||||
spec:
|
||||
securityContext:
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
runAsUser: #@ data.values.run_as_user
|
||||
runAsGroup: #@ data.values.run_as_group
|
||||
serviceAccountName: local-user-authenticator
|
||||
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
|
||||
imagePullSecrets:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@data/values
|
||||
---
|
||||
|
||||
#! Specify either an image_digest or an image_tag. If both are given, only image_digest will be used.
|
||||
image_repo: docker.io/getpinniped/pinniped-server
|
||||
image_repo: projects.registry.vmware.com/pinniped/pinniped-server
|
||||
image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8
|
||||
image_tag: latest
|
||||
|
||||
@@ -14,3 +14,6 @@ image_tag: latest
|
||||
#! Typically the value would be the output of: kubectl create secret docker-registry x --docker-server=https://example.io --docker-username="USERNAME" --docker-password="PASSWORD" --dry-run=client -o json | jq -r '.data[".dockerconfigjson"]'
|
||||
#! Optional.
|
||||
image_pull_dockerconfigjson: #! e.g. {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}}
|
||||
|
||||
run_as_user: 1001 #! run_as_user specifies the user ID that will own the process
|
||||
run_as_group: 1001 #! run_as_group specifies the group ID that will own the process
|
||||
|
||||
@@ -1,184 +1,3 @@
|
||||
# Deploying the Pinniped Supervisor
|
||||
# Pinniped Supervisor Deployment
|
||||
|
||||
## What is the Pinniped Supervisor?
|
||||
|
||||
The Pinniped Supervisor app is a component of the Pinniped OIDC and Cluster Federation solutions.
|
||||
It can be deployed when those features are needed.
|
||||
|
||||
## Installing the Latest Version with Default Options
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/latest/download/install-pinniped-supervisor.yaml
|
||||
```
|
||||
|
||||
## Installing an Older Version with Default Options
|
||||
|
||||
Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number
|
||||
and use it to replace the version number in the URL below.
|
||||
|
||||
```bash
|
||||
# Replace v0.3.0 with your preferred version in the URL below
|
||||
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/download/v0.3.0/install-pinniped-supervisor.yaml
|
||||
```
|
||||
|
||||
## Installing with Custom Options
|
||||
|
||||
Creating your own deployment YAML file requires `ytt` from [Carvel](https://carvel.dev/) to template the YAML files
|
||||
in the `deploy/supervisor` directory.
|
||||
Either [install `ytt`](https://get-ytt.io/) or use the [container image from Dockerhub](https://hub.docker.com/r/k14s/image/tags).
|
||||
|
||||
1. `git clone` this repo and `git checkout` the release version tag of the release that you would like to deploy.
|
||||
1. The configuration options are in [deploy/supervisor/values.yml](values.yaml).
|
||||
Fill in the values in that file, or override those values using additional `ytt` command-line options in
|
||||
the command below. Use the release version tag as the `image_tag` value.
|
||||
2. In a terminal, cd to this `deploy/supervisor` directory
|
||||
3. To generate the final YAML files, run `ytt --file .`
|
||||
4. Deploy the generated YAML using your preferred deployment tool, such as `kubectl` or [`kapp`](https://get-kapp.io/).
|
||||
For example: `ytt --file . | kapp deploy --yes --app pinniped-supervisor --diff-changes --file -`
|
||||
|
||||
## Configuring After Installing
|
||||
|
||||
### Exposing the Supervisor App as a Service
|
||||
|
||||
The Supervisor app's endpoints should be exposed as HTTPS endpoints with proper TLS certificates signed by a
|
||||
Certificate Authority which will be trusted by your user's web browsers. Because there are
|
||||
many ways to expose TLS services from a Kubernetes cluster, the Supervisor app leaves this up to the user.
|
||||
The most common ways are:
|
||||
|
||||
1. Define an [`Ingress` resource](https://kubernetes.io/docs/concepts/services-networking/ingress/) with TLS certificates.
|
||||
In this case, the ingress will terminate TLS. Typically, the ingress will then talk plain HTTP to its backend,
|
||||
which would be a NodePort or LoadBalancer Service in front of the HTTP port 8080 of the Supervisor pods.
|
||||
|
||||
The required configuration of the Ingress is specific to your cluster's Ingress Controller, so please refer to the
|
||||
documentation from your Kubernetes provider. If you are using a cluster from a cloud provider, then you'll probably
|
||||
want to start with that provider's documentation. For example, if your cluster is a Google GKE cluster, refer to
|
||||
the [GKE documentation for Ingress](https://cloud.google.com/kubernetes-engine/docs/concepts/ingress).
|
||||
Otherwise, the Kubernetes documentation provides a list of popular
|
||||
[Ingress Controllers](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/), including
|
||||
[Contour](https://projectcontour.io/) and many others.
|
||||
|
||||
1. Or, define a [TCP LoadBalancer Service](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer)
|
||||
which is a layer 4 load balancer and does not terminate TLS. In this case, the Supervisor app will need to be
|
||||
configured with TLS certificates and will terminate the TLS connection itself (see the section about OIDCProvider
|
||||
below). The LoadBalancer Service should be configured to use the HTTPS port 443 of the Supervisor pods as its `targetPort`.
|
||||
|
||||
*Warning:* Do not expose the Supervisor's port 8080 to the public. It would not be secure for the OIDC protocol
|
||||
to use HTTP, because the user's secret OIDC tokens would be transmitted across the network without encryption.
|
||||
|
||||
1. Or, expose the Supervisor app using a Kubernetes service mesh technology, e.g. [Istio](https://istio.io/).
|
||||
Please see the documentation for your service mesh. Generally, the setup would be similar to the description
|
||||
above for defining an ingress, expect the service mesh would probably provide both the ingress with TLS termination
|
||||
and the service.
|
||||
|
||||
For either of the first two options mentioned above, if you installed using `ytt` then you can use
|
||||
the related `service_*` options from [deploy/supervisor/values.yml](values.yaml) to create a Service.
|
||||
If you installed using `install-supervisor.yaml` then you can create
|
||||
the Service separately after installing the Supervisor app. There is no `Ingress` included in the `ytt` templates,
|
||||
so if you choose to use an Ingress then you'll need to create that separately after installing the Supervisor app.
|
||||
|
||||
#### Example: Using a LoadBalancer Service
|
||||
|
||||
This is an example of creating a LoadBalancer Service to expose port 8443 of the Supervisor app outside the cluster.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: pinniped-supervisor-loadbalancer
|
||||
# Assuming that this is the namespace where the supervisor was installed. This is the default in install-supervisor.yaml.
|
||||
namespace: pinniped-supervisor
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
# Assuming that this is how the supervisor pods are labeled. This is the default in install-supervisor.yaml.
|
||||
app: pinniped-supervisor
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
targetPort: 8443
|
||||
```
|
||||
|
||||
#### Example: Using a NodePort Service
|
||||
|
||||
A NodePort Service exposes the app as a port on the nodes of the cluster.
|
||||
|
||||
This is convenient for use with kind clusters, because kind can
|
||||
[expose node ports as localhost ports on the host machine](https://kind.sigs.k8s.io/docs/user/configuration/#extra-port-mappings)
|
||||
without requiring an Ingress, although
|
||||
[kind also supports several Ingress Controllers](https://kind.sigs.k8s.io/docs/user/ingress).
|
||||
|
||||
A NodePort Service could also be used behind an Ingress which is terminating TLS.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: pinniped-supervisor-nodeport
|
||||
# Assuming that this is the namespace where the supervisor was installed. This is the default in install-supervisor.yaml.
|
||||
namespace: pinniped-supervisor
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
# Assuming that this is how the supervisor pods are labeled. This is the default in install-supervisor.yaml.
|
||||
app: pinniped-supervisor
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
nodePort: 31234 # This is the port that you would forward to the kind host. Or omit this key for a random port.
|
||||
```
|
||||
|
||||
### Configuring the Supervisor to Act as an OIDC Provider
|
||||
|
||||
The Supervisor can be configured as an OIDC provider by creating `OIDCProvider` resources
|
||||
in the same namespace where the Supervisor app was installed. For example:
|
||||
|
||||
```yaml
|
||||
apiVersion: config.supervisor.pinniped.dev/v1alpha1
|
||||
kind: OIDCProvider
|
||||
metadata:
|
||||
name: my-provider
|
||||
# Assuming that this is the namespace where the supervisor was installed. This is the default in install-supervisor.yaml.
|
||||
namespace: pinniped-supervisor
|
||||
spec:
|
||||
# The hostname would typically match the DNS name of the public ingress or load balancer for the cluster.
|
||||
# Any path can be specified, which allows a single hostname to have multiple different issuers. The path is optional.
|
||||
issuer: https://my-issuer.example.com/any/path
|
||||
|
||||
# Optionally configure the name of a Secret in the same namespace, of type `kubernetes.io/tls`,
|
||||
# which contains the TLS serving certificate for the HTTPS endpoints served by this OIDC Provider.
|
||||
tls:
|
||||
secretName: my-tls-cert-secret
|
||||
```
|
||||
|
||||
#### Configuring TLS for the Supervisor OIDC Endpoints
|
||||
|
||||
If you have terminated TLS outside the app, for example using an Ingress with TLS certificates, then you do not need to
|
||||
configure TLS certificates on the OIDCProvider.
|
||||
|
||||
If you are using a LoadBalancer Service to expose the Supervisor app outside your cluster, then you will
|
||||
also need to configure the Supervisor app to terminate TLS. There are two places to configure TLS certificates:
|
||||
|
||||
1. Each `OIDCProvider` can be configured with TLS certificates, using the `spec.tls.secretName` field.
|
||||
|
||||
1. The default TLS certificate for all OIDC providers can be configured by creating a Secret called
|
||||
`pinniped-supervisor-default-tls-certificate` in the same namespace in which the Supervisor was installed.
|
||||
|
||||
The default TLS certificate will be used for all OIDC providers which did not declare a `spec.tls.secretName`.
|
||||
Also, the `spec.tls.secretName` will be ignored for incoming requests to the OIDC endpoints
|
||||
that use an IP address as the host, so those requests will always present the default TLS certificates
|
||||
to the client. When the request includes the hostname, and that hostname matches the hostname of an `Issuer`,
|
||||
then the TLS certificate defined by the `spec.tls.secretName` will be used. If that issuer did not
|
||||
define `spec.tls.secretName` then the default TLS certificate will be used. If neither exists,
|
||||
then the client will get a TLS error because the server will not present any TLS certificate.
|
||||
|
||||
It is recommended that you have a DNS entry for your load balancer or Ingress, and that you configure the
|
||||
OIDC provider's `Issuer` using that DNS hostname, and that the TLS certificate for that provider also
|
||||
covers that same hostname.
|
||||
|
||||
You can create the certificate Secrets however you like, for example you could use [cert-manager](https://cert-manager.io/)
|
||||
or `kubectl create secret tls`.
|
||||
Keep in mind that your users will load some of these endpoints in their web browsers, so the TLS certificates
|
||||
should be signed by a Certificate Authority that will be trusted by their browsers.
|
||||
See [the how-to guide for details](https://pinniped.dev/docs/howto/install-supervisor/).
|
||||
|
||||
@@ -6,20 +6,22 @@ metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.4.0
|
||||
creationTimestamp: null
|
||||
name: oidcproviders.config.supervisor.pinniped.dev
|
||||
name: federationdomains.config.supervisor.pinniped.dev
|
||||
spec:
|
||||
group: config.supervisor.pinniped.dev
|
||||
names:
|
||||
kind: OIDCProvider
|
||||
listKind: OIDCProviderList
|
||||
plural: oidcproviders
|
||||
singular: oidcprovider
|
||||
categories:
|
||||
- pinniped
|
||||
kind: FederationDomain
|
||||
listKind: FederationDomainList
|
||||
plural: federationdomains
|
||||
singular: federationdomain
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: OIDCProvider describes the configuration of an OIDC provider.
|
||||
description: FederationDomain describes the configuration of an OIDC provider.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
@@ -48,14 +50,14 @@ spec:
|
||||
minLength: 1
|
||||
type: string
|
||||
tls:
|
||||
description: TLS configures how this OIDCProvider is served over Transport
|
||||
Layer Security (TLS).
|
||||
description: TLS configures how this FederationDomain is served over
|
||||
Transport Layer Security (TLS).
|
||||
properties:
|
||||
secretName:
|
||||
description: "SecretName is an optional name of a Secret in the
|
||||
same namespace, of type `kubernetes.io/tls`, which contains
|
||||
the TLS serving certificate for the HTTPS endpoints served by
|
||||
this OIDCProvider. When provided, the TLS Secret named here
|
||||
this FederationDomain. When provided, the TLS Secret named here
|
||||
must contain keys named `tls.crt` and `tls.key` that contain
|
||||
the certificate and private key to use for TLS. \n Server Name
|
||||
Indication (SNI) is an extension to the Transport Layer Security
|
||||
@@ -79,17 +81,6 @@ spec:
|
||||
status:
|
||||
description: Status of the OIDC provider.
|
||||
properties:
|
||||
jwksSecret:
|
||||
description: JWKSSecret holds the name of the secret in which this
|
||||
OIDC Provider's signing/verification keys are stored. If it is empty,
|
||||
then the signing/verification keys are either unknown or they don't
|
||||
exist.
|
||||
properties:
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
type: object
|
||||
lastUpdateTime:
|
||||
description: LastUpdateTime holds the time at which the Status was
|
||||
last updated. It is a pointer to get around some undesirable behavior
|
||||
@@ -99,6 +90,51 @@ spec:
|
||||
message:
|
||||
description: Message provides human-readable details about the Status.
|
||||
type: string
|
||||
secrets:
|
||||
description: Secrets contains information about this OIDC Provider's
|
||||
secrets.
|
||||
properties:
|
||||
jwks:
|
||||
description: JWKS holds the name of the corev1.Secret in which
|
||||
this OIDC Provider's signing/verification keys are stored. If
|
||||
it is empty, then the signing/verification keys are either unknown
|
||||
or they don't exist.
|
||||
properties:
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
type: object
|
||||
stateEncryptionKey:
|
||||
description: StateSigningKey holds the name of the corev1.Secret
|
||||
in which this OIDC Provider's key for encrypting state parameters
|
||||
is stored.
|
||||
properties:
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
type: object
|
||||
stateSigningKey:
|
||||
description: StateSigningKey holds the name of the corev1.Secret
|
||||
in which this OIDC Provider's key for signing state parameters
|
||||
is stored.
|
||||
properties:
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
type: object
|
||||
tokenSigningKey:
|
||||
description: TokenSigningKey holds the name of the corev1.Secret
|
||||
in which this OIDC Provider's key for signing tokens is stored.
|
||||
properties:
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
status:
|
||||
description: Status holds an enum that describes the state of this
|
||||
OIDC Provider. Note that this Status can represent success or failure.
|
||||
@@ -106,6 +142,7 @@ spec:
|
||||
- Success
|
||||
- Duplicate
|
||||
- Invalid
|
||||
- SameIssuerHostMustUseSameSecret
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
@@ -113,6 +150,8 @@ spec:
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
@@ -1,9 +1,9 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
#@ load("@ytt:json", "json")
|
||||
#@ load("helpers.lib.yaml", "defaultLabel", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix")
|
||||
#@ load("helpers.lib.yaml", "defaultLabel", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix", "getAndValidateLogLevel")
|
||||
|
||||
#@ if not data.values.into_namespace:
|
||||
---
|
||||
@@ -30,9 +30,13 @@ metadata:
|
||||
data:
|
||||
#@yaml/text-templated-strings
|
||||
pinniped.yaml: |
|
||||
apiGroupSuffix: (@= data.values.api_group_suffix @)
|
||||
names:
|
||||
defaultTLSCertificateSecret: (@= defaultResourceNameWithSuffix("default-tls-certificate") @)
|
||||
labels: (@= json.encode(labels()).rstrip() @)
|
||||
(@ if data.values.log_level: @)
|
||||
logLevel: (@= getAndValidateLogLevel() @)
|
||||
(@ end @)
|
||||
---
|
||||
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
|
||||
apiVersion: v1
|
||||
@@ -61,8 +65,8 @@ spec:
|
||||
labels: #@ defaultLabel()
|
||||
spec:
|
||||
securityContext:
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
runAsUser: #@ data.values.run_as_user
|
||||
runAsGroup: #@ data.values.run_as_group
|
||||
serviceAccountName: #@ defaultResourceName()
|
||||
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
|
||||
imagePullSecrets:
|
||||
@@ -129,6 +133,9 @@ spec:
|
||||
- path: "namespace"
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- path: "name"
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
#! This will help make sure our multiple pods run on different nodes, making
|
||||
#! our deployment "more" "HA".
|
||||
affinity:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
@@ -12,6 +12,10 @@
|
||||
#@ return data.values.app_name + "-" + suffix
|
||||
#@ end
|
||||
|
||||
#@ def pinnipedDevAPIGroupWithPrefix(prefix):
|
||||
#@ return prefix + "." + data.values.api_group_suffix
|
||||
#@ end
|
||||
|
||||
#@ def namespace():
|
||||
#@ if data.values.into_namespace:
|
||||
#@ return data.values.into_namespace
|
||||
@@ -28,3 +32,11 @@ app: #@ data.values.app_name
|
||||
_: #@ template.replace(defaultLabel())
|
||||
_: #@ template.replace(data.values.custom_labels)
|
||||
#@ end
|
||||
|
||||
#@ def getAndValidateLogLevel():
|
||||
#@ log_level = data.values.log_level
|
||||
#@ if log_level != "info" and log_level != "debug" and log_level != "trace" and log_level != "all":
|
||||
#@ fail("log_level '" + log_level + "' is invalid")
|
||||
#@ end
|
||||
#@ return log_level
|
||||
#@ end
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.4.0
|
||||
creationTimestamp: null
|
||||
name: oidcidentityproviders.idp.supervisor.pinniped.dev
|
||||
spec:
|
||||
group: idp.supervisor.pinniped.dev
|
||||
names:
|
||||
categories:
|
||||
- pinniped
|
||||
- pinniped-idp
|
||||
- pinniped-idps
|
||||
kind: OIDCIdentityProvider
|
||||
listKind: OIDCIdentityProviderList
|
||||
plural: oidcidentityproviders
|
||||
singular: oidcidentityprovider
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.issuer
|
||||
name: Issuer
|
||||
type: string
|
||||
- jsonPath: .status.phase
|
||||
name: Status
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: OIDCIdentityProvider describes the configuration of an upstream
|
||||
OpenID Connect identity provider.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: Spec for configuring the identity provider.
|
||||
properties:
|
||||
authorizationConfig:
|
||||
description: AuthorizationConfig holds information about how to form
|
||||
the OAuth2 authorization request parameters to be used with this
|
||||
OIDC identity provider.
|
||||
properties:
|
||||
additionalScopes:
|
||||
description: AdditionalScopes are the scopes in addition to "openid"
|
||||
that will be requested as part of the authorization request
|
||||
flow with an OIDC identity provider. By default only the "openid"
|
||||
scope will be requested.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
claims:
|
||||
description: Claims provides the names of token claims that will be
|
||||
used when inspecting an identity from this OIDC identity provider.
|
||||
properties:
|
||||
groups:
|
||||
description: Groups provides the name of the token claim that
|
||||
will be used to ascertain the groups to which an identity belongs.
|
||||
type: string
|
||||
username:
|
||||
description: Username provides the name of the token claim that
|
||||
will be used to ascertain an identity's username.
|
||||
type: string
|
||||
type: object
|
||||
client:
|
||||
description: OIDCClient contains OIDC client information to be used
|
||||
used with this OIDC identity provider.
|
||||
properties:
|
||||
secretName:
|
||||
description: SecretName contains the name of a namespace-local
|
||||
Secret object that provides the clientID and clientSecret for
|
||||
an OIDC client. If only the SecretName is specified in an OIDCClient
|
||||
struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client"
|
||||
with keys "clientID" and "clientSecret".
|
||||
type: string
|
||||
required:
|
||||
- secretName
|
||||
type: object
|
||||
issuer:
|
||||
description: Issuer is the issuer URL of this OIDC identity provider,
|
||||
i.e., where to fetch /.well-known/openid-configuration.
|
||||
minLength: 1
|
||||
pattern: ^https://
|
||||
type: string
|
||||
tls:
|
||||
description: TLS configuration for discovery/JWKS requests to the
|
||||
issuer.
|
||||
properties:
|
||||
certificateAuthorityData:
|
||||
description: X.509 Certificate Authority (base64-encoded PEM bundle).
|
||||
If omitted, a default set of system roots will be trusted.
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- client
|
||||
- issuer
|
||||
type: object
|
||||
status:
|
||||
description: Status of the identity provider.
|
||||
properties:
|
||||
conditions:
|
||||
description: Represents the observations of an identity provider's
|
||||
current state.
|
||||
items:
|
||||
description: Condition status of a resource (mirrored from the metav1.Condition
|
||||
type added in Kubernetes 1.19). In a future API version we can
|
||||
switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: lastTransitionTime is the last time the condition
|
||||
transitioned from one status to another. This should be when
|
||||
the underlying condition changed. If that is not known, then
|
||||
using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: message is a human readable message indicating
|
||||
details about the transition. This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: observedGeneration represents the .metadata.generation
|
||||
that the condition was set based upon. For instance, if .metadata.generation
|
||||
is currently 12, but the .status.conditions[x].observedGeneration
|
||||
is 9, the condition is out of date with respect to the current
|
||||
state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: reason contains a programmatic identifier indicating
|
||||
the reason for the condition's last transition. Producers
|
||||
of specific condition types may define expected values and
|
||||
meanings for this field, and whether the values are considered
|
||||
a guaranteed API. The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
--- Many .condition.type values are consistent across resources
|
||||
like Available, but because arbitrary conditions can be useful
|
||||
(see .node.status.conditions), the ability to deconflict is
|
||||
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
phase:
|
||||
default: Pending
|
||||
description: Phase summarizes the overall status of the OIDCIdentityProvider.
|
||||
enum:
|
||||
- Pending
|
||||
- Ready
|
||||
- Error
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
@@ -1,8 +1,8 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix")
|
||||
#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix", "pinnipedDevAPIGroupWithPrefix")
|
||||
|
||||
#! Give permission to various objects within the app's own namespace
|
||||
---
|
||||
@@ -16,9 +16,30 @@ rules:
|
||||
- apiGroups: [""]
|
||||
resources: [secrets]
|
||||
verbs: [create, get, list, patch, update, watch, delete]
|
||||
- apiGroups: [config.supervisor.pinniped.dev]
|
||||
resources: [oidcproviders]
|
||||
verbs: [update, get, list, watch]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("config.supervisor")
|
||||
resources: [federationdomains]
|
||||
verbs: [get, list, watch]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("config.supervisor")
|
||||
resources: [federationdomains/status]
|
||||
verbs: [get, patch, update]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")
|
||||
resources: [oidcidentityproviders]
|
||||
verbs: [get, list, watch]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")
|
||||
resources: [oidcidentityproviders/status]
|
||||
verbs: [get, patch, update]
|
||||
#! We want to be able to read pods/replicasets/deployment so we can learn who our deployment is to set
|
||||
#! as an owner reference.
|
||||
- apiGroups: [""]
|
||||
resources: [pods]
|
||||
verbs: [get]
|
||||
- apiGroups: [apps]
|
||||
resources: [replicasets,deployments]
|
||||
verbs: [get]
|
||||
---
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@data/values
|
||||
@@ -25,7 +25,7 @@ custom_labels: {} #! e.g. {myCustomLabelName: myCustomLabelValue, otherCustomLab
|
||||
replicas: 2
|
||||
|
||||
#! Specify either an image_digest or an image_tag. If both are given, only image_digest will be used.
|
||||
image_repo: docker.io/getpinniped/pinniped-server
|
||||
image_repo: projects.registry.vmware.com/pinniped/pinniped-server
|
||||
image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8
|
||||
image_tag: latest
|
||||
|
||||
@@ -52,3 +52,16 @@ service_https_clusterip_port: #! when specified, creates a ClusterIP Service wit
|
||||
#! Ignored unless service_http_loadbalancer_port and/or service_https_loadbalancer_port are provided.
|
||||
#! Optional.
|
||||
service_loadbalancer_ip: #! e.g. 1.2.3.4
|
||||
|
||||
#! Specify the verbosity of logging: info ("nice to know" information), debug (developer
|
||||
#! information), trace (timing information), all (kitchen sink).
|
||||
log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs.
|
||||
|
||||
run_as_user: 1001 #! run_as_user specifies the user ID that will own the process
|
||||
run_as_group: 1001 #! run_as_group specifies the group ID that will own the process
|
||||
|
||||
#! Specify the API group suffix for all Pinniped API groups. By default, this is set to
|
||||
#! pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev,
|
||||
#! authentication.concierge.pinniped.dev, etc. As an example, if this is set to tuna.io, then
|
||||
#! Pinniped API groups will look like foo.tuna.io. authentication.concierge.tuna.io, etc.
|
||||
api_group_suffix: pinniped.dev
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:overlay", "overlay")
|
||||
#@ load("helpers.lib.yaml", "labels")
|
||||
#@ load("helpers.lib.yaml", "labels", "pinnipedDevAPIGroupWithPrefix")
|
||||
#@ load("@ytt:data", "data")
|
||||
|
||||
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"oidcproviders.config.supervisor.pinniped.dev"}}), expects=1
|
||||
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"federationdomains.config.supervisor.pinniped.dev"}}), expects=1
|
||||
---
|
||||
metadata:
|
||||
#@overlay/match missing_ok=True
|
||||
labels: #@ labels()
|
||||
name: #@ pinnipedDevAPIGroupWithPrefix("federationdomains.config.supervisor")
|
||||
spec:
|
||||
group: #@ pinnipedDevAPIGroupWithPrefix("config.supervisor")
|
||||
|
||||
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"oidcidentityproviders.idp.supervisor.pinniped.dev"}}), expects=1
|
||||
---
|
||||
metadata:
|
||||
#@overlay/match missing_ok=True
|
||||
labels: #@ labels()
|
||||
name: #@ pinnipedDevAPIGroupWithPrefix("oidcidentityproviders.idp.supervisor")
|
||||
spec:
|
||||
group: #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor")
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# Architecture
|
||||
|
||||
The principal purpose of Pinniped is to allow users to access Kubernetes
|
||||
clusters. Pinniped hopes to enable this access across a wide range of Kubernetes
|
||||
environments with zero configuration.
|
||||
|
||||
This integration is implemented using a credential exchange API which takes as
|
||||
input a credential from the external IDP and returns a credential which is understood by the host
|
||||
Kubernetes cluster.
|
||||
|
||||
<img src="img/pinniped_architecture.svg" alt="Pinniped Architecture Sketch" width="300px"/>
|
||||
|
||||
Pinniped supports various IDP types and implements different integration strategies
|
||||
for various Kubernetes distributions to make authentication possible.
|
||||
|
||||
## Supported Kubernetes Cluster Types
|
||||
|
||||
Pinniped supports the following types of Kubernetes clusters:
|
||||
|
||||
- Clusters where the Kube Controller Manager pod is accessible from Pinniped's pods.
|
||||
|
||||
Support for other types of Kubernetes distributions is coming soon.
|
||||
|
||||
## External Identity Provider Integrations
|
||||
|
||||
Pinniped will consume identity from one or more external identity providers
|
||||
(IDPs). Administrators will configure external IDPs via Kubernetes custom
|
||||
resources allowing Pinniped to be managed using GitOps and standard Kubernetes tools.
|
||||
|
||||
Pinniped supports the following external IDP types.
|
||||
|
||||
1. Any webhook which implements the
|
||||
[Kubernetes TokenReview API](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication).
|
||||
|
||||
In addition to allowing the integration of any existing IDP which implements this API, webhooks also
|
||||
serve as an extension point for Pinniped by allowing for integration of arbitrary custom authenticators.
|
||||
While a custom implementation may be in any language or framework, this project provides a
|
||||
sample implementation in Golang. See the `ServeHTTP` method of
|
||||
[cmd/local-user-authenticator/main.go](../cmd/local-user-authenticator/main.go).
|
||||
|
||||
More IDP types are coming soon.
|
||||
|
||||
## Cluster Integration Strategies
|
||||
|
||||
Pinniped will issue a cluster credential by leveraging cluster-specific
|
||||
functionality. In the near term, cluster integrations will happen via different
|
||||
cluster-specific flows depending on the type of cluster. In the longer term,
|
||||
Pinniped hopes to contribute and leverage upstream Kubernetes extension points that
|
||||
cleanly enable this integration.
|
||||
|
||||
Pinniped supports the following cluster integration strategies.
|
||||
|
||||
1. Pinniped hosts a credential exchange API endpoint via a Kubernetes aggregated API server.
|
||||
This API returns a new cluster-specific credential using the cluster's signing keypair to
|
||||
issue short-lived cluster certificates. (In the future, when the Kubernetes CSR API
|
||||
provides a way to issue short-lived certificates, then the Pinniped credential exchange API
|
||||
will use that instead of using the cluster's signing keypair.)
|
||||
|
||||
More cluster integration strategies are coming soon, which will allow Pinniped to
|
||||
support more Kubernetes cluster types.
|
||||
|
||||
## `kubectl` Integration
|
||||
|
||||
With any of the above IDPs and integration strategies, `kubectl` commands receive the
|
||||
cluster-specific credential via a
|
||||
[Kubernetes client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
|
||||
Users may use the Pinniped CLI as the credential plugin, or they may use any proprietary CLI
|
||||
built with the [Pinniped Go client library](../generated).
|
||||
|
||||
## Example Cluster Authentication Sequence Diagram
|
||||
|
||||
This diagram demonstrates using `kubectl get pods` with the Pinniped CLI configured as the credential plugin,
|
||||
and with a webhook IDP configured as the identity provider for the Pinniped server.
|
||||
|
||||

|
||||
198
doc/demo.md
198
doc/demo.md
@@ -1,198 +0,0 @@
|
||||
# Trying Pinniped
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A Kubernetes cluster of a type supported by Pinniped as described in [doc/architecture.md](../doc/architecture.md).
|
||||
|
||||
Don't have a cluster handy? Consider using [kind](https://kind.sigs.k8s.io/) on your local machine.
|
||||
See below for an example of using kind.
|
||||
|
||||
1. An identity provider of a type supported by Pinniped as described in [doc/architecture.md](../doc/architecture.md).
|
||||
|
||||
Don't have an identity provider of a type supported by Pinniped handy? No problem, there is a demo identity provider
|
||||
available. Start by installing local-user-authenticator on the same cluster where you would like to try Pinniped
|
||||
by following the directions in [deploy/local-user-authenticator/README.md](../deploy/local-user-authenticator/README.md).
|
||||
See below for an example of deploying this on kind.
|
||||
|
||||
1. A kubeconfig where the current context points to the cluster and has admin-like
|
||||
privileges on that cluster.
|
||||
|
||||
## Overview
|
||||
|
||||
Installing and trying Pinniped on any cluster will consist of the following general steps. See the next section below
|
||||
for a more specific example of installing onto a local kind cluster, including the exact commands to use for that case.
|
||||
|
||||
1. Install Pinniped. See [deploy/concierge/README.md](../deploy/concierge/README.md).
|
||||
1. Download the Pinniped CLI from [Pinniped's github Releases page](https://github.com/vmware-tanzu/pinniped/releases/latest).
|
||||
1. Generate a kubeconfig using the Pinniped CLI. Run `pinniped get-kubeconfig --help` for more information.
|
||||
1. Run `kubectl` commands using the generated kubeconfig. Pinniped will automatically be used for authentication during those commands.
|
||||
|
||||
## Example of Deploying on kind
|
||||
|
||||
[kind](https://kind.sigs.k8s.io) is a tool for creating and managing Kubernetes clusters on your local machine
|
||||
which uses Docker containers as the cluster's "nodes". This is a convenient way to try out Pinniped on a local
|
||||
non-production cluster.
|
||||
|
||||
The following steps will deploy the latest release of Pinniped on kind using the local-user-authenticator component
|
||||
as the identity provider.
|
||||
|
||||
<!-- The following image was uploaded to GitHub's CDN using this awesome trick: https://gist.github.com/vinkla/dca76249ba6b73c5dd66a4e986df4c8d -->
|
||||
<p align="center" width="100%">
|
||||
<img
|
||||
src="https://user-images.githubusercontent.com/25013435/95272990-b2ea9780-07f6-11eb-994d-872e3cb68457.gif"
|
||||
alt="Pinniped Installation Demo"
|
||||
width="80%"
|
||||
/>
|
||||
</p>
|
||||
|
||||
1. Install the tools required for the following steps.
|
||||
|
||||
- [Install kind](https://kind.sigs.k8s.io/docs/user/quick-start/), if not already installed. e.g. `brew install kind` on MacOS.
|
||||
|
||||
- kind depends on Docker. If not already installed, [install Docker](https://docs.docker.com/get-docker/), e.g. `brew cask install docker` on MacOS.
|
||||
|
||||
- This demo requires `kubectl`, which comes with Docker, or can be [installed separately](https://kubernetes.io/docs/tasks/tools/install-kubectl/).
|
||||
|
||||
- This demo requires a tool capable of generating a `bcrypt` hash in order to interact with
|
||||
the webhook. The example below uses `htpasswd`, which is installed on most macOS systems, and can be
|
||||
installed on some Linux systems via the `apache2-utils` package (e.g., `apt-get install
|
||||
apache2-utils`).
|
||||
|
||||
- One of the steps below optionally uses `jq` to help find the latest release version number. It is not required.
|
||||
Install `jq` if you would like, e.g. `brew install jq` on MacOS.
|
||||
|
||||
1. Create a new Kubernetes cluster using `kind create cluster`. Optionally provide a cluster name using the `--name` flag.
|
||||
kind will automatically update your kubeconfig to point to the new cluster as a user with admin-like permissions.
|
||||
|
||||
1. Query GitHub's API for the git tag of the latest Pinniped
|
||||
[release](https://github.com/vmware-tanzu/pinniped/releases/latest).
|
||||
|
||||
```bash
|
||||
pinniped_version=$(curl https://api.github.com/repos/vmware-tanzu/pinniped/releases/latest -s | jq .name -r)
|
||||
```
|
||||
|
||||
Alternatively, [any release version](https://github.com/vmware-tanzu/pinniped/releases)
|
||||
number can be manually selected.
|
||||
|
||||
```bash
|
||||
# Example of manually choosing a release version...
|
||||
pinniped_version=v0.2.0
|
||||
```
|
||||
|
||||
1. Deploy the local-user-authenticator app. This is a demo identity provider. In production, you would use your
|
||||
real identity provider, and therefore would not need to deploy or configure local-user-authenticator.
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/download/$pinniped_version/install-local-user-authenticator.yaml
|
||||
```
|
||||
|
||||
The `install-local-user-authenticator.yaml` file includes the default deployment options.
|
||||
If you would prefer to customize the available options, please
|
||||
see [deploy/local-user-authenticator/README.md](../deploy/local-user-authenticator/README.md)
|
||||
for instructions on how to deploy using `ytt`.
|
||||
|
||||
1. Create a test user named `pinny-the-seal` in the local-user-authenticator identity provider.
|
||||
|
||||
```bash
|
||||
kubectl create secret generic pinny-the-seal \
|
||||
--namespace local-user-authenticator \
|
||||
--from-literal=groups=group1,group2 \
|
||||
--from-literal=passwordHash=$(htpasswd -nbBC 10 x password123 | sed -e "s/^x://")
|
||||
```
|
||||
|
||||
1. Fetch the auto-generated CA bundle for the local-user-authenticator's HTTP TLS endpoint.
|
||||
|
||||
```bash
|
||||
kubectl get secret local-user-authenticator-tls-serving-certificate --namespace local-user-authenticator \
|
||||
-o jsonpath={.data.caCertificate} \
|
||||
| tee /tmp/local-user-authenticator-ca-base64-encoded
|
||||
```
|
||||
|
||||
1. Deploy Pinniped.
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/download/$pinniped_version/install-pinniped-concierge.yaml
|
||||
```
|
||||
|
||||
The `install-pinniped-concierge.yaml` file includes the default deployment options.
|
||||
If you would prefer to customize the available options, please see [deploy/concierge/README.md](../deploy/concierge/README.md)
|
||||
for instructions on how to deploy using `ytt`.
|
||||
|
||||
1. Create a `WebhookAuthenticator` object to configure Pinniped to authenticate using local-user-authenticator.
|
||||
|
||||
```bash
|
||||
cat <<EOF | kubectl create --namespace pinniped -f -
|
||||
apiVersion: authentication.concierge.pinniped.dev/v1alpha1
|
||||
kind: WebhookAuthenticator
|
||||
metadata:
|
||||
name: local-user-authenticator
|
||||
spec:
|
||||
endpoint: https://local-user-authenticator.local-user-authenticator.svc/authenticate
|
||||
tls:
|
||||
certificateAuthorityData: $(cat /tmp/local-user-authenticator-ca-base64-encoded)
|
||||
EOF
|
||||
```
|
||||
|
||||
1. Download the latest version of the Pinniped CLI binary for your platform
|
||||
from Pinniped's [latest release](https://github.com/vmware-tanzu/pinniped/releases/latest).
|
||||
|
||||
1. Move the Pinniped CLI binary to your preferred filename and directory. Add the executable bit,
|
||||
e.g. `chmod +x /usr/local/bin/pinniped`.
|
||||
|
||||
1. Generate a kubeconfig for the current cluster. Use `--token` to include a token which should
|
||||
allow you to authenticate as the user that you created above.
|
||||
|
||||
```bash
|
||||
pinniped get-kubeconfig --token "pinny-the-seal:password123" --authenticator-type webhook --authenticator-name local-user-authenticator > /tmp/pinniped-kubeconfig
|
||||
```
|
||||
|
||||
If you are using MacOS, you may get an error dialog that says
|
||||
`“pinniped” cannot be opened because the developer cannot be verified`. Cancel this dialog, open System Preferences,
|
||||
click on Security & Privacy, and click the Allow Anyway button next to the Pinniped message.
|
||||
Run the above command again and another dialog will appear saying
|
||||
`macOS cannot verify the developer of “pinniped”. Are you sure you want to open it?`.
|
||||
Click Open to allow the command to proceed.
|
||||
|
||||
Note that the above command will print a warning to the screen. You can ignore this warning.
|
||||
Pinniped tries to auto-discover the URL for the Kubernetes API server, but it is not able
|
||||
to do so on kind clusters. The warning is just letting you know that the Pinniped CLI decided
|
||||
to ignore the auto-discovery URL and instead use the URL from your existing kubeconfig.
|
||||
|
||||
1. Try using the generated kubeconfig to issue arbitrary `kubectl` commands as
|
||||
the `pinny-the-seal` user.
|
||||
|
||||
```bash
|
||||
kubectl --kubeconfig /tmp/pinniped-kubeconfig get pods -n pinniped
|
||||
```
|
||||
|
||||
Because this user has no RBAC permissions on this cluster, the previous command
|
||||
results in the error `Error from server (Forbidden): pods is forbidden: User "pinny-the-seal" cannot list resource "pods" in API group "" in the namespace "pinniped"`.
|
||||
However, this does prove that you are authenticated and acting as the `pinny-the-seal` user.
|
||||
|
||||
1. As the admin user, create RBAC rules for the test user to give them permissions to perform actions on the cluster.
|
||||
For example, grant the test user permission to view all cluster resources.
|
||||
|
||||
```bash
|
||||
kubectl create clusterrolebinding pinny-can-read --clusterrole view --user pinny-the-seal
|
||||
```
|
||||
|
||||
1. Use the generated kubeconfig to issue arbitrary `kubectl` commands as the `pinny-the-seal` user.
|
||||
|
||||
```bash
|
||||
kubectl --kubeconfig /tmp/pinniped-kubeconfig get pods -n pinniped
|
||||
```
|
||||
|
||||
The user has permission to list pods, so the command succeeds this time.
|
||||
Pinniped has provided authentication into the cluster for your `kubectl` command! 🎉
|
||||
|
||||
1. Carry on issuing as many `kubectl` commands as you'd like as the `pinny-the-seal` user.
|
||||
Each invocation will use Pinniped for authentication.
|
||||
You may find it convenient to set the `KUBECONFIG` environment variable rather than passing `--kubeconfig` to each invocation.
|
||||
|
||||
```bash
|
||||
export KUBECONFIG=/tmp/pinniped-kubeconfig
|
||||
kubectl get namespaces
|
||||
kubectl get pods -A
|
||||
```
|
||||
|
||||
1. Profit! 💰
|
||||
@@ -1,12 +0,0 @@
|
||||
# `doc/img` README
|
||||
|
||||
## How to Update these Images
|
||||
|
||||
- [pinniped.svg](pinniped.svg) was generated using [`plantuml`](https://plantuml.com/).
|
||||
To regenerate the image, run `plantuml -tsvg pinniped.txt` from this directory.
|
||||
|
||||
- [pinniped_architecture.svg](pinniped_architecture.svg) was created on [draw.io](https://draw.io).
|
||||
It can be opened again for editing on that site by choosing "File" -> "Open from" -> "Device".
|
||||
Because it includes embedded icons it should be exported using "File" -> "Export as" -> "SVG",
|
||||
with the "Transparent Background", "Embed Images", and "Include a copy of my diagram" options
|
||||
checked. The icons in this diagram are from their "CAE" shapes set.
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 79 KiB |
32
doc/scope.md
32
doc/scope.md
@@ -1,32 +0,0 @@
|
||||
# Project Scope
|
||||
|
||||
The Pinniped project is guided by the following principles.
|
||||
* Pinniped lets you plug any external identitiy providers into
|
||||
Kubernetes. These integrations follow enterprise-grade security principles.
|
||||
* Pinniped is easy to install and use on any Kubernetes cluster via
|
||||
distribution-specific integration mechanisms.
|
||||
* Pinniped uses a declarative configuration via Kubernetes APIs.
|
||||
* Pinniped provides optimal user experience when authenticating to many
|
||||
clusters at one time.
|
||||
* Pinniped provides enterprise-grade security posture via secure defaults and
|
||||
revocable or very short-lived credentials.
|
||||
* Where possible, Pinniped will contribute ideas and code to upstream
|
||||
Kubernetes.
|
||||
|
||||
When contributing to Pinniped, please consider whether your contribution follows
|
||||
these guiding principles.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
The following items are out of scope for the Pinniped project.
|
||||
* Authorization.
|
||||
* Standalone identity provider for general use.
|
||||
* Machine-to-machine (service) identity.
|
||||
* Running outside of Kubernetes.
|
||||
|
||||
## Roadmap
|
||||
|
||||
More details coming soon!
|
||||
|
||||
For more details on proposing features and bugs, check out our
|
||||
[contributing](../CONTRIBUTING.md) doc.
|
||||
595
generated/1.17/README.adoc
generated
595
generated/1.17/README.adoc
generated
@@ -6,9 +6,11 @@
|
||||
|
||||
.Packages
|
||||
- xref:{anchor_prefix}-authentication-concierge-pinniped-dev-v1alpha1[$$authentication.concierge.pinniped.dev/v1alpha1$$]
|
||||
- xref:{anchor_prefix}-config-concierge-pinniped-dev-config[$$config.concierge.pinniped.dev/config$$]
|
||||
- xref:{anchor_prefix}-config-concierge-pinniped-dev-v1alpha1[$$config.concierge.pinniped.dev/v1alpha1$$]
|
||||
- xref:{anchor_prefix}-config-supervisor-pinniped-dev-v1alpha1[$$config.supervisor.pinniped.dev/v1alpha1$$]
|
||||
- xref:{anchor_prefix}-identity-concierge-pinniped-dev-identity[$$identity.concierge.pinniped.dev/identity$$]
|
||||
- xref:{anchor_prefix}-identity-concierge-pinniped-dev-v1alpha1[$$identity.concierge.pinniped.dev/v1alpha1$$]
|
||||
- xref:{anchor_prefix}-idp-supervisor-pinniped-dev-v1alpha1[$$idp.supervisor.pinniped.dev/v1alpha1$$]
|
||||
- xref:{anchor_prefix}-login-concierge-pinniped-dev-v1alpha1[$$login.concierge.pinniped.dev/v1alpha1$$]
|
||||
|
||||
|
||||
@@ -22,10 +24,11 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authenticatio
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-condition"]
|
||||
==== Condition
|
||||
|
||||
Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413.
|
||||
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$]
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]
|
||||
****
|
||||
|
||||
@@ -41,13 +44,104 @@ Condition status of a resource (mirrored from the metav1.Condition type added in
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-tlsspec"]
|
||||
==== TLSSpec
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-conditionstatus"]
|
||||
==== ConditionStatus (string)
|
||||
|
||||
|
||||
Configuration for configuring TLS on various authenticators.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-condition[$$Condition$$]
|
||||
****
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticator"]
|
||||
==== JWTAuthenticator
|
||||
|
||||
JWTAuthenticator describes the configuration of a JWT authenticator.
|
||||
Upon receiving a signed JWT, a JWTAuthenticator will performs some validation on it (e.g., valid signature, existence of claims, etc.) and extract the username and groups from the token.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticatorlist[$$JWTAuthenticatorList$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`.
|
||||
|
||||
| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec[$$JWTAuthenticatorSpec$$]__ | Spec for configuring the authenticator.
|
||||
| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$]__ | Status of the authenticator.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec"]
|
||||
==== JWTAuthenticatorSpec
|
||||
|
||||
Spec for configuring a JWT authenticator.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticator[$$JWTAuthenticator$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`issuer`* __string__ | Issuer is the OIDC issuer URL that will be used to discover public signing keys. Issuer is also used to validate the "iss" JWT claim.
|
||||
| *`audience`* __string__ | Audience is the required value of the "aud" JWT claim.
|
||||
| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwttokenclaims[$$JWTTokenClaims$$]__ | Claims allows customization of the claims that will be mapped to user identity for Kubernetes access.
|
||||
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for communicating with the OIDC provider.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus"]
|
||||
==== JWTAuthenticatorStatus
|
||||
|
||||
Status of a JWT authenticator.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticator[$$JWTAuthenticator$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwttokenclaims"]
|
||||
==== JWTTokenClaims
|
||||
|
||||
JWTTokenClaims allows customization of the claims that will be mapped to user identity for Kubernetes access.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec[$$JWTAuthenticatorSpec$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`groups`* __string__ | Groups is the name of the claim which should be read to extract the user's group membership from the JWT token. When not specified, it will default to "groups".
|
||||
| *`username`* __string__ | Username is the name of the claim which should be read to extract the username from the JWT token. When not specified, it will default to "username".
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-tlsspec"]
|
||||
==== TLSSpec
|
||||
|
||||
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec[$$JWTAuthenticatorSpec$$]
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec[$$WebhookAuthenticatorSpec$$]
|
||||
****
|
||||
|
||||
@@ -111,19 +205,11 @@ Status of a webhook authenticator.
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-condition[$$Condition$$]__ | Represents the observations of the authenticator's current state.
|
||||
| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-authentication-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-config-concierge-pinniped-dev-config"]
|
||||
=== config.concierge.pinniped.dev/config
|
||||
|
||||
Package config is the internal version of the Pinniped concierge configuration API.
|
||||
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-config-concierge-pinniped-dev-v1alpha1"]
|
||||
=== config.concierge.pinniped.dev/v1alpha1
|
||||
|
||||
@@ -134,7 +220,7 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge configuration
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuer"]
|
||||
==== CredentialIssuer
|
||||
|
||||
|
||||
Describes the configuration status of a Pinniped credential issuer.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
@@ -150,6 +236,25 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge configuration
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerfrontend"]
|
||||
==== CredentialIssuerFrontend
|
||||
|
||||
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerstrategy[$$CredentialIssuerStrategy$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`type`* __FrontendType__ | Type describes which frontend mechanism clients can use with a strategy.
|
||||
| *`tokenCredentialRequestInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo[$$TokenCredentialRequestAPIInfo$$]__ | TokenCredentialRequestAPIInfo describes the parameters for the TokenCredentialRequest API on this Concierge. This field is only set when Type is "TokenCredentialRequestAPI".
|
||||
| *`impersonationProxyInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyinfo[$$ImpersonationProxyInfo$$]__ | ImpersonationProxyInfo describes the parameters for the impersonation proxy on this Concierge. This field is only set when Type is "ImpersonationProxy".
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerkubeconfiginfo"]
|
||||
==== CredentialIssuerKubeConfigInfo
|
||||
|
||||
@@ -184,7 +289,7 @@ Status of a credential issuer.
|
||||
|===
|
||||
| Field | Description
|
||||
| *`strategies`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerstrategy[$$CredentialIssuerStrategy$$] array__ | List of integration strategies that were attempted by Pinniped.
|
||||
| *`kubeConfigInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerkubeconfiginfo[$$CredentialIssuerKubeConfigInfo$$]__ | Information needed to form a valid Pinniped-based kubeconfig using this credential issuer.
|
||||
| *`kubeConfigInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerkubeconfiginfo[$$CredentialIssuerKubeConfigInfo$$]__ | Information needed to form a valid Pinniped-based kubeconfig using this credential issuer. This field is deprecated and will be removed in a future version.
|
||||
|===
|
||||
|
||||
|
||||
@@ -206,6 +311,43 @@ Status of a credential issuer.
|
||||
| *`reason`* __StrategyReason__ | Reason for the current status.
|
||||
| *`message`* __string__ | Human-readable description of the current status.
|
||||
| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#time-v1-meta[$$Time$$]__ | When the status was last checked.
|
||||
| *`frontend`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerfrontend[$$CredentialIssuerFrontend$$]__ | Frontend describes how clients can connect using this strategy.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyinfo"]
|
||||
==== ImpersonationProxyInfo
|
||||
|
||||
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerfrontend[$$CredentialIssuerFrontend$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`endpoint`* __string__ | Endpoint is the HTTPS endpoint of the impersonation proxy.
|
||||
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded PEM CA bundle of the impersonation proxy.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"]
|
||||
==== TokenCredentialRequestAPIInfo
|
||||
|
||||
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerfrontend[$$CredentialIssuerFrontend$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`server`* __string__ | Server is the Kubernetes API server URL.
|
||||
| *`certificateAuthorityData`* __string__ | CertificateAuthorityData is the base64-encoded Kubernetes API server CA bundle.
|
||||
|===
|
||||
|
||||
|
||||
@@ -217,14 +359,14 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuratio
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcprovider"]
|
||||
==== OIDCProvider
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomain"]
|
||||
==== FederationDomain
|
||||
|
||||
OIDCProvider describes the configuration of an OIDC provider.
|
||||
FederationDomain describes the configuration of an OIDC provider.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcproviderlist[$$OIDCProviderList$$]
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomainlist[$$FederationDomainList$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
@@ -232,21 +374,41 @@ OIDCProvider describes the configuration of an OIDC provider.
|
||||
| Field | Description
|
||||
| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`.
|
||||
|
||||
| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcproviderspec[$$OIDCProviderSpec$$]__ | Spec of the OIDC provider.
|
||||
| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcproviderstatus[$$OIDCProviderStatus$$]__ | Status of the OIDC provider.
|
||||
| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$]__ | Spec of the OIDC provider.
|
||||
| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$]__ | Status of the OIDC provider.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcproviderspec"]
|
||||
==== OIDCProviderSpec
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomainsecrets"]
|
||||
==== FederationDomainSecrets
|
||||
|
||||
OIDCProviderSpec is a struct that describes an OIDC Provider.
|
||||
FederationDomainSecrets holds information about this OIDC Provider's secrets.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcprovider[$$OIDCProvider$$]
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomainstatus[$$FederationDomainStatus$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`jwks`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | JWKS holds the name of the corev1.Secret in which this OIDC Provider's signing/verification keys are stored. If it is empty, then the signing/verification keys are either unknown or they don't exist.
|
||||
| *`tokenSigningKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | TokenSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for signing tokens is stored.
|
||||
| *`stateSigningKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for signing state parameters is stored.
|
||||
| *`stateEncryptionKey`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | StateSigningKey holds the name of the corev1.Secret in which this OIDC Provider's key for encrypting state parameters is stored.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomainspec"]
|
||||
==== FederationDomainSpec
|
||||
|
||||
FederationDomainSpec is a struct that describes an OIDC Provider.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomain[$$FederationDomain$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
@@ -254,44 +416,44 @@ OIDCProviderSpec is a struct that describes an OIDC Provider.
|
||||
| Field | Description
|
||||
| *`issuer`* __string__ | Issuer is the OIDC Provider's issuer, per the OIDC Discovery Metadata document, as well as the identifier that it will use for the iss claim in issued JWTs. This field will also be used as the base URL for any endpoints used by the OIDC Provider (e.g., if your issuer is https://example.com/foo, then your authorization endpoint will look like https://example.com/foo/some/path/to/auth/endpoint).
|
||||
See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3 for more information.
|
||||
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcprovidertlsspec[$$OIDCProviderTLSSpec$$]__ | TLS configures how this OIDCProvider is served over Transport Layer Security (TLS).
|
||||
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomaintlsspec[$$FederationDomainTLSSpec$$]__ | TLS configures how this FederationDomain is served over Transport Layer Security (TLS).
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcproviderstatus"]
|
||||
==== OIDCProviderStatus
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomainstatus"]
|
||||
==== FederationDomainStatus
|
||||
|
||||
OIDCProviderStatus is a struct that describes the actual state of an OIDC Provider.
|
||||
FederationDomainStatus is a struct that describes the actual state of an OIDC Provider.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcprovider[$$OIDCProvider$$]
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomain[$$FederationDomain$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`status`* __OIDCProviderStatusCondition__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure.
|
||||
| *`status`* __FederationDomainStatusCondition__ | Status holds an enum that describes the state of this OIDC Provider. Note that this Status can represent success or failure.
|
||||
| *`message`* __string__ | Message provides human-readable details about the Status.
|
||||
| *`lastUpdateTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#time-v1-meta[$$Time$$]__ | LastUpdateTime holds the time at which the Status was last updated. It is a pointer to get around some undesirable behavior with respect to the empty metav1.Time value (see https://github.com/kubernetes/kubernetes/issues/86811).
|
||||
| *`jwksSecret`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#localobjectreference-v1-core[$$LocalObjectReference$$]__ | JWKSSecret holds the name of the secret in which this OIDC Provider's signing/verification keys are stored. If it is empty, then the signing/verification keys are either unknown or they don't exist.
|
||||
| *`secrets`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomainsecrets[$$FederationDomainSecrets$$]__ | Secrets contains information about this OIDC Provider's secrets.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcprovidertlsspec"]
|
||||
==== OIDCProviderTLSSpec
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomaintlsspec"]
|
||||
==== FederationDomainTLSSpec
|
||||
|
||||
OIDCProviderTLSSpec is a struct that describes the TLS configuration for an OIDC Provider.
|
||||
FederationDomainTLSSpec is a struct that describes the TLS configuration for an OIDC Provider.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcproviderspec[$$OIDCProviderSpec$$]
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomainspec[$$FederationDomainSpec$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`secretName`* __string__ | SecretName is an optional name of a Secret in the same namespace, of type `kubernetes.io/tls`, which contains the TLS serving certificate for the HTTPS endpoints served by this OIDCProvider. When provided, the TLS Secret named here must contain keys named `tls.crt` and `tls.key` that contain the certificate and private key to use for TLS.
|
||||
| *`secretName`* __string__ | SecretName is an optional name of a Secret in the same namespace, of type `kubernetes.io/tls`, which contains the TLS serving certificate for the HTTPS endpoints served by this FederationDomain. When provided, the TLS Secret named here must contain keys named `tls.crt` and `tls.key` that contain the certificate and private key to use for TLS.
|
||||
Server Name Indication (SNI) is an extension to the Transport Layer Security (TLS) supported by all major browsers.
|
||||
SecretName is required if you would like to use different TLS certificates for issuers of different hostnames. SNI requests do not include port numbers, so all issuers with the same DNS hostname must use the same SecretName value even if they have different port numbers.
|
||||
SecretName is not required when you would like to use only the HTTP endpoints (e.g. when terminating TLS at an Ingress). It is also not required when you would like all requests to this OIDC Provider's HTTPS endpoints to use the default TLS certificate, which is configured elsewhere.
|
||||
@@ -300,6 +462,363 @@ OIDCProviderTLSSpec is a struct that describes the TLS configuration for an OIDC
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-identity-concierge-pinniped-dev-identity"]
|
||||
=== identity.concierge.pinniped.dev/identity
|
||||
|
||||
Package identity is the internal version of the Pinniped identity API.
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-extravalue"]
|
||||
==== ExtraValue
|
||||
|
||||
ExtraValue masks the value so protobuf can generate
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-userinfo[$$UserInfo$$]
|
||||
****
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-kubernetesuserinfo"]
|
||||
==== KubernetesUserInfo
|
||||
|
||||
KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it. Copied from the Kubernetes token review API.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-whoamirequeststatus[$$WhoAmIRequestStatus$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`User`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-userinfo[$$UserInfo$$]__ | User is the UserInfo associated with the current user.
|
||||
| *`Audiences`* __string array__ | Audiences are audience identifiers chosen by the authenticator.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-userinfo"]
|
||||
==== UserInfo
|
||||
|
||||
UserInfo holds the information about the user needed to implement the user.Info interface.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-kubernetesuserinfo[$$KubernetesUserInfo$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`Username`* __string__ | The name that uniquely identifies this user among all active users.
|
||||
| *`UID`* __string__ | A unique value that identifies this user across time. If this user is deleted and another user by the same name is added, they will have different UIDs.
|
||||
| *`Groups`* __string array__ | The names of groups this user is a part of.
|
||||
| *`Extra`* __object (keys:string, values:string array)__ | Any additional information provided by the authenticator.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-whoamirequest"]
|
||||
==== WhoAmIRequest
|
||||
|
||||
WhoAmIRequest submits a request to echo back the current authenticated user.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-whoamirequestlist[$$WhoAmIRequestList$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`ObjectMeta`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectmeta-v1-meta[$$ObjectMeta$$]__ |
|
||||
| *`Spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-whoamirequestspec[$$WhoAmIRequestSpec$$]__ |
|
||||
| *`Status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-whoamirequeststatus[$$WhoAmIRequestStatus$$]__ |
|
||||
|===
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-whoamirequeststatus"]
|
||||
==== WhoAmIRequestStatus
|
||||
|
||||
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-whoamirequest[$$WhoAmIRequest$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`KubernetesUserInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-kubernetesuserinfo[$$KubernetesUserInfo$$]__ | The current authenticated user, exactly as Kubernetes understands it.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-identity-concierge-pinniped-dev-v1alpha1"]
|
||||
=== identity.concierge.pinniped.dev/v1alpha1
|
||||
|
||||
Package v1alpha1 is the v1alpha1 version of the Pinniped identity API.
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-extravalue"]
|
||||
==== ExtraValue
|
||||
|
||||
ExtraValue masks the value so protobuf can generate
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-userinfo[$$UserInfo$$]
|
||||
****
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-kubernetesuserinfo"]
|
||||
==== KubernetesUserInfo
|
||||
|
||||
KubernetesUserInfo represents the current authenticated user, exactly as Kubernetes understands it. Copied from the Kubernetes token review API.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-whoamirequeststatus[$$WhoAmIRequestStatus$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`user`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-userinfo[$$UserInfo$$]__ | User is the UserInfo associated with the current user.
|
||||
| *`audiences`* __string array__ | Audiences are audience identifiers chosen by the authenticator.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-userinfo"]
|
||||
==== UserInfo
|
||||
|
||||
UserInfo holds the information about the user needed to implement the user.Info interface.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-kubernetesuserinfo[$$KubernetesUserInfo$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`username`* __string__ | The name that uniquely identifies this user among all active users.
|
||||
| *`uid`* __string__ | A unique value that identifies this user across time. If this user is deleted and another user by the same name is added, they will have different UIDs.
|
||||
| *`groups`* __string array__ | The names of groups this user is a part of.
|
||||
| *`extra`* __object (keys:string, values:string array)__ | Any additional information provided by the authenticator.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-whoamirequest"]
|
||||
==== WhoAmIRequest
|
||||
|
||||
WhoAmIRequest submits a request to echo back the current authenticated user.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-whoamirequestlist[$$WhoAmIRequestList$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`.
|
||||
|
||||
| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-whoamirequestspec[$$WhoAmIRequestSpec$$]__ |
|
||||
| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-whoamirequeststatus[$$WhoAmIRequestStatus$$]__ |
|
||||
|===
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-whoamirequeststatus"]
|
||||
==== WhoAmIRequestStatus
|
||||
|
||||
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-whoamirequest[$$WhoAmIRequest$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`kubernetesUserInfo`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-identity-v1alpha1-kubernetesuserinfo[$$KubernetesUserInfo$$]__ | The current authenticated user, exactly as Kubernetes understands it.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-idp-supervisor-pinniped-dev-v1alpha1"]
|
||||
=== idp.supervisor.pinniped.dev/v1alpha1
|
||||
|
||||
Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity provider (IDP) API.
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-condition"]
|
||||
==== Condition
|
||||
|
||||
Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus[$$OIDCIdentityProviderStatus$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`type`* __string__ | type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
| *`status`* __ConditionStatus__ | status of the condition, one of True, False, Unknown.
|
||||
| *`observedGeneration`* __integer__ | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
|
||||
| *`lastTransitionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#time-v1-meta[$$Time$$]__ | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
| *`reason`* __string__ | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
|
||||
| *`message`* __string__ | message is a human readable message indicating details about the transition. This may be an empty string.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig"]
|
||||
==== OIDCAuthorizationConfig
|
||||
|
||||
OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization request parameters.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`additionalScopes`* __string array__ | AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization request flow with an OIDC identity provider. By default only the "openid" scope will be requested.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcclaims"]
|
||||
==== OIDCClaims
|
||||
|
||||
OIDCClaims provides a mapping from upstream claims into identities.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`groups`* __string__ | Groups provides the name of the token claim that will be used to ascertain the groups to which an identity belongs.
|
||||
| *`username`* __string__ | Username provides the name of the token claim that will be used to ascertain an identity's username.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcclient"]
|
||||
==== OIDCClient
|
||||
|
||||
OIDCClient contains information about an OIDC client (e.g., client ID and client secret).
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc-client" with keys "clientID" and "clientSecret".
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityprovider"]
|
||||
==== OIDCIdentityProvider
|
||||
|
||||
OIDCIdentityProvider describes the configuration of an upstream OpenID Connect identity provider.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderlist[$$OIDCIdentityProviderList$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`.
|
||||
|
||||
| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$]__ | Spec for configuring the identity provider.
|
||||
| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus[$$OIDCIdentityProviderStatus$$]__ | Status of the identity provider.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec"]
|
||||
==== OIDCIdentityProviderSpec
|
||||
|
||||
Spec for configuring an OIDC identity provider.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityprovider[$$OIDCIdentityProvider$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration.
|
||||
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for discovery/JWKS requests to the issuer.
|
||||
| *`authorizationConfig`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]__ | AuthorizationConfig holds information about how to form the OAuth2 authorization request parameters to be used with this OIDC identity provider.
|
||||
| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcclaims[$$OIDCClaims$$]__ | Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider.
|
||||
| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcclient[$$OIDCClient$$]__ | OIDCClient contains OIDC client information to be used used with this OIDC identity provider.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus"]
|
||||
==== OIDCIdentityProviderStatus
|
||||
|
||||
Status of an OIDC identity provider.
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityprovider[$$OIDCIdentityProvider$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`phase`* __OIDCIdentityProviderPhase__ | Phase summarizes the overall status of the OIDCIdentityProvider.
|
||||
| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-condition[$$Condition$$]__ | Represents the observations of an identity provider's current state.
|
||||
|===
|
||||
|
||||
|
||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec"]
|
||||
==== TLSSpec
|
||||
|
||||
|
||||
|
||||
.Appears In:
|
||||
****
|
||||
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$]
|
||||
****
|
||||
|
||||
[cols="25a,75a", options="header"]
|
||||
|===
|
||||
| Field | Description
|
||||
| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
[id="{anchor_prefix}-login-concierge-pinniped-dev-v1alpha1"]
|
||||
=== login.concierge.pinniped.dev/v1alpha1
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user