mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-01-15 02:03:30 +00:00
Compare commits
1168 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c210b67d4 | ||
|
|
3a4405659e | ||
|
|
187bd9060c | ||
|
|
7a98900b28 | ||
|
|
e0b94f4780 | ||
|
|
587cced768 | ||
|
|
50964c6677 | ||
|
|
81eb0735d1 | ||
|
|
c7931bc6d5 | ||
|
|
b27e3e1a89 | ||
|
|
8db9331fed | ||
|
|
3e15e184ef | ||
|
|
6a457466df | ||
|
|
3a81fbd1b4 | ||
|
|
421c17c421 | ||
|
|
780d236d89 | ||
|
|
55483b726b | ||
|
|
32602f579b | ||
|
|
65e7df1417 | ||
|
|
b96d49df0f | ||
|
|
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 | ||
|
|
1223cf7877 | ||
|
|
036845deee | ||
|
|
c451604816 | ||
|
|
05cf56a0fa | ||
|
|
5a0e7fd358 | ||
|
|
2bf5c8b48b | ||
|
|
05233963fb | ||
|
|
2b8773aa54 | ||
|
|
59263ea733 | ||
|
|
b13a8075e4 | ||
|
|
d596f8c3e5 | ||
|
|
75c35e74cc | ||
|
|
e4f4cd7ca0 | ||
|
|
a01921012d | ||
|
|
2e50e8f01b | ||
|
|
935577f8e7 | ||
|
|
781f86d18c | ||
|
|
fcea48c8f9 | ||
|
|
7639d5e161 | ||
|
|
ab5c04b1f3 | ||
|
|
fb3c5749e8 | ||
|
|
7597b12a51 | ||
|
|
5bbfc35d27 | ||
|
|
f76b9857da | ||
|
|
9e1922f1ed | ||
|
|
01f4fdb5c3 | ||
|
|
a5379c08e2 | ||
|
|
ad95bb44b0 | ||
|
|
4b7592feaf | ||
|
|
34da8c7877 | ||
|
|
f3a83882a4 | ||
|
|
0f25657a35 | ||
|
|
e69183aa8a | ||
|
|
81390bba89 | ||
|
|
59431a3d3d | ||
|
|
9760c03617 | ||
|
|
8b8ffc21c4 | ||
|
|
f0320dfbd8 | ||
|
|
3277e778ea | ||
|
|
9c13b7144e | ||
|
|
059b6e885f | ||
|
|
4af508981a | ||
|
|
a007fc3bd3 | ||
|
|
c52874250a | ||
|
|
01dddd3cae | ||
|
|
bd04570e51 | ||
|
|
8ff64d4c1a | ||
|
|
2542a8e175 | ||
|
|
29e0ce5662 | ||
|
|
978ecda758 | ||
|
|
170d3a3993 | ||
|
|
2777c4e9f3 | ||
|
|
38802c2184 | ||
|
|
7bce16737b | ||
|
|
96c4661a25 | ||
|
|
45189e3e2b | ||
|
|
d5dd65cfe8 | ||
|
|
1f1b6c884e | ||
|
|
eeb110761e | ||
|
|
8b7c30cfbd | ||
|
|
7880f7ea41 | ||
|
|
13ccb07fe4 | ||
|
|
6c092deba5 | ||
|
|
25a91019c2 | ||
|
|
7615667b9b | ||
|
|
0948457521 | ||
|
|
110c72a5d4 | ||
|
|
f928ef4752 | ||
|
|
eafdef7b11 | ||
|
|
4c844ba334 | ||
|
|
07001e5ee3 | ||
|
|
3508a28369 | ||
|
|
397ec61e57 | ||
|
|
8ae04605ca | ||
|
|
8772a00824 | ||
|
|
ce598eb58e | ||
|
|
4b24e9c625 | ||
|
|
fe3b44b134 | ||
|
|
122f7cffdb | ||
|
|
5dbc03efe9 | ||
|
|
0adbb5234e | ||
|
|
e919ef6582 | ||
|
|
fa5f653de6 | ||
|
|
e8113e3770 | ||
|
|
7f6a82aa91 | ||
|
|
4ef41f969d | ||
|
|
3e39800005 | ||
|
|
52ebd77527 | ||
|
|
ec21fc8595 | ||
|
|
276dff5772 | ||
|
|
90235418b9 | ||
|
|
9ba93d66c3 | ||
|
|
aff85acf37 | ||
|
|
4da64f38b5 | ||
|
|
d9d76726c2 | ||
|
|
08659a6583 | ||
|
|
e2630be00a | ||
|
|
8fe031e73d | ||
|
|
617c5608ca | ||
|
|
dda3c21a8e | ||
|
|
f8e461dfc3 | ||
|
|
94f20e57b1 | ||
|
|
943286bbc6 | ||
|
|
e05213f9dd | ||
|
|
5a0dab768f | ||
|
|
fbcce700dc | ||
|
|
a5abe9ca3e | ||
|
|
1b99983441 | ||
|
|
31225ac7ae | ||
|
|
f21122a309 | ||
|
|
aef25163e2 | ||
|
|
87c7e9a556 | ||
|
|
c05bdb58ac | ||
|
|
84a0084703 | ||
|
|
1301018655 | ||
|
|
76e89b523b | ||
|
|
c030551af0 | ||
|
|
cd970616da | ||
|
|
68d20298f2 | ||
|
|
19a1d569c9 | ||
|
|
a197a26335 | ||
|
|
6aed025c79 | ||
|
|
aa705afc72 | ||
|
|
3d5937a8e8 | ||
|
|
33fcc74417 | ||
|
|
50d80489be | ||
|
|
8a16a92c01 | ||
|
|
d1e86e2616 | ||
|
|
67b692b11f | ||
|
|
ce49d8bd7b | ||
|
|
a13d7ec5a1 | ||
|
|
478b0a0fd8 | ||
|
|
ff545db869 | ||
|
|
6b135b93cf | ||
|
|
d81d395c80 | ||
|
|
171f3ed906 | ||
|
|
354b922e48 | ||
|
|
34549b779b | ||
|
|
72b2d02777 | ||
|
|
b71959961d | ||
|
|
f5a6a0bb1e | ||
|
|
c555c14ccb | ||
|
|
bb015adf4e | ||
|
|
fac4d074d0 | ||
|
|
b74486f305 | ||
|
|
a4389562e3 | ||
|
|
05141592f8 | ||
|
|
8b7d96f42c | ||
|
|
da00fc708f | ||
|
|
6b653fc663 | ||
|
|
154de991e4 | ||
|
|
f48a4e445e | ||
|
|
20ce142f90 | ||
|
|
c49ebf4b57 | ||
|
|
019f44982c | ||
|
|
8a772793b8 | ||
|
|
ead1ade24b | ||
|
|
ae56fcb46a | ||
|
|
a7c334a0f3 | ||
|
|
044b5c4d46 | ||
|
|
6f8f99e49b | ||
|
|
78cc49d658 | ||
|
|
8012d6a1c2 | ||
|
|
885005a3c1 | ||
|
|
79c07f3e21 | ||
|
|
14f1d86833 | ||
|
|
5b3dd5fc7d | ||
|
|
38501ff763 | ||
|
|
006d96ab92 | ||
|
|
fd6a7f5892 | ||
|
|
76bd462cf8 | ||
|
|
b0a4ae13c5 | ||
|
|
01153dcb9d | ||
|
|
c1c75a8f22 | ||
|
|
7eed7ba19a | ||
|
|
969c136921 | ||
|
|
8a360fe08e | ||
|
|
da695ef787 | ||
|
|
13e0b272c0 | ||
|
|
e97bad2198 | ||
|
|
fe12f85c70 | ||
|
|
127914703e | ||
|
|
916db74d65 | ||
|
|
0bfa351eb4 | ||
|
|
b69eb5e850 | ||
|
|
d43744f8e9 | ||
|
|
d23ff1f5eb | ||
|
|
d6571671f6 | ||
|
|
38e26d7a49 | ||
|
|
efe420b737 | ||
|
|
42e74a02e9 | ||
|
|
70480260dd | ||
|
|
82f8094de7 | ||
|
|
434e3fe435 | ||
|
|
b21b43c654 | ||
|
|
9e0195e024 | ||
|
|
d853cbc7ff | ||
|
|
9ed52e6b4a | ||
|
|
fab36c55f5 | ||
|
|
409d10baf8 | ||
|
|
ea762b405d | ||
|
|
3ff605bb39 | ||
|
|
856971e452 | ||
|
|
eaf2d9a185 | ||
|
|
3f06be2246 | ||
|
|
69137fb6b9 | ||
|
|
253d3bb36f | ||
|
|
9f80b0ea00 | ||
|
|
6f4cf705e5 | ||
|
|
ec3e4cae68 | ||
|
|
381811b36f | ||
|
|
906a88f2d3 | ||
|
|
0f8437bc3a | ||
|
|
6d047c151f | ||
|
|
9735122db9 | ||
|
|
4948e1702f | ||
|
|
406f2723ce | ||
|
|
6c555f94e3 | ||
|
|
f8e872d1af | ||
|
|
3e45bfc97d | ||
|
|
a55e9de4fc | ||
|
|
eb0d9a15fc | ||
|
|
6063674623 | ||
|
|
d574fe05ba | ||
|
|
4369cc9ff2 | ||
|
|
adf263b566 | ||
|
|
4edda802e5 | ||
|
|
db9a97721f | ||
|
|
3578d7cb9a | ||
|
|
83920db502 | ||
|
|
1a4f9e3466 | ||
|
|
e574a99c5e | ||
|
|
16ef2baf8a | ||
|
|
9beb3855b5 | ||
|
|
81f2362543 | ||
|
|
07f0181fa3 | ||
|
|
481308215d | ||
|
|
381fd51e13 | ||
|
|
541336b997 | ||
|
|
6cdd4a9506 | ||
|
|
fbe0551426 | ||
|
|
164f64a370 | ||
|
|
526be79b11 | ||
|
|
820f1e977e | ||
|
|
50258fc569 | ||
|
|
0d3ad0085d | ||
|
|
cfb76a538c | ||
|
|
e18b6fdddc | ||
|
|
5a608cc84c | ||
|
|
49145791cc | ||
|
|
6989e5da63 | ||
|
|
a2365b1cce | ||
|
|
80a520390b | ||
|
|
86e1c99dcd | ||
|
|
78ac27c262 | ||
|
|
f86a5244a6 | ||
|
|
907ccb68f5 | ||
|
|
98490b1a1b | ||
|
|
2d4d7e588a | ||
|
|
24f962f1b8 | ||
|
|
2ecb43154b | ||
|
|
dba951fe89 | ||
|
|
245854b85a | ||
|
|
5867f3699c | ||
|
|
7d5f57f923 | ||
|
|
2d497cbd36 | ||
|
|
eabe51c446 | ||
|
|
a479450940 | ||
|
|
b523e5832c | ||
|
|
079e07a51f | ||
|
|
025940d4f1 | ||
|
|
8c9c1e206d | ||
|
|
4c9cbf0706 | ||
|
|
a70a4766d2 | ||
|
|
1741f832eb | ||
|
|
b3327d7522 | ||
|
|
10793ac11f | ||
|
|
7ce760a5dd | ||
|
|
af034befb0 | ||
|
|
a8487b78c9 | ||
|
|
58bf93b10c | ||
|
|
f464e03380 | ||
|
|
efbe3a26c1 | ||
|
|
4f59d9286c | ||
|
|
6c75de9334 | ||
|
|
f425eed07c | ||
|
|
7a975d98fb | ||
|
|
635ecd7b1a | ||
|
|
29305777bb | ||
|
|
6d0b83aabf | ||
|
|
6ba712d612 | ||
|
|
eab5c2b86b | ||
|
|
e7b389ae6c | ||
|
|
e51e51dfd4 | ||
|
|
cd0194cb68 | ||
|
|
a73f14e03d | ||
|
|
e3b8c3b611 | ||
|
|
da9f24cf30 | ||
|
|
67de7f5646 | ||
|
|
43c69ec339 | ||
|
|
014fb518bc | ||
|
|
321c6a5392 | ||
|
|
db98f2810f | ||
|
|
062dfa3e75 | ||
|
|
1244a950e7 | ||
|
|
8df910361c | ||
|
|
37da441e96 | ||
|
|
6faf224e20 | ||
|
|
92372d20a9 | ||
|
|
12f0997193 | ||
|
|
e428877473 | ||
|
|
cecd691a84 | ||
|
|
1c7b3c3072 | ||
|
|
b1ea04b036 | ||
|
|
36a66f4e8b | ||
|
|
b39160e4c4 | ||
|
|
a22b414b58 | ||
|
|
8de046a561 | ||
|
|
f7c9ae8ba3 | ||
|
|
75ea0f48d9 | ||
|
|
acfc5acfb2 | ||
|
|
6506a82b19 | ||
|
|
66f4e62c6c | ||
|
|
80a23bd2fd | ||
|
|
2bdbac3e15 | ||
|
|
5b9f2ec9fc | ||
|
|
fc220d5f79 | ||
|
|
3344b5b86a | ||
|
|
557fd0df26 | ||
|
|
9bb3d4ef28 | ||
|
|
4ced58b5b7 | ||
|
|
831df90c93 | ||
|
|
82ef9e4806 | ||
|
|
879d847ffb | ||
|
|
4379d2772c | ||
|
|
21187bc28a | ||
|
|
9bad0d52f7 | ||
|
|
92fabf43b3 | ||
|
|
7d8c28a9dc | ||
|
|
bbef017989 | ||
|
|
7515af639a | ||
|
|
39b66086cc | ||
|
|
872330bee9 | ||
|
|
2cdc3defb7 | ||
|
|
da7c981f14 | ||
|
|
19c671a60a | ||
|
|
17d40b7a73 | ||
|
|
4e40c0320e | ||
|
|
a3dbb309d0 | ||
|
|
c436f84b3d | ||
|
|
f685cd228f | ||
|
|
63f9db72e8 | ||
|
|
004cfe380d | ||
|
|
b1d9665b03 | ||
|
|
4fa7e1bd76 | ||
|
|
22bf24b775 | ||
|
|
6deaa0fb1a | ||
|
|
4fe609a043 | ||
|
|
e6cb2f8220 | ||
|
|
b7bdb7f3b1 | ||
|
|
9baea83066 | ||
|
|
56be4a6761 | ||
|
|
b506ac5823 | ||
|
|
fec31b71c0 | ||
|
|
89d01b84f8 | ||
|
|
fc3b4e9ae1 | ||
|
|
2565f67824 | ||
|
|
3ee7a0d881 | ||
|
|
7207041c37 | ||
|
|
7f9cb43ffa | ||
|
|
20b21e8639 | ||
|
|
3d09afbfb3 | ||
|
|
b0315e5e9f | ||
|
|
f8f16fadb9 | ||
|
|
ba53218711 | ||
|
|
1415fcc6dc | ||
|
|
ab82b2ea64 | ||
|
|
1dcba155a2 | ||
|
|
9c8d30fa86 | ||
|
|
1d004a7326 | ||
|
|
a2e8b2aa0c | ||
|
|
3e4816c811 | ||
|
|
8e5912e4c2 | ||
|
|
2959b54e7b | ||
|
|
f49317d7e4 | ||
|
|
2546d3f823 | ||
|
|
0c5d38090e | ||
|
|
cd00aad610 | ||
|
|
eb4b2b1ecd | ||
|
|
b5f7ff2e33 | ||
|
|
21fd807037 | ||
|
|
b0d99abf22 | ||
|
|
0135d8b6c3 | ||
|
|
ecf67862e2 | ||
|
|
aeee2cf05e | ||
|
|
f0c400235a | ||
|
|
7848332d47 | ||
|
|
1fcf95af01 | ||
|
|
a503fa8673 | ||
|
|
371b172616 | ||
|
|
ddb7a20c53 | ||
|
|
a4fe76f6a9 | ||
|
|
9d7e073a9d | ||
|
|
118ee7f9aa | ||
|
|
e0b5c3a146 | ||
|
|
cbc80d5bc4 | ||
|
|
20a3208564 | ||
|
|
91ba39bd3b | ||
|
|
f6ea93e273 | ||
|
|
d728c926c1 | ||
|
|
9ecc88a898 | ||
|
|
18b000e324 | ||
|
|
e6dd22ffb5 | ||
|
|
92a6b7f4a4 | ||
|
|
e39a38ecf2 | ||
|
|
9d9b56073c | ||
|
|
07bb2bb956 | ||
|
|
abe3f1ba4b | ||
|
|
1375df185d | ||
|
|
8f93fbb87b | ||
|
|
68893a1e15 | ||
|
|
9440316c20 | ||
|
|
f9554e0bde | ||
|
|
89f059ae03 | ||
|
|
7360489d1b | ||
|
|
61b758450e | ||
|
|
9539f29f94 | ||
|
|
6cc7bdf7d3 | ||
|
|
8f4a2f98d7 | ||
|
|
8ddc1a1e92 | ||
|
|
d240796110 | ||
|
|
7502190135 | ||
|
|
aea3f0f90d | ||
|
|
f66f7f14f5 | ||
|
|
d8bcea88a7 | ||
|
|
2629a9c42f | ||
|
|
90fe733f94 | ||
|
|
5ed97f7f9e | ||
|
|
80153f9a80 | ||
|
|
4306599396 | ||
|
|
6e59596285 | ||
|
|
c2e6a1408d | ||
|
|
4e08866e87 | ||
|
|
cbd6dd3356 | ||
|
|
eb05e7a138 | ||
|
|
22f1ca24d9 | ||
|
|
8b36f2e8ae | ||
|
|
34d13f71c2 | ||
|
|
1aef2f07d3 | ||
|
|
142e9a1583 | ||
|
|
ed8b1be178 | ||
|
|
399e1d2eb8 | ||
|
|
ba2e2f509a | ||
|
|
6d43d7ba19 | ||
|
|
ace01c86de | ||
|
|
d4b184a7d5 | ||
|
|
76bd274fc4 | ||
|
|
0a805861ea | ||
|
|
2b297c28d5 | ||
|
|
d0a9d8df33 | ||
|
|
88f3b41e71 | ||
|
|
89b6b9ee44 | ||
|
|
39c299a32d | ||
|
|
3929fa672e | ||
|
|
43888e9e0a | ||
|
|
a26d86044e | ||
|
|
5946c2920a | ||
|
|
6b90dc8bb7 | ||
|
|
1b9a70d089 | ||
|
|
40d1360b74 | ||
|
|
57578f16d4 | ||
|
|
003aef75d2 | ||
|
|
e3397c1c35 | ||
|
|
c4ce97f1a5 | ||
|
|
f95f5857ef | ||
|
|
cedd47b92e | ||
|
|
7fa8f7797a | ||
|
|
a456daa0b2 | ||
|
|
ecde8fa8af | ||
|
|
29654c39a5 | ||
|
|
d8d49be5d9 | ||
|
|
769ef71db7 | ||
|
|
87b9ff2131 | ||
|
|
a45748f020 | ||
|
|
ccefc29eb0 | ||
|
|
76a44ecd58 | ||
|
|
787cf47c39 | ||
|
|
9376f034ea | ||
|
|
1977dc2ce7 | ||
|
|
3fd4458e6a | ||
|
|
ae0b97d807 | ||
|
|
50e70f73ae | ||
|
|
df1a1cf1bd | ||
|
|
0d034cd18e | ||
|
|
dd8ce677ba | ||
|
|
c6f1defa9d | ||
|
|
6e46ff345a | ||
|
|
b6c468117e | ||
|
|
1b23e31464 | ||
|
|
c02b6fee8f | ||
|
|
87eddf8bbd | ||
|
|
9648db0837 | ||
|
|
ba0b997234 | ||
|
|
864db74306 | ||
|
|
e48d9faf27 | ||
|
|
031129778e | ||
|
|
ed9fdce6a8 | ||
|
|
d2f6eebc66 | ||
|
|
4cb0fd3949 | ||
|
|
e0f0eca512 | ||
|
|
bfabcdcdd1 | ||
|
|
224b59e740 | ||
|
|
553b519d0f | ||
|
|
b80f3148fd | ||
|
|
d6e745203d | ||
|
|
0806074d94 | ||
|
|
13d4a38eca | ||
|
|
5ec1fbd1ca | ||
|
|
fadd718d08 | ||
|
|
28a500fce9 | ||
|
|
745775bf4b | ||
|
|
ce3de2b516 | ||
|
|
8034ef24ff | ||
|
|
626fc6aa8d | ||
|
|
cc9ae23a0c | ||
|
|
7152ffd730 | ||
|
|
6300898810 | ||
|
|
7c8876a812 | ||
|
|
b3df59ca13 | ||
|
|
b4130af2bf | ||
|
|
5394008d6f | ||
|
|
3583f7a09f | ||
|
|
df3c387f2e | ||
|
|
fa0533fae9 | ||
|
|
86c3f89b2e | ||
|
|
b00cec954e | ||
|
|
b379d5148c | ||
|
|
aecd005c60 | ||
|
|
6dd331b21d | ||
|
|
c4bbb64622 | ||
|
|
7143058462 | ||
|
|
c5d5914866 | ||
|
|
af656d4b02 | ||
|
|
9e9868bd16 | ||
|
|
cbe4c1b370 | ||
|
|
ad55f9e310 | ||
|
|
0b4590b237 | ||
|
|
f10c61f591 | ||
|
|
31e6d8fbb1 | ||
|
|
dd278b46a8 | ||
|
|
da5b509cc6 | ||
|
|
2b573d8642 | ||
|
|
519484816d | ||
|
|
6da420d865 | ||
|
|
f8567450ee | ||
|
|
08961919b5 | ||
|
|
92939cf118 | ||
|
|
fb843aa15b | ||
|
|
7ce49bf89c | ||
|
|
09571d1117 | ||
|
|
573202140d | ||
|
|
fdbc30365d | ||
|
|
b70c62a1b3 | ||
|
|
2b9d2ca293 | ||
|
|
12120d7e8b | ||
|
|
727a5883f2 | ||
|
|
ca80d87dcf | ||
|
|
e884cef1ef | ||
|
|
597408a977 | ||
|
|
548874a641 | ||
|
|
cf56c67329 | ||
|
|
9fe82ec5f1 | ||
|
|
2aa80e3576 | ||
|
|
0f248768a3 | ||
|
|
52546fad90 | ||
|
|
bd594e19ff | ||
|
|
2e05e032ee | ||
|
|
733f80b7ae | ||
|
|
ae7be3ea94 | ||
|
|
a8dbdfd1c4 | ||
|
|
3d293c96bc | ||
|
|
02c17d875e | ||
|
|
076f8805d2 | ||
|
|
5aebb76146 | ||
|
|
ec6ec2abe9 | ||
|
|
b59604b47c | ||
|
|
66fe580e99 | ||
|
|
a448b3474e | ||
|
|
04cacabc16 | ||
|
|
3bc0389bab | ||
|
|
15bee7456c | ||
|
|
8bdf05dae4 | ||
|
|
ee865fe97f | ||
|
|
9a859875a7 | ||
|
|
e0cac97084 | ||
|
|
a5f7de429d | ||
|
|
aa90173891 | ||
|
|
409462e989 | ||
|
|
a8f3c62d37 | ||
|
|
7ba43e0c3f | ||
|
|
43c3f1ab2e | ||
|
|
b70f3aefe5 | ||
|
|
1e56ecfdb4 | ||
|
|
42616e7d8a | ||
|
|
271eb9b837 | ||
|
|
48433eb36b | ||
|
|
bc4351f51a | ||
|
|
531954511b | ||
|
|
a15a106fd3 | ||
|
|
b0d9db1bcc | ||
|
|
1a349bb609 | ||
|
|
0ee4f0417d | ||
|
|
ebe39c8663 | ||
|
|
1e8463ac2d | ||
|
|
a5dbc324f6 | ||
|
|
27cd82065b | ||
|
|
9e44bc28d9 | ||
|
|
0acb8c8d3c | ||
|
|
ce71a5bac8 | ||
|
|
425e95bed4 | ||
|
|
418811ef19 | ||
|
|
c9026cd150 | ||
|
|
63a5381968 | ||
|
|
74a328de41 | ||
|
|
8a313bc653 | ||
|
|
6dfae48b65 | ||
|
|
8a8a278029 | ||
|
|
f7b0cf8f8a | ||
|
|
69f766d41d | ||
|
|
5dea51c062 | ||
|
|
b16bf52580 | ||
|
|
f47927331f | ||
|
|
066bc84e2a | ||
|
|
9f0d2606b1 | ||
|
|
f986600d5b | ||
|
|
349dd98a2f | ||
|
|
60bbcc12d8 | ||
|
|
259fc0e794 | ||
|
|
a1593c4b7b | ||
|
|
8606cc9662 | ||
|
|
613f324a47 | ||
|
|
d8c7a25487 | ||
|
|
07a71236aa | ||
|
|
757d987204 | ||
|
|
899f736b8c | ||
|
|
6001f1f456 | ||
|
|
99b35e1a61 | ||
|
|
e5902533eb | ||
|
|
0d667466e8 | ||
|
|
9bfec08d90 | ||
|
|
6cc8a2f8dd | ||
|
|
6fe7a4c9dc | ||
|
|
924eb1abaa | ||
|
|
a7748a360e | ||
|
|
84bb0a9a21 | ||
|
|
e1f44e2654 | ||
|
|
9af3637403 | ||
|
|
6a93de3931 | ||
|
|
6c87c793db | ||
|
|
5fdc20886d | ||
|
|
23c1b32a02 | ||
|
|
d4eeb74641 | ||
|
|
31c4e6560d | ||
|
|
4b1a7436a9 | ||
|
|
549da37805 | ||
|
|
240f9f86b1 | ||
|
|
b638bd7eeb | ||
|
|
5fa5b9a9a9 | ||
|
|
9118869d04 | ||
|
|
e92bdbea64 | ||
|
|
d71a620a18 | ||
|
|
7cac20fc89 | ||
|
|
260a271859 | ||
|
|
611859f04a | ||
|
|
fd4c6f6a71 | ||
|
|
092cc26789 | ||
|
|
a3bce5f42e | ||
|
|
a01970602a | ||
|
|
da4f036622 | ||
|
|
ffa417f745 | ||
|
|
61a4eec144 | ||
|
|
9edae03812 | ||
|
|
63f5416b21 | ||
|
|
5a66b56b93 | ||
|
|
2596ddfa25 | ||
|
|
89c8d1183b | ||
|
|
7da347866b | ||
|
|
d3d9cc6fac | ||
|
|
81e91accfa | ||
|
|
a544f7d7bf | ||
|
|
3fd7e7835a | ||
|
|
a9cf376000 | ||
|
|
fe81958d2c | ||
|
|
12255109bd | ||
|
|
e9145bbe2e | ||
|
|
c307a263ec | ||
|
|
1c7109d5aa | ||
|
|
85e3b356dd | ||
|
|
518ae7eb4c | ||
|
|
619ae2b178 | ||
|
|
568febea79 | ||
|
|
8d6a645915 | ||
|
|
fd70eda033 | ||
|
|
622d488fc3 | ||
|
|
f0d7077efc | ||
|
|
ee7480bcda | ||
|
|
68d01f97a4 | ||
|
|
4e17853ecf | ||
|
|
7eaca5a56d | ||
|
|
82f89c501a | ||
|
|
9bcd532c19 | ||
|
|
84dcbf4f5f | ||
|
|
57a22f99aa | ||
|
|
cc81dd04e9 | ||
|
|
c85507e46d | ||
|
|
90ff9d57b8 | ||
|
|
fb6085da39 | ||
|
|
911f8736f1 |
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.go.tmpl linguist-language=Go
|
||||
generated/** linguist-generated
|
||||
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Explain a problem you are experiencing
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Hey! Thanks for opening an issue!
|
||||
|
||||
IMPORTANT: If you believe this bug is a security issue, please don't use this template and follow our [security guidelines](/doc/security.md).
|
||||
|
||||
It is recommended that you include screenshots and logs to help everyone achieve a shared understanding of the bug.
|
||||
|
||||
-->
|
||||
|
||||
**What happened?**
|
||||
|
||||
> Please be specific and include screenshots and logs!
|
||||
|
||||
**What did you expect to happen?**
|
||||
|
||||
> Please be specific and include proposed behavior!
|
||||
|
||||
**What is the simplest way to reproduce this behavior?**
|
||||
|
||||
**In what environment did you see this bug?**
|
||||
- Pinniped server version:
|
||||
- Pinniped client version:
|
||||
- Pinniped container image (if using a public container image):
|
||||
- Pinniped configuration (what IDP(s) are you using? what downstream credential minting mechanisms are you using?):
|
||||
- Kubernetes version (use `kubectl version`):
|
||||
- Kubernetes installer & version (e.g., `kubeadm version`):
|
||||
- Cloud provider or hardware configuration:
|
||||
- OS (e.g: `cat /etc/os-release`):
|
||||
- Kernel (e.g. `uname -a`):
|
||||
- Others:
|
||||
|
||||
**What else is there to know about this bug?**
|
||||
35
.github/ISSUE_TEMPLATE/feature-proposal.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/feature-proposal.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Feature proposal
|
||||
about: Suggest a way to improve this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Hey! Thanks for opening an issue!
|
||||
|
||||
It is recommended that you include screenshots and logs to help everyone achieve a shared understanding of the improvement.
|
||||
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Are you considering submitting a PR for this feature?**
|
||||
|
||||
- **How will this project improvement be tested?**
|
||||
- **How does this change the current architecture?**
|
||||
- **How will this change be backwards compatible?**
|
||||
- **How will this feature be documented?**
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# See https://docs.github.com/en/github/administering-a-repository/enabling-and-disabling-version-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
41
.github/pull_request_template.md
vendored
Normal file
41
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
<!--
|
||||
Thank you for submitting a pull request for Pinniped!
|
||||
|
||||
Before submitting, please see the guidelines in CONTRIBUTING.md in this repo.
|
||||
|
||||
Please note that a project maintainer will need to review and provide an
|
||||
initial approval on the PR to cause CI tests to automatically start.
|
||||
Also note that if you push additional commits to the PR, those commits
|
||||
will need another initial approval before CI will pick them up.
|
||||
|
||||
Reminder: Did you remember to run all the linter, unit tests, and integration tests
|
||||
described in CONTRIBUTING.md on your branch before submitting this PR?
|
||||
|
||||
Below is a template to help you describe your PR.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Provide a summary of your change. Feel free to use paragraphs or a bulleted list, for example:
|
||||
|
||||
- Improves performance by 10,000%.
|
||||
- Fixes all bugs.
|
||||
- Boils the oceans.
|
||||
|
||||
-->
|
||||
|
||||
<!--
|
||||
Does this PR fix one or more reported issues?
|
||||
If yes, use `Fixes #<issue number>` to automatically close the fixed issue(s) when the PR is merged.
|
||||
-->
|
||||
|
||||
**Release note**:
|
||||
|
||||
<!--
|
||||
Does this PR introduce a user-facing change?
|
||||
|
||||
If no, just write "NONE" in the release-note block below.
|
||||
If yes, a release note is required. Enter your extended release note in the block below.
|
||||
-->
|
||||
```release-note
|
||||
|
||||
```
|
||||
26
.gitignore
vendored
26
.gitignore
vendored
@@ -1,7 +1,21 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# goland
|
||||
.idea
|
||||
.terraform
|
||||
*.tfstate.*
|
||||
*.tfstate
|
||||
kubeconfig.yaml
|
||||
.DS_Store
|
||||
site/
|
||||
|
||||
# Intermediate files used by Tilt
|
||||
/hack/lib/tilt/build
|
||||
|
||||
68
.golangci.yaml
Normal file
68
.golangci.yaml
Normal file
@@ -0,0 +1,68 @@
|
||||
# https://github.com/golangci/golangci-lint#config-file
|
||||
run:
|
||||
deadline: 1m
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
# default linters
|
||||
- deadcode
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- varcheck
|
||||
|
||||
# additional linters for this project (we should disable these if they get annoying).
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- depguard
|
||||
- dogsled
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- goheader
|
||||
- goimports
|
||||
- golint
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- misspell
|
||||
- nakedret
|
||||
- nestif
|
||||
- noctx
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- sqlclosecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- whitespace
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# exclude tests from some rules for things that are useful in a testing context.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
|
||||
linters-settings:
|
||||
funlen:
|
||||
lines: 150
|
||||
statements: 50
|
||||
goheader:
|
||||
template: |-
|
||||
Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
goimports:
|
||||
local-prefixes: go.pinniped.dev
|
||||
@@ -1,23 +1,17 @@
|
||||
# This is a configuration for https://pre-commit.com/.
|
||||
# On macOS, try `brew install pre-commit` and then run `pre-commit install`.
|
||||
exclude: '^(generated|hack/lib/tilt/tilt_modules)/'
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
# TODO: find a version of this to validate ytt templates?
|
||||
# - id: check-yaml
|
||||
# args: ['--allow-multiple-documents']
|
||||
- id: check-json
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
- id: check-byte-order-marker
|
||||
- id: detect-private-key
|
||||
- id: mixed-line-ending
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: validate-copyright-year
|
||||
name: Validate copyright year
|
||||
entry: hack/check-copyright-year.sh
|
||||
language: script
|
||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
# TODO: find a version of this to validate ytt templates?
|
||||
# - id: check-yaml
|
||||
# args: ['--allow-multiple-documents']
|
||||
- id: check-json
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: check-merge-conflict
|
||||
- id: check-added-large-files
|
||||
- id: check-byte-order-marker
|
||||
- id: detect-private-key
|
||||
exclude: testdata
|
||||
- id: mixed-line-ending
|
||||
|
||||
603
AD-SETUP.md
603
AD-SETUP.md
@@ -1,603 +0,0 @@
|
||||
# Creating an Active Directory server on Google Cloud for Pinniped integration tests
|
||||
|
||||
This documents the steps that were taken to create our test AD server used by the integration tests.
|
||||
The integration tests use LDAPS and StartTLS to connect to the AD server.
|
||||
|
||||
## Create a Windows Server VM and configure it as an AD Domain Controller
|
||||
|
||||
The steps in this section were mostly inspired by
|
||||
https://cloud.google.com/architecture/deploy-an-active-directory-forest-on-compute-engine.
|
||||
|
||||
From your Mac, create a VPC, subnet, firewall rules, admin password, reserved static IP, and the VM itself.
|
||||
|
||||
On your Mac:
|
||||
|
||||
```shell
|
||||
# Login as yourself.
|
||||
gcloud auth login
|
||||
|
||||
# Set some variables.
|
||||
project="REDACTED" # Change this to be the actual project name before running these commands.
|
||||
region="us-west1"
|
||||
zone="us-west1-c"
|
||||
vpc_name="ad"
|
||||
|
||||
# Create VPC.
|
||||
gcloud compute networks create ${vpc_name} \
|
||||
--project ${project} \
|
||||
--description "VPC network to deploy Active Directory" \
|
||||
--subnet-mode custom
|
||||
|
||||
# Create subnet.
|
||||
# The google tutorial says to "enable Private Google Access so that Windows can activate without internet access."
|
||||
gcloud compute networks subnets create domain-controllers \
|
||||
--project ${project} --region ${region} \
|
||||
--network ${vpc_name} \
|
||||
--range "10.0.0.0/28" \
|
||||
--enable-private-ip-google-access
|
||||
|
||||
# Create a firewall rule to allow RDP. Find out what your public IP address is by going to https://whatismyipaddress.com.
|
||||
# Replace the X.X.X.X placeholder address shown here with your real IPv4 address.
|
||||
my_ip=X.X.X.X
|
||||
gcloud compute firewall-rules create allow-rdp-ingress-to-addc \
|
||||
--project ${project} \
|
||||
--direction INGRESS \
|
||||
--action allow \
|
||||
--rules tcp:3389 \
|
||||
--source-ranges "${my_ip}/32" \
|
||||
--target-tags ad-domaincontroller \
|
||||
--network ${vpc_name} \
|
||||
--priority 10000
|
||||
|
||||
# Allow LDAPS (port 636) from the whole internet.
|
||||
gcloud compute firewall-rules create allow-ldaps-ingress-to-addc \
|
||||
--project ${project} \
|
||||
--direction INGRESS \
|
||||
--action allow \
|
||||
--rules tcp:636 \
|
||||
--source-ranges "0.0.0.0/0" \
|
||||
--target-tags ad-domaincontroller \
|
||||
--network ${vpc_name} \
|
||||
--priority 10000
|
||||
|
||||
# Allow LDAP (port 389) from the whole internet, to allow the integration tests to use StartTLS.
|
||||
gcloud compute firewall-rules create allow-ldap-ingress-to-addc \
|
||||
--project ${project} \
|
||||
--direction INGRESS \
|
||||
--action allow \
|
||||
--rules tcp:389 \
|
||||
--source-ranges "0.0.0.0/0" \
|
||||
--target-tags ad-domaincontroller \
|
||||
--network ${vpc_name} \
|
||||
--priority 10000
|
||||
|
||||
# Reserve a static public IP address for the domain controller VM.
|
||||
addressOfDc1=$(gcloud compute addresses create ad-domain-controller \
|
||||
--project ${project} --region ${region} \
|
||||
--format="value(address)")
|
||||
|
||||
# Create an admin password for the Administrator user on Windows, and save it to secrets manager.
|
||||
password="$(openssl rand -hex 8)-$(openssl rand -hex 8)"
|
||||
echo -n "$password" > password.tmp
|
||||
gcloud secrets create active-directory-dc1-password \
|
||||
--project ${project} \
|
||||
--data-file password.tmp
|
||||
rm password.tmp
|
||||
|
||||
# This creates a service account called ad-domaincontroller@PROJECT_NAME.iam.gserviceaccount.com
|
||||
# (where PROJECT_NAME is the actual GCP project name) and sets the account name to the
|
||||
# variable $dcServiceAccount.
|
||||
dcServiceAccount=$(gcloud iam service-accounts create ad-domaincontroller \
|
||||
--project ${project} \
|
||||
--display-name "AD Domain Controller VM Service Account" \
|
||||
--format "value(email)")
|
||||
|
||||
# Allow the new service account to temporarily read the Windows admin password from secret manager.
|
||||
# The following `date` command might only work on MacOS. It prints the time like this: 2024-10-23T19:20:36Z
|
||||
one_hour_from_now=$(TZ=UTC date -v "+1H" +"%Y-%m-%dT%H:%M:%SZ")
|
||||
gcloud secrets add-iam-policy-binding active-directory-dc1-password \
|
||||
--project ${project} \
|
||||
"--member=serviceAccount:$dcServiceAccount" \
|
||||
--role=roles/secretmanager.secretAccessor \
|
||||
--condition="title=Expires after 1h,expression=request.time < timestamp('$one_hour_from_now')"
|
||||
|
||||
# Optional: list all bindings to see the binding that you just created.
|
||||
gcloud secrets get-iam-policy active-directory-dc1-password \
|
||||
--project ${project}
|
||||
|
||||
# Create a powershell startup script in a local file.
|
||||
cat <<"EOF" > dc-startup.ps1
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
#
|
||||
# Only run the script if the VM is not a domain controller already.
|
||||
#
|
||||
if ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType -eq 2) {
|
||||
exit
|
||||
}
|
||||
|
||||
#
|
||||
# Read configuration from metadata.
|
||||
#
|
||||
Import-Module "${Env:ProgramFiles}\Google\Compute Engine\sysprep\gce_base.psm1"
|
||||
|
||||
Write-Host "Reading metadata..."
|
||||
$ActiveDirectoryDnsDomain = Get-MetaData -Property "attributes/ActiveDirectoryDnsDomain" -instance_only
|
||||
$ActiveDirectoryNetbiosDomain = Get-MetaData -Property "attributes/ActiveDirectoryNetbiosDomain" -instance_only
|
||||
$ProjectId = Get-MetaData -Property "project-id" -project_only
|
||||
$AccessToken = (Get-MetaData -Property "service-accounts/default/token" | ConvertFrom-Json).access_token
|
||||
|
||||
#
|
||||
# Read the DSRM password from secret manager.
|
||||
#
|
||||
Write-Host "Reading secret from secret manager..."
|
||||
$Secret = (Invoke-RestMethod `
|
||||
-Headers @{
|
||||
"Metadata-Flavor" = "Google";
|
||||
"x-goog-user-project" = $ProjectId;
|
||||
"Authorization" = "Bearer $AccessToken"} `
|
||||
-Uri "https://secretmanager.googleapis.com/v1/projects/$ProjectId/secrets/active-directory-dc1-password/versions/latest:access")
|
||||
$DsrmPassword = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Secret.payload.data))
|
||||
$DsrmPassword = ConvertTo-SecureString -AsPlainText $DsrmPassword -force
|
||||
|
||||
#
|
||||
# Promote.
|
||||
#
|
||||
Write-Host "Setting administrator password..."
|
||||
Set-LocalUser -Name Administrator -Password $DsrmPassword
|
||||
|
||||
Write-Host "Creating a new forest $ActiveDirectoryDnsDomain ($ActiveDirectoryNetbiosDomain)..."
|
||||
Install-ADDSForest `
|
||||
-DomainName $ActiveDirectoryDnsDomain `
|
||||
-DomainNetbiosName $ActiveDirectoryNetbiosDomain `
|
||||
-SafeModeAdministratorPassword $DsrmPassword `
|
||||
-DomainMode Win2008R2 `
|
||||
-ForestMode Win2008R2 `
|
||||
-InstallDns `
|
||||
-CreateDnsDelegation:$False `
|
||||
-NoRebootOnCompletion:$True `
|
||||
-Confirm:$false
|
||||
|
||||
#
|
||||
# Configure DNS.
|
||||
#
|
||||
Write-Host "Configuring DNS settings..."
|
||||
Get-Netadapter| Disable-NetAdapterBinding -ComponentID ms_tcpip6
|
||||
Set-DnsClientServerAddress `
|
||||
-InterfaceIndex (Get-NetAdapter -Name Ethernet).InterfaceIndex `
|
||||
-ServerAddresses 127.0.0.1
|
||||
|
||||
#
|
||||
# Enable LSA protection.
|
||||
#
|
||||
New-ItemProperty `
|
||||
-Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" `
|
||||
-Name "RunAsPPL" `
|
||||
-Value 1 `
|
||||
-PropertyType DWord
|
||||
|
||||
Write-Host "Restarting to apply all settings..."
|
||||
Restart-Computer
|
||||
EOF
|
||||
|
||||
# Create a domain controller VM.
|
||||
# E2 are the cheapest VMs. e2-medium has 2 vCPUs (shared with other customers) and 4 GB of memory.
|
||||
# See https://cloud.google.com/compute/docs/general-purpose-machines#e2-shared-core.
|
||||
# When we originally set up this VM, we actually started it as n2-standard-2 and after we
|
||||
# finished setting up everything as shown in this guide, then we stopped the VM and changed its
|
||||
# type to e2-medium and started the VM again. Maybe it would work fine to create it as
|
||||
# e2-medium from the beginning, but note that we didn't actually test that.
|
||||
gcloud compute instances create active-directory-dc1 \
|
||||
--project ${project} \
|
||||
--zone ${zone} \
|
||||
--image-family windows-2022 \
|
||||
--image-project windows-cloud \
|
||||
--machine-type e2-medium \
|
||||
--tags ad-domaincontroller \
|
||||
--metadata "ActiveDirectoryDnsDomain=activedirectory.test.pinniped.dev,ActiveDirectoryNetbiosDomain=pinniped-ad,sysprep-specialize-script-ps1=Install-WindowsFeature AD-Domain-Services -IncludeManagementTools; Install-WindowsFeature DNS,disable-account-manager=true" \
|
||||
--metadata-from-file windows-startup-script-ps1=dc-startup.ps1 \
|
||||
--address ${addressOfDc1} \
|
||||
--subnet=domain-controllers \
|
||||
--service-account "$dcServiceAccount" \
|
||||
--scopes cloud-platform \
|
||||
--shielded-integrity-monitoring \
|
||||
--shielded-secure-boot \
|
||||
--shielded-vtpm
|
||||
|
||||
# Monitor the initialization process of the first domain controller by viewing its serial port output.
|
||||
# It should install the sysprep stuff, reboot, run our startup script, and then reboot again.
|
||||
gcloud compute instances tail-serial-port-output active-directory-dc1 \
|
||||
--project ${project} \
|
||||
--zone ${zone}
|
||||
# Use CTRL-C to cancel tailing the output.
|
||||
```
|
||||
|
||||
## Update DNS
|
||||
|
||||
Update the Cloud DNS entry for `activedirectory.test.pinniped.dev.` to be an "A" record pointing to the
|
||||
public static IP of the VM. This is easier to do in the Cloud DNS UI in your browser.
|
||||
It would take many gcloud CLI commands to accomplish the same task.
|
||||
|
||||
## Configure test users and groups
|
||||
|
||||
Make sure you have an RDP client installed. On a Mac, you can install RDP from the App Store.
|
||||
It was recently renamed "Windows App".
|
||||
|
||||
Note: To copy/paste in the RDP client, you may need to use CTRL-C/CTRL-V if CMD-C/CMD-V don't work.
|
||||
|
||||
RDP into the Windows VM. To connect, use `activedirectory.test.pinniped.dev` as the name of the server,
|
||||
the username `Administrator`, and the password from the `active-directory-dc1-password` entry in Secrets Manager.
|
||||
You can ignore the RDP certificate error.
|
||||
|
||||
In your RDP session, open Powershell. Then run the following commands to add some users and groups,
|
||||
change the password policy, and grant some permissions.
|
||||
|
||||
Before running the commands, replace the redacted passwords as follows:
|
||||
- The value for `REDACTED_BIND_USER_PASSWORD` can be found at `aws-ad-bind-account-password` in the `concourse-secrets` secret
|
||||
- The value for `REDACTED_PINNY_USER_PASSWORD` can be found at `aws-ad-user-password` in the `concourse-secrets` secret
|
||||
- The value for `REDACTED_DEACTIVATED_USER_PASSWORD` can be found at `aws-ad-deactivated-user-password` in the `concourse-secrets` secret
|
||||
|
||||
```shell
|
||||
New-ADOrganizationalUnit -Name "pinniped-ad" `
|
||||
-ProtectedFromAccidentalDeletion $false
|
||||
|
||||
New-ADOrganizationalUnit -Name "Users" `
|
||||
-Path "OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" `
|
||||
-ProtectedFromAccidentalDeletion $false
|
||||
|
||||
New-ADOrganizationalUnit -Name "test-users" `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" `
|
||||
-Description "integration tests will create and delete ephemeral users here" `
|
||||
-ProtectedFromAccidentalDeletion $false
|
||||
|
||||
# Print all OUs to validate that they were created.
|
||||
Get-ADOrganizationalUnit -Filter *
|
||||
|
||||
New-ADUser -Name "Bind User" -SamAccountName "bind-user" -GivenName "Bind" -Surname "User" -DisplayName "Bind User" `
|
||||
-UserPrincipalName "bind-user@activedirectory.test.pinniped.dev" `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" `
|
||||
-AccountPassword (ConvertTo-SecureString "REDACTED_BIND_USER_PASSWORD" -AsPlainText -Force) `
|
||||
-Enabled $true -PasswordNeverExpires $true
|
||||
|
||||
# Note that the value of EmailAddress is not a real email address, but that's okay.
|
||||
New-ADUser -Name "Pinny Seal" -SamAccountName "pinny" -GivenName "Pinny" -Surname "Seal" -DisplayName "Pinny Seal" `
|
||||
-UserPrincipalName "pinny@activedirectory.test.pinniped.dev" `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" `
|
||||
-EmailAddress "tanzu-user-authentication@groups.vmware.com" `
|
||||
-AccountPassword (ConvertTo-SecureString "REDACTED_PINNY_USER_PASSWORD" -AsPlainText -Force) `
|
||||
-Enabled $true -PasswordNeverExpires $true
|
||||
|
||||
New-ADUser -Name "Deactivated User" -SamAccountName "deactivated-user" -GivenName "Deactivated" -Surname "User" -DisplayName "Deactivated User" `
|
||||
-UserPrincipalName "deactivated-user@activedirectory.test.pinniped.dev" `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" `
|
||||
-AccountPassword (ConvertTo-SecureString "REDACTED_DEACTIVATED_USER_PASSWORD" -AsPlainText -Force) `
|
||||
-Enabled $false -PasswordNeverExpires $true
|
||||
|
||||
# Take note of the pinny account's ObjectGUID. You will need to edit the concourse-secrets secret later to update this GUID value.
|
||||
# This value should look something like "288188dd-ab76-4f61-b6e4-c72e081502c5".
|
||||
Get-ADUser pinny -Properties * | Select SamaccountName,ObjectGUID
|
||||
|
||||
# Print all users to validate that they were created.
|
||||
Get-ADUser -Filter *
|
||||
|
||||
New-ADGroup -Name "Marine Mammals" -SamAccountName "Marine Mammals" -DisplayName "Marine Mammals" `
|
||||
-GroupCategory Security -GroupScope Global `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev"
|
||||
|
||||
Add-ADGroupMember -Identity "Marine Mammals" -Members "pinny"
|
||||
|
||||
New-ADGroup -Name "Mammals" -SamAccountName "Mammals" -DisplayName "Mammals" `
|
||||
-GroupCategory Security -GroupScope Global `
|
||||
-Path "OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev"
|
||||
|
||||
Add-ADGroupMember -Identity "Mammals" -Members "Marine Mammals"
|
||||
|
||||
# Change the default password policy. There are some integration tests that rely on this.
|
||||
# This is the equivalent of doing this in the Windows "Active Directory Administrative Center" UI:
|
||||
# check "enforce account lockout policy", give it 20 failed attempts and a 15-minute reset, then
|
||||
# uncheck "enforce minimum password age" so we can change the password immediately upon creating a user.
|
||||
Set-ADDefaultDomainPasswordPolicy -Identity "activedirectory.test.pinniped.dev" `
|
||||
-LockoutThreshold 20 -LockoutDuration "00:15:00" -LockoutObservationWindow "00:15:00" `
|
||||
-MinPasswordAge 0
|
||||
|
||||
# Print the policy to validate that it was updated.
|
||||
Get-ADDefaultDomainPasswordPolicy
|
||||
|
||||
# We need to allow the bind-user to create/delete/edit users and groups within the test-users OU, because several
|
||||
# integration tests want to crate/delete/edit ephemeral test users and groups.
|
||||
# These access control steps were inspired by https://the-itguy.de/delegate-access-in-active-directory-with-powershell/.
|
||||
# This is intended to be the equivalent of using the UI to assign permissions like this: right click on "test-users",
|
||||
# select Delegate Control, select "bind-user" as the user, select "create, delete and manage user accounts" and
|
||||
# "reset user passwords" as the tasks to delegate.
|
||||
function New-ADDGuidMap
|
||||
{
|
||||
$rootdse = Get-ADRootDSE
|
||||
$guidmap = @{ }
|
||||
$GuidMapParams = @{
|
||||
SearchBase = ($rootdse.SchemaNamingContext)
|
||||
LDAPFilter = "(schemaidguid=*)"
|
||||
Properties = ("lDAPDisplayName", "schemaIDGUID")
|
||||
}
|
||||
Get-ADObject @GuidMapParams | ForEach-Object { $guidmap[$_.lDAPDisplayName] = [System.GUID]$_.schemaIDGUID }
|
||||
return $guidmap
|
||||
}
|
||||
$GuidMap = New-ADDGuidMap
|
||||
$BindUserSID = New-Object System.Security.Principal.SecurityIdentifier (Get-ADUser "bind-user").SID
|
||||
$acl = Get-Acl -Path "AD:OU=test-users,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev"
|
||||
$ace1 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $BindUserSID, "GenericAll", "Allow", "Descendents", $GuidMap["user"]
|
||||
$ace2 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $BindUserSID, "CreateChild, DeleteChild", "Allow", $GuidMap["user"], "All"
|
||||
$ace3 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $BindUserSID, "GenericAll", "Allow", "Descendents", $GuidMap["group"]
|
||||
$ace4 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $BindUserSID, "CreateChild, DeleteChild", "Allow", $GuidMap["group"], "All"
|
||||
$acl.AddAccessRule($ace1)
|
||||
$acl.AddAccessRule($ace2)
|
||||
$acl.AddAccessRule($ace3)
|
||||
$acl.AddAccessRule($ace4)
|
||||
Set-Acl -Path "AD:OU=test-users,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev" -AclObject $acl
|
||||
|
||||
# Print the access control rules that were just applied.
|
||||
$acl = Get-Acl -Path "AD:OU=test-users,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev"
|
||||
$acl.Access | Where-Object { $_.IdentityReference -eq "pinniped-ad\bind-user" }
|
||||
```
|
||||
|
||||
If you would like to see these OUs, users, and groups in the UI, you can open the "Active Directory Users and Computers"
|
||||
app in your RDP session.
|
||||
|
||||
## Configure a CA and a serving certificate for LDAPS
|
||||
|
||||
Now we need to create and configure a TLS serving certificate for LDAPS.
|
||||
|
||||
The certificate needs to include two hostnames. One of the hostnames is the name that the AD server
|
||||
thinks is its own hostname (`active-directory-dc1.activedirectory.test.pinniped.dev`).
|
||||
This is how the AD server will decide to use this cert for the LDAPS port.
|
||||
The other hostname is the one that clients will use when making connections from the outside
|
||||
(`activedirectory.test.pinniped.dev`) so they can validate the server certificate.
|
||||
|
||||
The steps here were inspired by https://gist.github.com/magnetikonline/0ccdabfec58eb1929c997d22e7341e45.
|
||||
|
||||
On your mac:
|
||||
|
||||
```shell
|
||||
# On your Mac: Create a self-signed CA public/private keypair.
|
||||
openssl req -x509 -newkey rsa:4096 \
|
||||
-keyout ad-ca.key -out ad-ca.crt \
|
||||
-sha256 -days 36500 -nodes \
|
||||
-subj "/C=US/ST=California/L=San Francisco/O=Pinniped/OU=Pinniped CI/CN=Pinniped AD CA"
|
||||
|
||||
# Copy the public key to your clipboard.
|
||||
cat ad-ca.crt| pbcopy
|
||||
```
|
||||
|
||||
In Powershell terminal:
|
||||
|
||||
```shell
|
||||
# In your Windows RDP session's Powershell terminal, put the content of the clipboard into a file.
|
||||
# Note that if you copy/paste this command to your RDP session, then you need to pbcopy the public
|
||||
# key again before you hit return for this command.
|
||||
Get-Clipboard | Out-File -FilePath "C:\users\administrator\desktop\ca.crt"
|
||||
|
||||
# In Powershell terminal, check that the file exists and looks correct.
|
||||
type "C:\users\administrator\desktop\ca.crt"
|
||||
|
||||
# Import root certificate into trusted store of domain controller in your Powershell terminal:
|
||||
Import-Certificate -FilePath "C:\users\administrator\desktop\ca.crt" -CertStoreLocation Cert:\LocalMachine\Root
|
||||
```
|
||||
|
||||
If you want to validate that this was imported, open the UI tool called "Manage computer certificates"
|
||||
and look in the folder called "Trusted Root Certification Authorities\Certificates".
|
||||
If the UI was already open, click the refresh button.
|
||||
|
||||
Copy the following file contents to your clipboard:
|
||||
|
||||
```shell
|
||||
[Version]
|
||||
Signature="$Windows NT$"
|
||||
|
||||
[NewRequest]
|
||||
Subject = "CN=activedirectory.test.pinniped.dev"
|
||||
KeySpec = 1
|
||||
KeyLength = 2048
|
||||
Exportable = TRUE
|
||||
MachineKeySet = TRUE
|
||||
SMIME = FALSE
|
||||
PrivateKeyArchive = FALSE
|
||||
UserProtected = FALSE
|
||||
UseExistingKeySet = FALSE
|
||||
ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
|
||||
ProviderType = 12
|
||||
RequestType = PKCS10
|
||||
KeyUsage = 0xa0
|
||||
|
||||
[EnhancedKeyUsageExtension]
|
||||
OID = 1.3.6.1.5.5.7.3.1 ; Server Authentication
|
||||
|
||||
[Extensions]
|
||||
2.5.29.17 = "{text}"
|
||||
_continue_ = "DNS=activedirectory.test.pinniped.dev"
|
||||
_continue_ = "DNS=active-directory-dc1.activedirectory.test.pinniped.dev"
|
||||
```
|
||||
|
||||
In Powershell terminal:
|
||||
|
||||
```shell
|
||||
# In your Windows RDP session's Powershell terminal, put the content of the clipboard into a file.
|
||||
# Note that if you copy/paste this command to your RDP session, then you need to copy the file contents
|
||||
# from above again before you hit return for this command.
|
||||
Get-Clipboard | Out-File -FilePath "C:\users\administrator\desktop\request.inf"
|
||||
|
||||
# In Powershell terminal, check that the file exists and looks correct.
|
||||
type "C:\users\administrator\desktop\request.inf"
|
||||
|
||||
# Create a CSR. This command will also generate a private key for the AD server and save it.
|
||||
certreq -new "C:\users\administrator\desktop\request.inf" "C:\users\administrator\desktop\client.csr"
|
||||
|
||||
# Show the CSR.
|
||||
type "C:\users\administrator\desktop\client.csr"
|
||||
|
||||
# Copy the content of this file to your clipboard.
|
||||
Get-Content "C:\users\administrator\desktop\client.csr" | Set-Clipboard
|
||||
```
|
||||
|
||||
On your mac:
|
||||
|
||||
```shell
|
||||
# On your Mac, use the CA to issue a serving cert based on the CSR.
|
||||
pbpaste > client.csr
|
||||
|
||||
cat <<EOF > v3ext.txt
|
||||
keyUsage=digitalSignature,keyEncipherment
|
||||
extendedKeyUsage=serverAuth
|
||||
subjectKeyIdentifier=hash
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
DNS.1 = activedirectory.test.pinniped.dev
|
||||
DNS.2 = active-directory-dc1.activedirectory.test.pinniped.dev
|
||||
EOF
|
||||
|
||||
# Create a cert from the CSR signed by the CA.
|
||||
openssl x509 \
|
||||
-req -days 36500 \
|
||||
-in client.csr -CA ad-ca.crt -CAkey ad-ca.key -extfile v3ext.txt \
|
||||
-set_serial 01 -out client.crt
|
||||
|
||||
# Inspect the generated certificate.
|
||||
# Ensure the following X509v3 extensions are all present:
|
||||
# Key Usage: Digital Signature, Key Encipherment
|
||||
# Extended Key Usage: TLS Web Server Authentication
|
||||
# Subject Key Identifier
|
||||
# Subject Alternative Name with 2 DNS hostnames
|
||||
# Authority Key Identifier
|
||||
openssl x509 -in client.crt -text
|
||||
|
||||
# Copy the generated cert.
|
||||
cat client.crt | pbcopy
|
||||
```
|
||||
|
||||
In Powershell terminal:
|
||||
|
||||
```shell
|
||||
# In your Windows RDP session's Powershell terminal, put the content of the clipboard into a file.
|
||||
# Note that if you copy/paste this command to your RDP session, then you need to pbcopy the file contents
|
||||
# from above again before you hit return for this command.
|
||||
Get-Clipboard | Out-File -FilePath "C:\users\administrator\desktop\client.crt"
|
||||
|
||||
# In Powershell terminal, check that the file exists and looks correct.
|
||||
type "C:\users\administrator\desktop\client.crt"
|
||||
|
||||
# Add the serving certificate to Windows. This will also automatically associate it to the private key that you
|
||||
# generated with the previous usage of certreq.
|
||||
certreq -accept "C:\users\administrator\desktop\client.crt"
|
||||
|
||||
# If you want to validate that this was imported, open the UI tool called "Manage computer certificates"
|
||||
# and look in the folder called "Personal\Certificates". If the UI was already open, click the refresh button.
|
||||
# Double click on the cert. Ensure that it says, "you have a private key that corresponds to this certificate".
|
||||
# Next, we need to reboot the VM for the cert to get picked up and used for serving incoming LDAPS connections.
|
||||
# After showing you a warning dialog box, this should terminate your RDP session and stop the VM.
|
||||
shutdown /s
|
||||
```
|
||||
|
||||
Wait for the VM to stop, then start the VM again from your Mac:
|
||||
|
||||
```shell
|
||||
gcloud compute instances start active-directory-dc1 --project ${project} --zone ${zone}
|
||||
```
|
||||
|
||||
Wait for the VM to finish booting. Then we can confirm that LDAPS is working. On your Mac:
|
||||
|
||||
```shell
|
||||
# Check that serving cert is being returned on the LDAPS port. This command should show the cert chain.
|
||||
# It should also verify the server cert using our CA. The output should include "Verify return code: 0 (ok)".
|
||||
openssl s_client -connect activedirectory.test.pinniped.dev:636 -showcerts -CAfile ad-ca.crt < /dev/null
|
||||
|
||||
# Unfortunately, the ldapsearch command that comes pre-installed on MacOS does not seem to respect
|
||||
# the LDAPTLS_CACERT env variable. So it will not be able to validate the server certificates.
|
||||
# As a workaround, we can use docker to run ldapsearch commands in a linux container.
|
||||
|
||||
# Test the regular LDAP port by issuing a query on your Mac. The -ZZ option asks it to use StartTLS.
|
||||
# This should list all users. Replace REDACTED_BIND_USER_PASSWORD with the real password.
|
||||
docker run -v "$(pwd):/certs" -e LDAPTLS_CACERT="/certs/ad-ca.crt" --rm -it bitnami/openldap \
|
||||
ldapsearch -d8 -v -x -ZZ -H 'ldap://activedirectory.test.pinniped.dev' \
|
||||
-D 'CN=Bind User,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev' \
|
||||
-w 'REDACTED_BIND_USER_PASSWORD' \
|
||||
-b 'OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev' \
|
||||
-s sub \
|
||||
'(objectClass=user)' '*'
|
||||
|
||||
# Test the LDAPS port by issuing a query on your Mac. This should list all users.
|
||||
# Replace REDACTED_BIND_USER_PASSWORD with the real password.
|
||||
docker run -v "$(pwd):/certs" -e LDAPTLS_CACERT="/certs/ad-ca.crt" --rm -it bitnami/openldap \
|
||||
ldapsearch -d8 -v -x -H 'ldaps://activedirectory.test.pinniped.dev' \
|
||||
-D 'CN=Bind User,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev' \
|
||||
-w 'REDACTED_BIND_USER_PASSWORD' \
|
||||
-b 'OU=pinniped-ad,DC=activedirectory,DC=test,DC=pinniped,DC=dev' \
|
||||
-s sub \
|
||||
'(objectClass=user)' '*'
|
||||
```
|
||||
|
||||
## Update the `concourse-secrets` secret in GCP Secrets Manager
|
||||
|
||||
On your Mac:
|
||||
|
||||
```shell
|
||||
# Copy the CA's public cert.
|
||||
cat ad-ca.crt | base64 | pbcopy
|
||||
|
||||
# cd to your local clone of the `ci` branch of the pinniped repo
|
||||
cd pinniped-ci-branch
|
||||
|
||||
# Edit the secret.
|
||||
./hack/edit-gcloud-secret.sh concourse-secret
|
||||
# This opens vim to edit the secret.
|
||||
# Paste the cert as the value for `aws-ad-ca-data`.
|
||||
# Also edit the the value of `aws-ad-user-unique-id-attribute-value`. The value should be the ObjectGUID of the pinny
|
||||
# user that you created in the steps above.
|
||||
# Save your changes, exit vim, and when prompted say that you want to save this as the new version of concourse-secrets.
|
||||
```
|
||||
|
||||
## Confirm that Active Directory integration tests can pass
|
||||
|
||||
Use these commands run all the Active Directory integration tests on your Mac.
|
||||
The `-run` filter is based on the tests as they existed at the time of writing this doc.
|
||||
You can find AD tests by searching for `SkipTestWhenActiveDirectoryIsUnavailable`.
|
||||
|
||||
On your Mac:
|
||||
|
||||
```shell
|
||||
# Login so we can read the secrets from GCP Secret Manager.
|
||||
gcloud auth login
|
||||
|
||||
# cd to your local git clone
|
||||
cd pinniped
|
||||
|
||||
# Compile and install onto a local kind cluster.
|
||||
./hack/prepare-for-integration-tests.sh -c --get-active-directory-vars "../pinniped-ci-branch/hack/get-aws-ad-env-vars.sh"
|
||||
|
||||
# Run all the tests that depend on AD.
|
||||
source /tmp/integration-test-env && go test -v -race -count 1 -timeout 0 ./test/integration \
|
||||
-run "/TestSupervisorLogin_Browser/active_directory|/TestE2EFullIntegration_Browser/with_Supervisor_ActiveDirectory|/TestActiveDirectoryIDPPhaseAndConditions_Parallel|/TestSupervisorWarnings_Browser/Active_Directory"
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
On your Mac:
|
||||
|
||||
```shell
|
||||
# Remove all bindings for the service account from the secret.
|
||||
# The binding was only needed during the first boot of the VM.
|
||||
gcloud secrets remove-iam-policy-binding active-directory-dc1-password \
|
||||
--project ${project} \
|
||||
--member "serviceAccount:${dcServiceAccount}" --role roles/secretmanager.secretAccessor \
|
||||
--all
|
||||
|
||||
# Remove the firewall rule which allows incoming RDP connections.
|
||||
# If you need to RDP to this AD VM in the future, then you will need to create
|
||||
# a new firewall rule to allow it.
|
||||
gcloud compute firewall-rules delete allow-rdp-ingress-to-addc \
|
||||
--project ${project} \
|
||||
--quiet
|
||||
|
||||
# Remove all temp files. It's okay to remove the private key for our CA because we
|
||||
# created certs that are good for 100 years, as long as you have already added the
|
||||
# public cert to the concourse-secrets secret. If we need to create a new AD VM, we
|
||||
# can also create a new CA.
|
||||
rm ad-ca.crt ad-ca.key client.crt client.csr v3ext.txt
|
||||
```
|
||||
9
ADOPTERS.md
Normal file
9
ADOPTERS.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Pinniped Adopters
|
||||
|
||||
These organizations are using Pinniped.
|
||||
|
||||
* [VMware Tanzu](https://tanzu.vmware.com/) ([Tanzu Mission Control](https://tanzu.vmware.com/mission-control))
|
||||
|
||||
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.
|
||||
@@ -1 +1,84 @@
|
||||
Please see https://github.com/vmware/pinniped/blob/main/CODE_OF_CONDUCT.md
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [oss-coc@vmware.com](mailto:oss-coc@vmware.com). All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
||||
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
||||
|
||||
166
CONTRIBUTING.md
166
CONTRIBUTING.md
@@ -1 +1,165 @@
|
||||
Please see https://github.com/vmware/pinniped/blob/main/CONTRIBUTING.md
|
||||
# Contributing to Pinniped
|
||||
|
||||
Contributions to Pinniped are welcome. Here are some things to help you get started.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please see the [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||
|
||||
## Project Scope
|
||||
|
||||
Learn about the [scope](https://pinniped.dev/docs/scope/) of the project.
|
||||
|
||||
## Meeting with the Maintainers
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Issues
|
||||
|
||||
Need an idea for a project to get started contributing? Take a look at the open
|
||||
[issues](https://github.com/vmware-tanzu/pinniped/issues).
|
||||
Also check to see if any open issues are labeled with
|
||||
["good first issue"](https://github.com/vmware-tanzu/pinniped/labels/good%20first%20issue)
|
||||
or ["help wanted"](https://github.com/vmware-tanzu/pinniped/labels/help%20wanted).
|
||||
|
||||
### Bugs
|
||||
|
||||
To file a bug report, please first open an
|
||||
[issue](https://github.com/vmware-tanzu/pinniped/issues/new?template=bug_report.md). The project team
|
||||
will work with you on your bug report.
|
||||
|
||||
Once the bug has been validated, a [pull request](https://github.com/vmware-tanzu/pinniped/compare)
|
||||
can be opened to fix the bug.
|
||||
|
||||
For specifics on what to include in your bug report, please follow the
|
||||
guidelines in the issue and pull request templates.
|
||||
|
||||
### Features
|
||||
|
||||
To suggest a feature, please first open an
|
||||
[issue](https://github.com/vmware-tanzu/pinniped/issues/new?template=feature-proposal.md)
|
||||
and tag it with `proposal`, or create a new [Discussion](https://github.com/vmware-tanzu/pinniped/discussions).
|
||||
The project team will work with you on your feature request.
|
||||
|
||||
Once the feature request has been validated, a [pull request](https://github.com/vmware-tanzu/pinniped/compare)
|
||||
can be opened to implement the feature.
|
||||
|
||||
For specifics on what to include in your feature request, please follow the
|
||||
guidelines in the issue and pull request templates.
|
||||
|
||||
## CLA
|
||||
|
||||
We welcome contributions from everyone but we can only accept them if you sign
|
||||
our Contributor License Agreement (CLA). If you would like to contribute and you
|
||||
have not signed it, our CLA-bot will walk you through the process when you open
|
||||
a Pull Request. For questions about the CLA process, see the
|
||||
[FAQ](https://cla.vmware.com/faq) or submit a question through the GitHub issue
|
||||
tracker.
|
||||
|
||||
## Building
|
||||
|
||||
The [Dockerfile](Dockerfile) at the root of the repo can be used to build and
|
||||
package the code. After making a change to the code, rebuild the docker image with the following command.
|
||||
|
||||
```bash
|
||||
# From the root directory of the repo...
|
||||
docker build .
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Running Lint
|
||||
|
||||
```bash
|
||||
./hack/module.sh lint
|
||||
```
|
||||
|
||||
### Running Unit Tests
|
||||
|
||||
```bash
|
||||
./hack/module.sh units
|
||||
```
|
||||
|
||||
### Running Integration Tests
|
||||
|
||||
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/))
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
1. Create a local Kubernetes cluster using `kind`:
|
||||
|
||||
```bash
|
||||
./hack/kind-up.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
|
||||
```
|
||||
|
||||
To uninstall the test environment, run `./hack/tilt-down.sh`.
|
||||
To destroy the local Kubernetes cluster, run `./hack/kind-down.sh`.
|
||||
|
||||
### Observing Tests on the Continuous Integration Environment
|
||||
|
||||
[CI](https://hush-house.pivotal.io/teams/tanzu-user-auth/pipelines/pinniped-pull-requests)
|
||||
will not be triggered on a pull request until the pull request is reviewed and
|
||||
approved for CI by a project [maintainer](MAINTAINERS.md). Once CI is triggered,
|
||||
the progress and results will appear on the Github page for that
|
||||
[pull request](https://github.com/vmware-tanzu/pinniped/pulls) as checks. Links
|
||||
will appear to view the details of each check.
|
||||
|
||||
## Documentation
|
||||
|
||||
Any pull request which adds a new feature or changes the behavior of any feature which was previously documented
|
||||
should include updates to the documentation. All documentation lives in this repository. This project aspires to
|
||||
follow the Kubernetes [documentation style guide](https://kubernetes.io/docs/contribute/style/style-guide).
|
||||
|
||||
## Pre-commit Hooks
|
||||
|
||||
This project uses [pre-commit](https://pre-commit.com/) to agree on some conventions about whitespace/file encoding.
|
||||
|
||||
```bash
|
||||
$ brew install pre-commit
|
||||
[...]
|
||||
$ pre-commit install
|
||||
pre-commit installed at .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
## Becoming a Pinniped Maintainer
|
||||
|
||||
Regular contributors who are active in the Pinniped community and who have contributed at least several
|
||||
significant pull requests may be considered for promotion to become a maintainer upon request. Please
|
||||
contact an existing [maintainer](MAINTAINERS.md) if you would like to be considered.
|
||||
|
||||
42
Dockerfile
Normal file
42
Dockerfile
Normal file
@@ -0,0 +1,42 @@
|
||||
# Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM golang:1.15.6 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 pkg ./pkg
|
||||
COPY internal ./internal
|
||||
COPY hack ./hack
|
||||
|
||||
# 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/...
|
||||
|
||||
# Use a runtime image based on Debian slim
|
||||
FROM debian:10.7-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 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
|
||||
|
||||
# Document the ports
|
||||
EXPOSE 8080 8443
|
||||
|
||||
# Run as non-root for security posture
|
||||
USER 1001:1001
|
||||
|
||||
# Set the entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/pinniped-concierge"]
|
||||
@@ -1 +1,19 @@
|
||||
Please see https://github.com/vmware/pinniped/blob/main/MAINTAINERS.md
|
||||
# Pinniped Maintainers
|
||||
|
||||
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/) |
|
||||
|
||||
## Pinniped Contributors & Stakeholders
|
||||
|
||||
| Feature Area | Lead |
|
||||
| ----------------------------- | :---------------------: |
|
||||
| Technical Lead | Matt Moyer (mattmoyer) |
|
||||
| Product Management | Pablo Schuhmacher (pabloschuhmacher) |
|
||||
|
||||
230
README.md
230
README.md
@@ -1,219 +1,55 @@
|
||||
# Pinniped's `ci` branch
|
||||
<img src="site/content/docs/img/pinniped_logo.svg" alt="Pinniped Logo" width="100%"/>
|
||||
|
||||
This `ci` branch contains the CI/CD tooling for [Pinniped](https://github.com/vmware/pinniped).
|
||||
## Overview
|
||||
|
||||
The documentation and code in this branch is mainly intended for the maintainers of Pinniped.
|
||||
Pinniped provides identity services to Kubernetes.
|
||||
|
||||
This branch is not intended to be merged to the `main` branch.
|
||||
Pinniped allows cluster administrators to easily plug in external identity
|
||||
providers (IDPs) into Kubernetes clusters. This is achieved via a uniform
|
||||
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.
|
||||
|
||||
The code in the branch previously lived in a private repository. It was made public by moving
|
||||
the code into the `ci` branch of the Pinniped repository in late 2024. The previous git history
|
||||
for these files was not copied from the private repository at the time of this migration.
|
||||
### Example Use Cases
|
||||
|
||||
## Reporting an issue in this branch
|
||||
* Your team uses a large enterprise IDP, and has many clusters that they
|
||||
manage. Pinniped provides:
|
||||
* Seamless and robust integration with the IDP
|
||||
* Easy installation across clusters of any type and origin
|
||||
* A simplified login flow across all clusters
|
||||
* Your team shares a single cluster. Pinniped provides:
|
||||
* Simple configuration to integrate an IDP
|
||||
* Individual, revocable identities
|
||||
|
||||
Found a bug or would like to make an enhancement request?
|
||||
Please report issues in [this repo](https://github.com/vmware/pinniped).
|
||||
### Architecture
|
||||
|
||||
## Reporting security vulnerabilities
|
||||
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
|
||||
distributions to make authentication possible.
|
||||
|
||||
Please follow the procedure described in [SECURITY.md](https://github.com/vmware/pinniped/blob/main/SECURITY.md).
|
||||
To learn more, see [architecture](https://pinniped.dev/docs/architecture/).
|
||||
|
||||
## Creating a release
|
||||
<img src="site/content/docs/img/pinniped_architecture.svg" alt="Pinniped Architecture Sketch" width="300px"/>
|
||||
|
||||
When the team is preparing to ship a release, a maintainer will create a new
|
||||
GitHub [Issue](https://github.com/vmware/pinniped/issues/new/choose) in this repo to
|
||||
collaboratively track progress on the release checklist. As tasks are completed,
|
||||
the team will check them off. When all the tasks are completed, the issue is closed.
|
||||
## Trying Pinniped
|
||||
|
||||
The release checklist is committed to this repo as an [issue template](https://github.com/vmware/pinniped/tree/main/.github/ISSUE_TEMPLATE/release_checklist.md).
|
||||
Care to kick the tires? It's easy to [install and try Pinniped](https://pinniped.dev/docs/demo/).
|
||||
|
||||
## Pipelines
|
||||
## Discussion
|
||||
|
||||
Pinniped uses [Concourse](https://concourse-ci.org) for CI/CD.
|
||||
We are currently running our Concourse on a network that can only be reached from inside the corporate network at [ci.pinniped.broadcom.net](https://ci.pinniped.broadcom.net).
|
||||
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.
|
||||
|
||||
The following pipelines are implemented in this branch. Not all pipelines are necessarily publicly visible, although our goal is to make them all visible.
|
||||
## Contributions
|
||||
|
||||
- `main`
|
||||
Contributions are welcome. Before contributing, please see the [contributing guide](CONTRIBUTING.md).
|
||||
|
||||
This is the main pipeline that runs on merges to `main`. It builds, tests, and (when manually triggered) releases from main.
|
||||
## Reporting Security Vulnerabilities
|
||||
|
||||
- `pull-requests`
|
||||
|
||||
This is a pipeline that triggers for each open pull request. It runs a smaller subset of the integration tests and validations as `pinniped`.
|
||||
|
||||
- `dockerfile-builders`
|
||||
|
||||
This pipeline builds a bunch of custom utility container images that are used in our CI and testing.
|
||||
|
||||
- `build-gi-cli` (a container image that includes the GitHub CLI)
|
||||
- `build-github-pr-resource` (a [fork](https://github.com/pinniped-ci-bot/github-pr-resource) of the `github-pr-resource` with support for gating PRs for untrusted users)
|
||||
- `build-code-coverage-uploader` (uploading code coverage during unit tests)
|
||||
- `build-eks-deployer-dockerfile` (deploying our app to EKS clusters)
|
||||
- `build-k8s-app-deployer-dockerfile` (deploying our app to clusters)
|
||||
- `build-pool-trigger-resource-dockerfile` (an updated implementation of the [pool-trigger-resource](https://github.com/cfmobile/pool-trigger-resource) for use in our CI)
|
||||
- `build-integration-test-runner-dockerfile` (running our integration tests)
|
||||
- `build-integration-test-runner-beta-dockerfile` (running our integration tests with the latest Chrome beta version)
|
||||
- `build-deployment-yaml-formatter-dockerfile` (templating our deployment YAML during a release)
|
||||
- `build-crane` (copy and tag container images during release)
|
||||
- `build-k8s-code-generator-*` (running our Kubernetes code generation under different Kubernetes dependency versions)
|
||||
- `build-test-dex` (a Dex used during tests)
|
||||
- `build-test-cfssl` (a cfssl used during tests)
|
||||
- `build-test-kubectl` (a kubectl used during tests)
|
||||
- `build-test-forward-proxy` (a Squid forward proxy used during tests)
|
||||
- `build-test-bitnami-ldap` (an OpenLDAP used during tests)
|
||||
|
||||
- `cleanup-aws`
|
||||
|
||||
This runs a script that runs [aws-nuke](https://github.com/rebuy-de/aws-nuke) against our test AWS account.
|
||||
This was occasionally needed because [eksctl](https://eksctl.io/) sometimes fails and leaks AWS resources. These resources cost money and use up our AWS quota.
|
||||
However, we seem to have worked around these issues and this pipeline has not been used for some time.
|
||||
|
||||
These jobs are only triggered manually. This is dangerous and should be used with care.
|
||||
|
||||
- `concourse-workers`
|
||||
|
||||
Deploys worker replicas on a long-lived GKE cluster that runs the Concourse workers, and can scale them up or down.
|
||||
|
||||
- `go-compatibility`
|
||||
|
||||
This pipeline runs nightly jobs that validate the compatibility of our code as a Go module in various contexts. We have jobs that test that our code compiles under older Go versions and that our CLI can be installed using `go install`.
|
||||
|
||||
- `security-scan`
|
||||
|
||||
This pipeline has nightly jobs that run security scans on our current main branch and most recently released artifacts.
|
||||
|
||||
The tools we use are:
|
||||
- [sonatype-nexus-community/nancy](https://github.com/sonatype-nexus-community/nancy), which scans Go module versions.
|
||||
- [aquasecurity/trivy](https://github.com/aquasecurity/trivy), which scans container images and Go binaries.
|
||||
- [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck), which scans Go code to find calls to known-vulnerable dependencies.
|
||||
|
||||
This pipeline also has a job called `all-golang-deps-updated` which automatically submits PRs to update all
|
||||
direct dependencies in Pinniped's go.mod file, and update the Golang and distroless container images used in
|
||||
Pinniped's Dockerfiles.
|
||||
|
||||
- `kind-node-builder`
|
||||
|
||||
A nightly build job which uses the latest version of kind to build the HEAD of master of Kubernetes as a container
|
||||
image that can be used to deploy kind clusters. Other pipelines use this container image to install Pinniped and run
|
||||
integration tests. This gives us insight in any compatibility problems with the upcoming next release of Kubernetes.
|
||||
|
||||
## Deploying pipeline changes
|
||||
|
||||
After any shared tasks (`./pipelines/shared-tasks`) or helpers (`./pipelines/shared-helpers`) are edited,
|
||||
the commits must be pushed to the `ci` branch of this repository to take effect.
|
||||
|
||||
After editing any CI secrets or pipeline definitions, a maintainer must run the corresponding
|
||||
`./pipelines/$PIPELINE_NAME/update-pipeline.sh` script to apply the changes to Concourse.
|
||||
To deploy _all_ pipelines, a maintainer can run `./pipelines/update-all-pipelines.sh`.
|
||||
Don't forget to commit and push your changes after applying them!
|
||||
|
||||
## Github webhooks for pipelines
|
||||
|
||||
Some pipelines use github [webhooks to trigger resource checks](https://concourse-ci.org/resources.html#schema.resource.webhook_token),
|
||||
rather than the default of polling every minute, to make these pipelines more responsive and use fewer compute resources
|
||||
for running checks. Refer to places where `webhook_token` is configured in various `pipeline.yml` files.
|
||||
|
||||
To make these webhooks work, they must be defined on the [GitHub repo's settings](https://github.com/vmware/pinniped/settings/hooks).
|
||||
|
||||
## Installing and operating Concourse
|
||||
|
||||
See [infra/README.md](./infra/README.md) for details about how Concourse was installed and how it can be operated.
|
||||
|
||||
## Acceptance environments
|
||||
|
||||
In addition to the many ephemeral Kubernetes clusters we use for testing, we also deploy a long-running acceptance environment.
|
||||
|
||||
Google Kubernetes Engine (GKE) in the `gke-acceptance-cluster` cluster in our GCP project in the `us-west1-c` availability zone.
|
||||
|
||||
To access this cluster, download the kubeconfig to `gke-acceptance.yaml` by running:
|
||||
|
||||
```cmd
|
||||
KUBECONFIG=gke-acceptance.yaml gcloud container clusters get-credentials gke-acceptance-cluster --project "$PINNIPED_GCP_PROJECT" --zone us-west1-c
|
||||
```
|
||||
|
||||
The above command assumes that you have already set `PINNIPED_GCP_PROJECT` to be the name of the GCP project.
|
||||
|
||||
## CI secrets
|
||||
|
||||
We use [Google Secret Manager](https://cloud.google.com/secret-manager) on GCP to store build/test/release secrets.
|
||||
These secrets are only available to the maintainers.
|
||||
|
||||
Using the `gcloud secrets list` command or the [web console](https://console.cloud.google.com/security/secret-manager),
|
||||
you can list the available secrets. The content of each secret is a YAML file with secret key/value pairs.
|
||||
You can also use the `./hack/edit-gcloud-secret.sh <secretName>` script to edit or inspect each secret.
|
||||
|
||||
## Configure Azure for CI to test on AKS
|
||||
|
||||
There are several CI jobs which test that Pinniped works when installed on Azure's AKS.
|
||||
For these jobs to run, they need to be able to create and delete ephemeral AKS clusters.
|
||||
This requires the following:
|
||||
|
||||
1. An active Azure Subscription. (A "subscription" in Azure is the equivalent of an "account" in AWS or a "project" in GCP.)
|
||||
2. An Azure App Registration (basically, a service account) active in the same Directory (aka tenant) as the Subscription.
|
||||
Create the app in "My Organization Only". It does not need a redirect URI or any other optional settings.
|
||||
Create a client secret for this app. If you want the client secret to have a long lifetime, you can use the `az` CLI to create it.
|
||||
In the Subscription's IAM settings, assign this app the role "Azure Kubernetes Service Contributor Role" to allow
|
||||
the app to manage AKS clusters. Also assign this app the role "Reader" to allow it to read all resources
|
||||
(used by the `remove-orphaned-aks-clusters` CI task).
|
||||
Do not grant this app permissions in any other Subscription or use it for any other purpose.
|
||||
3. Configure the pipelines with the app's Application (client) ID, Client Secret, and Directory (tenant) ID
|
||||
as the appropriate secret values.
|
||||
|
||||
The CI jobs will create and delete AKS clusters in a Resource Group called `pinniped-ci` within the provided Subscription.
|
||||
|
||||
## Configure AWS for CI to test on EKS
|
||||
|
||||
There are several CI jobs which test that Pinniped works when installed on Amazon's EKS.
|
||||
For these jobs to run, they need to be able to create and delete ephemeral EKS clusters.
|
||||
There are also some jobs to cleanup any orphaned resources (e.g. IP addresses) in the AWS account.
|
||||
These jobs requires the following:
|
||||
|
||||
1. An active AWS account, which will only be used for this purpose.
|
||||
2. Two IAM users in that account, each with a role that can be assumed.
|
||||
These IAM users which should only be used for Pinniped CI and no other purpose.
|
||||
They should only have permissions to perform AWS actions in the relevant AWS account, and no other account.
|
||||
3. The first user and role should have permission to create and delete EKS clusters using `eksctl`.
|
||||
The permissions required can be found in the [eksctl docs](https://eksctl.io/usage/minimum-iam-policies).
|
||||
The user also needs permission to run `aws logs put-retention-policy`, `aws ec2 describe-nat-gateways`,
|
||||
and `aws ec2 delete-nat-gateway`.
|
||||
4. The second user and role should have broad permissions to get and delete everything in the account.
|
||||
It will be used to run `aws-nuke` to list and/or clean resources from the AWS account.
|
||||
To use `aws-nuke`, the user also needs to have an AWS account alias
|
||||
(see the [cleanup-aws task](pipelines/shared-tasks/cleanup-aws/task.sh) for details).
|
||||
|
||||
## Setting Up Active Directory Test Environment
|
||||
|
||||
To test the `ActiveDirectoryIdentityProvider` functionality, we have a long-running Active Directory Domain Controller
|
||||
server instance in our GCP account. See [AD-SETUP.md](AD-SETUP.md) for details.
|
||||
|
||||
## Running integration tests on your laptop using AD
|
||||
|
||||
The relevant environment variables can be pulled from the secret manager via the `hack/get-active-directory-env-vars.sh` script.
|
||||
This can be used by maintainers with Pinniped's `/hack/prepare-for-integration-tests.sh` script in the following way:
|
||||
|
||||
```bash
|
||||
# Must authenticate to glcoud to access the secret manager.
|
||||
gcloud auth login
|
||||
# In the pinniped repo's main branch or in your PR branch:
|
||||
hack/prepare-for-integration-tests.sh --get-active-directory-vars "$HOME/path/to/pinniped-ci-branch/hack/get-active-directory-env-vars.sh"
|
||||
```
|
||||
|
||||
## Running integration tests on your laptop using GitHub
|
||||
|
||||
The relevant environment variables can be pulled from the secret manager via the `hack/get-github-env-vars.sh` script.
|
||||
This can be used by maintainers with Pinniped's `/hack/prepare-for-integration-tests.sh` script in the following way:
|
||||
|
||||
```bash
|
||||
# Must authenticate to glcoud to access the secret manager.
|
||||
gcloud auth login
|
||||
# In the pinniped repo's main branch or in your PR branch:
|
||||
hack/prepare-for-integration-tests.sh --get-github-vars "$HOME/path/to/pinniped-ci-branch/hack/get-github-env-vars.sh"
|
||||
```
|
||||
Please follow the procedure described in [SECURITY.md](SECURITY.md).
|
||||
|
||||
## License
|
||||
|
||||
Pinniped is open source and licensed under Apache License Version 2.0. See [LICENSE](LICENSE).
|
||||
|
||||
Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
|
||||
13
SECURITY.md
13
SECURITY.md
@@ -1 +1,12 @@
|
||||
Please see https://github.com/vmware/pinniped/blob/main/SECURITY.md
|
||||
# Reporting a Vulnerability
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
5
apis/README.md
Normal file
5
apis/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# API Generation Templates
|
||||
|
||||
This directory contains a template for generating our Kubernetes API code across several Kubernetes versions.
|
||||
|
||||
See the [`./generated`](../generated) directory for the rendered output.
|
||||
10
apis/concierge/authentication/v1alpha1/doc.go.tmpl
Normal file
10
apis/concierge/authentication/v1alpha1/doc.go.tmpl
Normal file
@@ -0,0 +1,10 @@
|
||||
// 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=authentication.concierge.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped concierge authentication API.
|
||||
package v1alpha1
|
||||
45
apis/concierge/authentication/v1alpha1/register.go.tmpl
Normal file
45
apis/concierge/authentication/v1alpha1/register.go.tmpl
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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 = "authentication.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)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&WebhookAuthenticator{},
|
||||
&WebhookAuthenticatorList{},
|
||||
&JWTAuthenticator{},
|
||||
&JWTAuthenticatorList{},
|
||||
)
|
||||
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()
|
||||
}
|
||||
81
apis/concierge/authentication/v1alpha1/types_jwt.go.tmpl
Normal file
81
apis/concierge/authentication/v1alpha1/types_jwt.go.tmpl
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators
|
||||
// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer`
|
||||
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"`
|
||||
}
|
||||
75
apis/concierge/authentication/v1alpha1/types_meta.go.tmpl
Normal file
75
apis/concierge/authentication/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"`
|
||||
}
|
||||
11
apis/concierge/authentication/v1alpha1/types_tls.go.tmpl
Normal file
11
apis/concierge/authentication/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 configuring TLS on various authenticators.
|
||||
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"`
|
||||
}
|
||||
53
apis/concierge/authentication/v1alpha1/types_webhook.go.tmpl
Normal file
53
apis/concierge/authentication/v1alpha1/types_webhook.go.tmpl
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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 webhook authenticator.
|
||||
type WebhookAuthenticatorStatus 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 webhook authenticator.
|
||||
type WebhookAuthenticatorSpec struct {
|
||||
// Webhook server endpoint URL.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^https://`
|
||||
Endpoint string `json:"endpoint"`
|
||||
|
||||
// TLS configuration.
|
||||
// +optional
|
||||
TLS *TLSSpec `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookAuthenticator describes the configuration of a webhook authenticator.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped;pinniped-authenticator;pinniped-authenticators
|
||||
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.spec.endpoint`
|
||||
type WebhookAuthenticator struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec for configuring the authenticator.
|
||||
Spec WebhookAuthenticatorSpec `json:"spec"`
|
||||
|
||||
// Status of the authenticator.
|
||||
Status WebhookAuthenticatorStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// List of WebhookAuthenticator objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type WebhookAuthenticatorList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []WebhookAuthenticator `json:"items"`
|
||||
}
|
||||
10
apis/concierge/config/v1alpha1/doc.go.tmpl
Normal file
10
apis/concierge/config/v1alpha1/doc.go.tmpl
Normal file
@@ -0,0 +1,10 @@
|
||||
// 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=config.concierge.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped concierge configuration API.
|
||||
package v1alpha1
|
||||
43
apis/concierge/config/v1alpha1/register.go.tmpl
Normal file
43
apis/concierge/config/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 = "config.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)
|
||||
}
|
||||
|
||||
// Adds the list of known types to the given scheme.
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&CredentialIssuer{},
|
||||
&CredentialIssuerList{},
|
||||
)
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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"
|
||||
|
||||
// +kubebuilder:validation:Enum=KubeClusterSigningCertificate
|
||||
type StrategyType string
|
||||
|
||||
// +kubebuilder:validation:Enum=Success;Error
|
||||
type StrategyStatus string
|
||||
|
||||
// +kubebuilder:validation:Enum=FetchedKey;CouldNotFetchKey
|
||||
type StrategyReason string
|
||||
|
||||
const (
|
||||
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
|
||||
|
||||
SuccessStrategyStatus = StrategyStatus("Success")
|
||||
ErrorStrategyStatus = StrategyStatus("Error")
|
||||
|
||||
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
|
||||
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
|
||||
)
|
||||
|
||||
// Status of a credential issuer.
|
||||
type CredentialIssuerStatus struct {
|
||||
// List of integration strategies that were attempted by Pinniped.
|
||||
Strategies []CredentialIssuerStrategy `json:"strategies"`
|
||||
|
||||
// Information needed to form a valid Pinniped-based kubeconfig using this credential issuer.
|
||||
// +optional
|
||||
KubeConfigInfo *CredentialIssuerKubeConfigInfo `json:"kubeConfigInfo,omitempty"`
|
||||
}
|
||||
|
||||
// Information needed to form a valid Pinniped-based kubeconfig using this credential issuer.
|
||||
type CredentialIssuerKubeConfigInfo struct {
|
||||
// The K8s API server URL.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:Pattern=`^https://|^http://`
|
||||
Server string `json:"server"`
|
||||
|
||||
// The K8s API server CA bundle.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
CertificateAuthorityData string `json:"certificateAuthorityData"`
|
||||
}
|
||||
|
||||
// Status of an integration strategy that was attempted by Pinniped.
|
||||
type CredentialIssuerStrategy struct {
|
||||
// Type of integration attempted.
|
||||
Type StrategyType `json:"type"`
|
||||
|
||||
// Status of the attempted integration strategy.
|
||||
Status StrategyStatus `json:"status"`
|
||||
|
||||
// Reason for the current status.
|
||||
Reason StrategyReason `json:"reason"`
|
||||
|
||||
// Human-readable description of the current status.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Message string `json:"message"`
|
||||
|
||||
// When the status was last checked.
|
||||
LastUpdateTime metav1.Time `json:"lastUpdateTime"`
|
||||
}
|
||||
|
||||
// Describes the configuration status of a Pinniped credential issuer.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped
|
||||
type CredentialIssuer struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Status of the credential issuer.
|
||||
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"`
|
||||
|
||||
Items []CredentialIssuer `json:"items"`
|
||||
}
|
||||
8
apis/concierge/login/doc.go.tmpl
Normal file
8
apis/concierge/login/doc.go.tmpl
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +groupName=login.concierge.pinniped.dev
|
||||
|
||||
// Package login is the internal version of the Pinniped login API.
|
||||
package login
|
||||
38
apis/concierge/login/register.go.tmpl
Normal file
38
apis/concierge/login/register.go.tmpl
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package login
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const GroupName = "login.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,
|
||||
&TokenCredentialRequest{},
|
||||
&TokenCredentialRequestList{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
21
apis/concierge/login/types_clustercred.go.tmpl
Normal file
21
apis/concierge/login/types_clustercred.go.tmpl
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package login
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// ClusterCredential is a credential (token or certificate) which is valid on the Kubernetes cluster.
|
||||
type ClusterCredential struct {
|
||||
// ExpirationTimestamp indicates a time when the provided credentials expire.
|
||||
ExpirationTimestamp metav1.Time
|
||||
|
||||
// Token is a bearer token used by the client for request authentication.
|
||||
Token string
|
||||
|
||||
// PEM-encoded client TLS certificates (including intermediates, if any).
|
||||
ClientCertificateData string
|
||||
|
||||
// PEM-encoded private key for the above certificate.
|
||||
ClientKeyData string
|
||||
}
|
||||
48
apis/concierge/login/types_token.go.tmpl
Normal file
48
apis/concierge/login/types_token.go.tmpl
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package login
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type TokenCredentialRequestSpec struct {
|
||||
// Bearer token supplied with the credential request.
|
||||
Token string
|
||||
|
||||
// Reference to an authenticator which can validate this credential request.
|
||||
Authenticator corev1.TypedLocalObjectReference
|
||||
}
|
||||
|
||||
type TokenCredentialRequestStatus struct {
|
||||
// A ClusterCredential will be returned for a successful credential request.
|
||||
// +optional
|
||||
Credential *ClusterCredential
|
||||
|
||||
// An error message will be returned for an unsuccessful credential request.
|
||||
// +optional
|
||||
Message *string
|
||||
}
|
||||
|
||||
// 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
|
||||
metav1.ObjectMeta
|
||||
|
||||
Spec TokenCredentialRequestSpec
|
||||
Status TokenCredentialRequestStatus
|
||||
}
|
||||
|
||||
// TokenCredentialRequestList is a list of TokenCredentialRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type TokenCredentialRequestList struct {
|
||||
metav1.TypeMeta
|
||||
metav1.ListMeta
|
||||
|
||||
// Items is a list of TokenCredentialRequest
|
||||
Items []TokenCredentialRequest
|
||||
}
|
||||
4
apis/concierge/login/v1alpha1/conversion.go.tmpl
Normal file
4
apis/concierge/login/v1alpha1/conversion.go.tmpl
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
12
apis/concierge/login/v1alpha1/defaults.go.tmpl
Normal file
12
apis/concierge/login/v1alpha1/defaults.go.tmpl
Normal file
@@ -0,0 +1,12 @@
|
||||
// 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)
|
||||
}
|
||||
11
apis/concierge/login/v1alpha1/doc.go.tmpl
Normal file
11
apis/concierge/login/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:conversion-gen=go.pinniped.dev/GENERATED_PKG/apis/concierge/login
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=login.concierge.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped login API.
|
||||
package v1alpha1
|
||||
43
apis/concierge/login/v1alpha1/register.go.tmpl
Normal file
43
apis/concierge/login/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 = "login.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,
|
||||
&TokenCredentialRequest{},
|
||||
&TokenCredentialRequestList{},
|
||||
)
|
||||
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()
|
||||
}
|
||||
22
apis/concierge/login/v1alpha1/types_clustercred.go.tmpl
Normal file
22
apis/concierge/login/v1alpha1/types_clustercred.go.tmpl
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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"
|
||||
|
||||
// ClusterCredential is the cluster-specific credential returned on a successful credential request. It
|
||||
// contains either a valid bearer token or a valid TLS certificate and corresponding private key for the cluster.
|
||||
type ClusterCredential struct {
|
||||
// ExpirationTimestamp indicates a time when the provided credentials expire.
|
||||
ExpirationTimestamp metav1.Time `json:"expirationTimestamp,omitempty"`
|
||||
|
||||
// Token is a bearer token used by the client for request authentication.
|
||||
Token string `json:"token,omitempty"`
|
||||
|
||||
// PEM-encoded client TLS certificates (including intermediates, if any).
|
||||
ClientCertificateData string `json:"clientCertificateData,omitempty"`
|
||||
|
||||
// PEM-encoded private key for the above certificate.
|
||||
ClientKeyData string `json:"clientKeyData,omitempty"`
|
||||
}
|
||||
49
apis/concierge/login/v1alpha1/types_token.go.tmpl
Normal file
49
apis/concierge/login/v1alpha1/types_token.go.tmpl
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// TokenCredentialRequestSpec is the specification of a TokenCredentialRequest, expected on requests to the Pinniped API.
|
||||
type TokenCredentialRequestSpec struct {
|
||||
// Bearer token supplied with the credential request.
|
||||
Token string `json:"token,omitempty"`
|
||||
|
||||
// Reference to an authenticator which can validate this credential request.
|
||||
Authenticator corev1.TypedLocalObjectReference `json:"authenticator"`
|
||||
}
|
||||
|
||||
// TokenCredentialRequestStatus is the status of a TokenCredentialRequest, returned on responses to the Pinniped API.
|
||||
type TokenCredentialRequestStatus struct {
|
||||
// A Credential will be returned for a successful credential request.
|
||||
// +optional
|
||||
Credential *ClusterCredential `json:"credential,omitempty"`
|
||||
|
||||
// An error message will be returned for an unsuccessful credential request.
|
||||
// +optional
|
||||
Message *string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// 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 `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec TokenCredentialRequestSpec `json:"spec,omitempty"`
|
||||
Status TokenCredentialRequestStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// TokenCredentialRequestList is a list of TokenCredentialRequest objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type TokenCredentialRequestList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []TokenCredentialRequest `json:"items"`
|
||||
}
|
||||
11
apis/supervisor/config/v1alpha1/doc.go.tmpl
Normal file
11
apis/supervisor/config/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:conversion-gen=go.pinniped.dev/GENERATED_PKG/apis/supervisor/config
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=config.supervisor.pinniped.dev
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuration API.
|
||||
package v1alpha1
|
||||
43
apis/supervisor/config/v1alpha1/register.go.tmpl
Normal file
43
apis/supervisor/config/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 = "config.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,
|
||||
&FederationDomain{},
|
||||
&FederationDomainList{},
|
||||
)
|
||||
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()
|
||||
}
|
||||
130
apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl
Normal file
130
apis/supervisor/config/v1alpha1/types_federationdomain.go.tmpl
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +kubebuilder:validation:Enum=Success;Duplicate;Invalid;SameIssuerHostMustUseSameSecret
|
||||
type FederationDomainStatusCondition string
|
||||
|
||||
const (
|
||||
SuccessFederationDomainStatusCondition = FederationDomainStatusCondition("Success")
|
||||
DuplicateFederationDomainStatusCondition = FederationDomainStatusCondition("Duplicate")
|
||||
SameIssuerHostMustUseSameSecretFederationDomainStatusCondition = FederationDomainStatusCondition("SameIssuerHostMustUseSameSecret")
|
||||
InvalidFederationDomainStatusCondition = FederationDomainStatusCondition("Invalid")
|
||||
)
|
||||
|
||||
// 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 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.
|
||||
//
|
||||
// When your Issuer URL's host is an IP address, then this field is ignored. SNI does not work for IP addresses.
|
||||
//
|
||||
// +optional
|
||||
SecretName string `json:"secretName,omitempty"`
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
Issuer string `json:"issuer"`
|
||||
|
||||
// TLS configures how this FederationDomain is served over Transport Layer Security (TLS).
|
||||
// +optional
|
||||
TLS *FederationDomainTLSSpec `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// 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 FederationDomainStatusCondition `json:"status,omitempty"`
|
||||
|
||||
// Message provides human-readable details about the Status.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
// 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).
|
||||
// +optional
|
||||
LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"`
|
||||
|
||||
// Secrets contains information about this OIDC Provider's secrets.
|
||||
// +optional
|
||||
Secrets FederationDomainSecrets `json:"secrets,omitempty"`
|
||||
}
|
||||
|
||||
// FederationDomain describes the configuration of an OIDC provider.
|
||||
// +genclient
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
// +kubebuilder:resource:categories=pinniped
|
||||
type FederationDomain struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec of the OIDC provider.
|
||||
Spec FederationDomainSpec `json:"spec"`
|
||||
|
||||
// Status of the OIDC provider.
|
||||
Status FederationDomainStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// List of FederationDomain objects.
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type FederationDomainList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
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"`
|
||||
}
|
||||
406
cmd/local-user-authenticator/main.go
Normal file
406
cmd/local-user-authenticator/main.go
Normal file
@@ -0,0 +1,406 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package main provides a authentication webhook program.
|
||||
//
|
||||
// This webhook is meant to be used in demo settings to play around with
|
||||
// Pinniped. As well, it can come in handy in integration tests.
|
||||
//
|
||||
// This webhook is NOT meant for use in production systems.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
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/plog"
|
||||
)
|
||||
|
||||
const (
|
||||
// This string must match the name of the Namespace declared in the deployment yaml.
|
||||
namespace = "local-user-authenticator"
|
||||
// This string must match the name of the Service declared in the deployment yaml.
|
||||
serviceName = "local-user-authenticator"
|
||||
|
||||
singletonWorker = 1
|
||||
defaultResyncInterval = 3 * time.Minute
|
||||
|
||||
invalidRequest = constable.Error("invalid request")
|
||||
)
|
||||
|
||||
type webhook struct {
|
||||
certProvider dynamiccert.Provider
|
||||
secretInformer corev1informers.SecretInformer
|
||||
}
|
||||
|
||||
func newWebhook(
|
||||
certProvider dynamiccert.Provider,
|
||||
secretInformer corev1informers.SecretInformer,
|
||||
) *webhook {
|
||||
return &webhook{
|
||||
certProvider: certProvider,
|
||||
secretInformer: secretInformer,
|
||||
}
|
||||
}
|
||||
|
||||
// start runs the webhook in a separate goroutine and returns whether or not the
|
||||
// webhook was started successfully.
|
||||
func (w *webhook) start(ctx context.Context, l net.Listener) error {
|
||||
server := http.Server{
|
||||
Handler: w,
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
certPEM, keyPEM := w.certProvider.CurrentCertKeyContent()
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
return &cert, err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
// Per ListenAndServeTLS doc, the {cert,key}File parameters can be empty
|
||||
// since we want to use the certs from http.Server.TLSConfig.
|
||||
errCh <- server.ServeTLS(l, "", "")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
plog.Debug("server exited", "err", err)
|
||||
case <-ctx.Done():
|
||||
plog.Debug("server context cancelled", "err", ctx.Err())
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
plog.Debug("server shutdown failed", "err", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *webhook) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
|
||||
username, password, err := getUsernameAndPasswordFromRequest(rsp, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = req.Body.Close() }()
|
||||
|
||||
secret, err := w.secretInformer.Lister().Secrets(namespace).Get(username)
|
||||
notFound := k8serrors.IsNotFound(err)
|
||||
if err != nil && !notFound {
|
||||
plog.Debug("could not get secret", "err", err)
|
||||
rsp.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if notFound {
|
||||
plog.Debug("user not found")
|
||||
respondWithUnauthenticated(rsp)
|
||||
return
|
||||
}
|
||||
|
||||
passwordMatches := bcrypt.CompareHashAndPassword(
|
||||
secret.Data["passwordHash"],
|
||||
[]byte(password),
|
||||
) == nil
|
||||
if !passwordMatches {
|
||||
plog.Debug("authentication failed: wrong password")
|
||||
respondWithUnauthenticated(rsp)
|
||||
return
|
||||
}
|
||||
|
||||
groups := []string{}
|
||||
groupsBuf := bytes.NewBuffer(secret.Data["groups"])
|
||||
if groupsBuf.Len() > 0 {
|
||||
groupsCSVReader := csv.NewReader(groupsBuf)
|
||||
groups, err = groupsCSVReader.Read()
|
||||
if err != nil {
|
||||
plog.Debug("could not read groups", "err", err)
|
||||
rsp.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
trimLeadingAndTrailingWhitespace(groups)
|
||||
}
|
||||
|
||||
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" {
|
||||
plog.Debug("received request path other than /authenticate", "path", req.URL.Path)
|
||||
rsp.WriteHeader(http.StatusNotFound)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
if req.Method != http.MethodPost {
|
||||
plog.Debug("received request method other than post", "method", req.Method)
|
||||
rsp.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
if !headerContains(req, "Content-Type", "application/json") {
|
||||
plog.Debug("content type is not application/json", "Content-Type", req.Header.Values("Content-Type"))
|
||||
rsp.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
if !headerContains(req, "Accept", "application/json") &&
|
||||
!headerContains(req, "Accept", "application/*") &&
|
||||
!headerContains(req, "Accept", "*/*") {
|
||||
plog.Debug("client does not accept application/json", "Accept", req.Header.Values("Accept"))
|
||||
rsp.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
if req.Body == nil {
|
||||
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 {
|
||||
plog.Debug("failed to decode body", "err", err)
|
||||
rsp.WriteHeader(http.StatusBadRequest)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
if body.APIVersion != authenticationv1beta1.SchemeGroupVersion.String() {
|
||||
plog.Debug("invalid TokenReview apiVersion", "apiVersion", body.APIVersion)
|
||||
rsp.WriteHeader(http.StatusBadRequest)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
if body.Kind != "TokenReview" {
|
||||
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 {
|
||||
plog.Debug("bad token format in request")
|
||||
rsp.WriteHeader(http.StatusBadRequest)
|
||||
return "", "", invalidRequest
|
||||
}
|
||||
|
||||
return tokenSegments[0], tokenSegments[1], nil
|
||||
}
|
||||
|
||||
func headerContains(req *http.Request, headerName, s string) bool {
|
||||
headerValues := req.Header.Values(headerName)
|
||||
for i := range headerValues {
|
||||
mimeTypes := strings.Split(headerValues[i], ",")
|
||||
for _, mimeType := range mimeTypes {
|
||||
mediaType, _, _ := mime.ParseMediaType(mimeType)
|
||||
if mediaType == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func trimLeadingAndTrailingWhitespace(ss []string) {
|
||||
for i := range ss {
|
||||
ss[i] = strings.TrimSpace(ss[i])
|
||||
}
|
||||
}
|
||||
|
||||
func respondWithUnauthenticated(rsp http.ResponseWriter) {
|
||||
rsp.Header().Add("Content-Type", "application/json")
|
||||
|
||||
body := authenticationv1beta1.TokenReview{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "TokenReview",
|
||||
APIVersion: authenticationv1beta1.SchemeGroupVersion.String(),
|
||||
},
|
||||
Status: authenticationv1beta1.TokenReviewStatus{
|
||||
Authenticated: false,
|
||||
},
|
||||
}
|
||||
if err := json.NewEncoder(rsp).Encode(body); err != nil {
|
||||
plog.Debug("could not encode response", "err", err)
|
||||
rsp.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func respondWithAuthenticated(
|
||||
rsp http.ResponseWriter,
|
||||
username, uid string,
|
||||
groups []string,
|
||||
) {
|
||||
rsp.Header().Add("Content-Type", "application/json")
|
||||
body := authenticationv1beta1.TokenReview{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "TokenReview",
|
||||
APIVersion: authenticationv1beta1.SchemeGroupVersion.String(),
|
||||
},
|
||||
Status: authenticationv1beta1.TokenReviewStatus{
|
||||
Authenticated: true,
|
||||
User: authenticationv1beta1.UserInfo{
|
||||
Username: username,
|
||||
Groups: groups,
|
||||
UID: uid,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := json.NewEncoder(rsp).Encode(body); err != nil {
|
||||
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,
|
||||
kubeClient kubernetes.Interface,
|
||||
kubeInformers kubeinformers.SharedInformerFactory,
|
||||
) {
|
||||
aVeryLongTime := time.Hour * 24 * 365 * 100
|
||||
|
||||
const certsSecretResourceName = "local-user-authenticator-tls-serving-certificate"
|
||||
|
||||
// Create controller manager.
|
||||
controllerManager := controllerlib.
|
||||
NewManager().
|
||||
WithController(
|
||||
apicerts.NewCertsManagerController(
|
||||
namespace,
|
||||
certsSecretResourceName,
|
||||
map[string]string{
|
||||
"app": "local-user-authenticator",
|
||||
},
|
||||
kubeClient,
|
||||
kubeInformers.Core().V1().Secrets(),
|
||||
controllerlib.WithInformer,
|
||||
controllerlib.WithInitialEvent,
|
||||
aVeryLongTime,
|
||||
"local-user-authenticator CA",
|
||||
serviceName,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
apicerts.NewCertsObserverController(
|
||||
namespace,
|
||||
certsSecretResourceName,
|
||||
dynamicCertProvider,
|
||||
kubeInformers.Core().V1().Secrets(),
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
)
|
||||
|
||||
kubeInformers.Start(ctx.Done())
|
||||
|
||||
go controllerManager.Start(ctx)
|
||||
}
|
||||
|
||||
func startWebhook(
|
||||
ctx context.Context,
|
||||
l net.Listener,
|
||||
dynamicCertProvider dynamiccert.Provider,
|
||||
secretInformer corev1informers.SecretInformer,
|
||||
) error {
|
||||
return newWebhook(dynamicCertProvider, secretInformer).start(ctx, l)
|
||||
}
|
||||
|
||||
func waitForSignal() os.Signal {
|
||||
signalCh := make(chan os.Signal, 1)
|
||||
signal.Notify(signalCh, os.Interrupt)
|
||||
return <-signalCh
|
||||
}
|
||||
|
||||
func run() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
kubeClient, err := newK8sClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create k8s client: %w", err)
|
||||
}
|
||||
|
||||
kubeInformers := kubeinformers.NewSharedInformerFactoryWithOptions(
|
||||
kubeClient,
|
||||
defaultResyncInterval,
|
||||
kubeinformers.WithNamespace(namespace),
|
||||
)
|
||||
|
||||
dynamicCertProvider := dynamiccert.New()
|
||||
|
||||
startControllers(ctx, dynamicCertProvider, kubeClient, kubeInformers)
|
||||
plog.Debug("controllers are ready")
|
||||
|
||||
//nolint: gosec // Intentionally binding to all network interfaces.
|
||||
l, err := net.Listen("tcp", ":8443")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create listener: %w", err)
|
||||
}
|
||||
defer func() { _ = l.Close() }()
|
||||
|
||||
err = startWebhook(ctx, l, dynamicCertProvider, kubeInformers.Core().V1().Secrets())
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start webhook: %w", err)
|
||||
}
|
||||
plog.Debug("webhook is ready", "address", l.Addr().String())
|
||||
|
||||
gotSignal := waitForSignal()
|
||||
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)
|
||||
}
|
||||
}
|
||||
578
cmd/local-user-authenticator/main_test.go
Normal file
578
cmd/local-user-authenticator/main_test.go
Normal file
@@ -0,0 +1,578 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
corev1informers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/dynamiccert"
|
||||
)
|
||||
|
||||
func TestWebhook(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const namespace = "local-user-authenticator"
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
user, otherUser, colonUser, noGroupUser, oneGroupUser, passwordUndefinedUser, emptyPasswordUser, invalidPasswordHashUser, undefinedGroupsUser :=
|
||||
"some-user", "other-user", "colon-user", "no-group-user", "one-group-user", "password-undefined-user", "empty-password-user", "invalid-password-hash-user", "undefined-groups-user"
|
||||
uid, otherUID, colonUID, noGroupUID, oneGroupUID, passwordUndefinedUID, emptyPasswordUID, invalidPasswordHashUID, undefinedGroupsUID :=
|
||||
"some-uid", "other-uid", "colon-uid", "no-group-uid", "one-group-uid", "password-undefined-uid", "empty-password-uid", "invalid-password-hash-uid", "undefined-groups-uid"
|
||||
password, otherPassword, colonPassword, noGroupPassword, oneGroupPassword, undefinedGroupsPassword :=
|
||||
"some-password", "other-password", "some-:-password", "no-group-password", "one-group-password", "undefined-groups-password"
|
||||
|
||||
group0, group1 := "some-group-0", "some-group-1"
|
||||
groups := group0 + " , " + group1
|
||||
|
||||
kubeClient := kubernetesfake.NewSimpleClientset()
|
||||
addSecretToFakeClientTracker(t, kubeClient, user, uid, password, groups)
|
||||
addSecretToFakeClientTracker(t, kubeClient, otherUser, otherUID, otherPassword, groups)
|
||||
addSecretToFakeClientTracker(t, kubeClient, colonUser, colonUID, colonPassword, groups)
|
||||
addSecretToFakeClientTracker(t, kubeClient, noGroupUser, noGroupUID, noGroupPassword, "")
|
||||
addSecretToFakeClientTracker(t, kubeClient, oneGroupUser, oneGroupUID, oneGroupPassword, group0)
|
||||
addSecretToFakeClientTracker(t, kubeClient, emptyPasswordUser, emptyPasswordUID, "", groups)
|
||||
|
||||
require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: types.UID(passwordUndefinedUID),
|
||||
Name: passwordUndefinedUser,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"groups": []byte(groups),
|
||||
},
|
||||
}))
|
||||
|
||||
undefinedGroupsUserPasswordHash, err := bcrypt.GenerateFromPassword([]byte(undefinedGroupsPassword), bcrypt.MinCost)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: types.UID(undefinedGroupsUID),
|
||||
Name: undefinedGroupsUser,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"passwordHash": undefinedGroupsUserPasswordHash,
|
||||
},
|
||||
}))
|
||||
|
||||
require.NoError(t, kubeClient.Tracker().Add(&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: types.UID(invalidPasswordHashUID),
|
||||
Name: invalidPasswordHashUser,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"groups": []byte(groups),
|
||||
"passwordHash": []byte("not a valid password hash"),
|
||||
},
|
||||
}))
|
||||
|
||||
secretInformer := createSecretInformer(t, kubeClient)
|
||||
|
||||
certProvider, caBundle, serverName := newCertProvider(t)
|
||||
w := newWebhook(certProvider, secretInformer)
|
||||
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = l.Close() }()
|
||||
require.NoError(t, w.start(ctx, l))
|
||||
|
||||
client := newClient(caBundle, serverName)
|
||||
|
||||
goodURL := fmt.Sprintf("https://%s/authenticate", l.Addr().String())
|
||||
goodRequestHeaders := map[string][]string{
|
||||
"Content-Type": {"application/json; charset=UTF-8"},
|
||||
"Accept": {"application/json, */*"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
method string
|
||||
headers map[string][]string
|
||||
body func() (io.ReadCloser, error)
|
||||
|
||||
wantStatus int
|
||||
wantHeaders map[string][]string
|
||||
wantBody *authenticationv1beta1.TokenReview
|
||||
}{
|
||||
{
|
||||
name: "success for a user who belongs to multiple groups",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(user, uid, []string{group0, group1}),
|
||||
},
|
||||
{
|
||||
name: "success for a user who belongs to one groups",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(oneGroupUser + ":" + oneGroupPassword) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(oneGroupUser, oneGroupUID, []string{group0}),
|
||||
},
|
||||
{
|
||||
name: "success for a user who belongs to no groups",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(noGroupUser + ":" + noGroupPassword) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(noGroupUser, noGroupUID, nil),
|
||||
},
|
||||
{
|
||||
name: "wrong username for password",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(otherUser + ":" + password) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: unauthenticatedResponseJSON(),
|
||||
},
|
||||
{
|
||||
name: "when a user has no password hash in the secret",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(passwordUndefinedUser + ":foo") },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: unauthenticatedResponseJSON(),
|
||||
},
|
||||
{
|
||||
name: "when a user has an invalid password hash in the secret",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(invalidPasswordHashUser + ":foo") },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: unauthenticatedResponseJSON(),
|
||||
},
|
||||
{
|
||||
name: "success for a user has no groups defined in the secret",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) {
|
||||
return newTokenReviewBody(undefinedGroupsUser + ":" + undefinedGroupsPassword)
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(undefinedGroupsUser, undefinedGroupsUID, nil),
|
||||
},
|
||||
{
|
||||
name: "when a user has empty string as their password",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(passwordUndefinedUser + ":foo") },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: unauthenticatedResponseJSON(),
|
||||
},
|
||||
{
|
||||
name: "wrong password for username",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + otherPassword) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: unauthenticatedResponseJSON(),
|
||||
},
|
||||
{
|
||||
name: "non-existent password for username",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + "some-non-existent-password") },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: unauthenticatedResponseJSON(),
|
||||
},
|
||||
{
|
||||
name: "non-existent username",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-non-existent-user" + ":" + password) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: unauthenticatedResponseJSON(),
|
||||
},
|
||||
{
|
||||
name: "bad token format (missing colon)",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user) },
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "password contains colon",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(colonUser + ":" + colonPassword) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(colonUser, colonUID, []string{group0, group1}),
|
||||
},
|
||||
{
|
||||
name: "bad TokenReview group",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) {
|
||||
return newTokenReviewBodyWithGVK(
|
||||
user+":"+password,
|
||||
&schema.GroupVersionKind{
|
||||
Group: "bad group",
|
||||
Version: authenticationv1beta1.SchemeGroupVersion.Version,
|
||||
Kind: "TokenReview",
|
||||
},
|
||||
)
|
||||
},
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "bad TokenReview version",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) {
|
||||
return newTokenReviewBodyWithGVK(
|
||||
user+":"+password,
|
||||
&schema.GroupVersionKind{
|
||||
Group: authenticationv1beta1.SchemeGroupVersion.Group,
|
||||
Version: "bad version",
|
||||
Kind: "TokenReview",
|
||||
},
|
||||
)
|
||||
},
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "bad TokenReview kind",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) {
|
||||
return newTokenReviewBodyWithGVK(
|
||||
user+":"+password,
|
||||
&schema.GroupVersionKind{
|
||||
Group: authenticationv1beta1.SchemeGroupVersion.Group,
|
||||
Version: authenticationv1beta1.SchemeGroupVersion.Version,
|
||||
Kind: "wrong-kind",
|
||||
},
|
||||
)
|
||||
},
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "bad path",
|
||||
url: fmt.Sprintf("https://%s/tuna", l.Addr().String()),
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
|
||||
wantStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "bad method",
|
||||
url: goodURL,
|
||||
method: http.MethodGet,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
|
||||
wantStatus: http.StatusMethodNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "bad content type",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: map[string][]string{
|
||||
"Content-Type": {"application/xml"},
|
||||
"Accept": {"application/json"},
|
||||
},
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
|
||||
wantStatus: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
name: "bad accept",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
"Accept": {"application/xml"},
|
||||
},
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody("some-token") },
|
||||
wantStatus: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
name: "success when there are multiple accepts and one of them is json",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
"Accept": {"something/else, application/xml, application/json"},
|
||||
},
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(user, uid, []string{group0, group1}),
|
||||
},
|
||||
{
|
||||
name: "success when there are multiple accepts and one of them is */*",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
"Accept": {"something/else, */*, application/foo"},
|
||||
},
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(user, uid, []string{group0, group1}),
|
||||
},
|
||||
{
|
||||
name: "success when there are multiple accepts and one of them is application/*",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
"Accept": {"something/else, application/*, application/foo"},
|
||||
},
|
||||
body: func() (io.ReadCloser, error) { return newTokenReviewBody(user + ":" + password) },
|
||||
wantStatus: http.StatusOK,
|
||||
wantHeaders: map[string][]string{"Content-Type": {"application/json"}},
|
||||
wantBody: authenticatedResponseJSON(user, uid, []string{group0, group1}),
|
||||
},
|
||||
{
|
||||
name: "bad body",
|
||||
url: goodURL,
|
||||
method: http.MethodPost,
|
||||
headers: goodRequestHeaders,
|
||||
body: func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewBuffer([]byte("invalid body"))), nil },
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
parsedURL, err := url.Parse(test.url)
|
||||
require.NoError(t, err)
|
||||
|
||||
body, err := test.body()
|
||||
require.NoError(t, err)
|
||||
|
||||
rsp, err := client.Do(&http.Request{
|
||||
Method: test.method,
|
||||
URL: parsedURL,
|
||||
Header: test.headers,
|
||||
Body: body,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = rsp.Body.Close() }()
|
||||
|
||||
require.Equal(t, test.wantStatus, rsp.StatusCode)
|
||||
|
||||
if test.wantHeaders != nil {
|
||||
for k, v := range test.wantHeaders {
|
||||
require.Equal(t, v, rsp.Header.Values(k))
|
||||
}
|
||||
}
|
||||
|
||||
responseBody, err := ioutil.ReadAll(rsp.Body)
|
||||
require.NoError(t, err)
|
||||
if test.wantBody != nil {
|
||||
require.NoError(t, err)
|
||||
|
||||
var tr authenticationv1beta1.TokenReview
|
||||
require.NoError(t, json.Unmarshal(responseBody, &tr))
|
||||
require.Equal(t, test.wantBody, &tr)
|
||||
} else {
|
||||
require.Empty(t, responseBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createSecretInformer(t *testing.T, kubeClient kubernetes.Interface) corev1informers.SecretInformer {
|
||||
t.Helper()
|
||||
|
||||
kubeInformers := kubeinformers.NewSharedInformerFactory(kubeClient, 0)
|
||||
|
||||
secretInformer := kubeInformers.Core().V1().Secrets()
|
||||
|
||||
// We need to call Informer() on the secretInformer to lazily instantiate the
|
||||
// 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())
|
||||
require.True(t, informerTypesSynced[reflect.TypeOf(&corev1.Secret{})])
|
||||
|
||||
return secretInformer
|
||||
}
|
||||
|
||||
// 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) {
|
||||
t.Helper()
|
||||
|
||||
serverName := "local-user-authenticator"
|
||||
|
||||
ca, err := certauthority.New(pkix.Name{CommonName: serverName + " CA"}, time.Hour*24)
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := ca.Issue(pkix.Name{CommonName: serverName}, []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)
|
||||
|
||||
return certProvider, ca.Bundle(), serverName
|
||||
}
|
||||
|
||||
// newClient creates an http.Client that can be used to make an HTTPS call to a
|
||||
// service whose serving certs can be verified by the provided CA bundle.
|
||||
func newClient(caBundle []byte, serverName string) *http.Client {
|
||||
rootCAs := x509.NewCertPool()
|
||||
rootCAs.AppendCertsFromPEM(caBundle)
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
RootCAs: rootCAs,
|
||||
ServerName: serverName,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newTokenReviewBody creates an io.ReadCloser that contains a JSON-encodeed
|
||||
// TokenReview request with expected APIVersion and Kind fields.
|
||||
func newTokenReviewBody(token string) (io.ReadCloser, error) {
|
||||
return newTokenReviewBodyWithGVK(
|
||||
token,
|
||||
&schema.GroupVersionKind{
|
||||
Group: authenticationv1beta1.SchemeGroupVersion.Group,
|
||||
Version: authenticationv1beta1.SchemeGroupVersion.Version,
|
||||
Kind: "TokenReview",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// newTokenReviewBodyWithGVK creates an io.ReadCloser that contains a
|
||||
// JSON-encoded TokenReview request. The TypeMeta fields of the TokenReview are
|
||||
// filled in with the provided gvk.
|
||||
func newTokenReviewBodyWithGVK(token string, gvk *schema.GroupVersionKind) (io.ReadCloser, error) {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
tr := authenticationv1beta1.TokenReview{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
Kind: gvk.Kind,
|
||||
},
|
||||
Spec: authenticationv1beta1.TokenReviewSpec{
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
err := json.NewEncoder(buf).Encode(&tr)
|
||||
return ioutil.NopCloser(buf), err
|
||||
}
|
||||
|
||||
func unauthenticatedResponseJSON() *authenticationv1beta1.TokenReview {
|
||||
return &authenticationv1beta1.TokenReview{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "TokenReview",
|
||||
APIVersion: "authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Status: authenticationv1beta1.TokenReviewStatus{
|
||||
Authenticated: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func authenticatedResponseJSON(user, uid string, groups []string) *authenticationv1beta1.TokenReview {
|
||||
return &authenticationv1beta1.TokenReview{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "TokenReview",
|
||||
APIVersion: "authentication.k8s.io/v1beta1",
|
||||
},
|
||||
Status: authenticationv1beta1.TokenReviewStatus{
|
||||
Authenticated: true,
|
||||
User: authenticationv1beta1.UserInfo{
|
||||
Username: user,
|
||||
Groups: groups,
|
||||
UID: uid,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func addSecretToFakeClientTracker(t *testing.T, kubeClient *kubernetesfake.Clientset, username, uid, password, groups string) {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
require.NoError(t, err)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: types.UID(uid),
|
||||
Name: username,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"passwordHash": passwordHash,
|
||||
"groups": []byte(groups),
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||
}
|
||||
35
cmd/pinniped-concierge/main.go
Normal file
35
cmd/pinniped-concierge/main.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/client-go/pkg/version"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/component-base/logs"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"go.pinniped.dev/internal/concierge/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logs.InitLogs()
|
||||
defer logs.FlushLogs()
|
||||
|
||||
// Dump out the time since compile (mostly useful for benchmarking our local development cycle latency).
|
||||
var timeSinceCompile time.Duration
|
||||
if buildDate, err := time.Parse(time.RFC3339, version.Get().BuildDate); err == nil {
|
||||
timeSinceCompile = time.Since(buildDate).Round(time.Second)
|
||||
}
|
||||
klog.Infof("Running %s at %#v (%s since build)", rest.DefaultKubernetesUserAgent(), version.Get(), timeSinceCompile)
|
||||
|
||||
ctx := genericapiserver.SetupSignalContext()
|
||||
|
||||
if err := server.New(ctx, os.Args[1:], os.Stdout, os.Stderr).Run(); err != nil {
|
||||
klog.Fatal(err)
|
||||
}
|
||||
}
|
||||
437
cmd/pinniped-supervisor/main.go
Normal file
437
cmd/pinniped-supervisor/main.go
Normal file
@@ -0,0 +1,437 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/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"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1"
|
||||
pinnipedclientset "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned"
|
||||
pinnipedinformers "go.pinniped.dev/generated/1.19/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/downward"
|
||||
"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 (
|
||||
singletonWorker = 1
|
||||
defaultResyncInterval = 3 * time.Minute
|
||||
)
|
||||
|
||||
func start(ctx context.Context, l net.Listener, handler http.Handler) {
|
||||
server := http.Server{Handler: handler}
|
||||
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
errCh <- server.Serve(l)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
plog.Debug("server exited", "err", err)
|
||||
case <-ctx.Done():
|
||||
plog.Debug("server context cancelled", "err", ctx.Err())
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
plog.Debug("server shutdown failed", "err", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func waitForSignal() os.Signal {
|
||||
signalCh := make(chan os.Signal, 1)
|
||||
signal.Notify(signalCh, os.Interrupt)
|
||||
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(
|
||||
supervisorstorage.GarbageCollectorController(
|
||||
clock.RealClock{},
|
||||
kubeClient,
|
||||
secretInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
supervisorconfig.NewFederationDomainWatcherController(
|
||||
issuerManager,
|
||||
clock.RealClock{},
|
||||
pinnipedClient,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
supervisorconfig.NewJWKSWriterController(
|
||||
cfg.Labels,
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
supervisorconfig.NewJWKSObserverController(
|
||||
dynamicJWKSProvider,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
supervisorconfig.NewTLSCertObserverController(
|
||||
dynamicTLSCertProvider,
|
||||
cfg.NamesConfig.DefaultTLSCertificateSecret,
|
||||
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.FederationDomain) *corev1.LocalObjectReference {
|
||||
return &fd.Status.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.FederationDomain) *corev1.LocalObjectReference {
|
||||
return &fd.Status.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.FederationDomain) *corev1.LocalObjectReference {
|
||||
return &fd.Status.Secrets.StateEncryptionKey
|
||||
},
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
secretInformer,
|
||||
federationDomainInformer,
|
||||
controllerlib.WithInformer,
|
||||
),
|
||||
singletonWorker,
|
||||
).
|
||||
WithController(
|
||||
upstreamwatcher.New(
|
||||
dynamicUpstreamIDPProvider,
|
||||
pinnipedClient,
|
||||
pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
|
||||
secretInformer,
|
||||
klogr.New()),
|
||||
singletonWorker)
|
||||
|
||||
kubeInformers.Start(ctx.Done())
|
||||
pinnipedInformers.Start(ctx.Done())
|
||||
|
||||
// Wait until the caches are synced before returning.
|
||||
kubeInformers.WaitForCacheSync(ctx.Done())
|
||||
pinnipedInformers.WaitForCacheSync(ctx.Done())
|
||||
|
||||
go controllerManager.Start(ctx)
|
||||
}
|
||||
|
||||
func getSupervisorDeployment(
|
||||
ctx context.Context,
|
||||
kubeClient kubernetes.Interface,
|
||||
podInfo *downward.PodInfo,
|
||||
) (*appsv1.Deployment, error) {
|
||||
ns := podInfo.Namespace
|
||||
|
||||
pod, err := kubeClient.CoreV1().Pods(ns).Get(ctx, podInfo.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get pod: %w", err)
|
||||
}
|
||||
|
||||
podOwner := metav1.GetControllerOf(pod)
|
||||
if podOwner == nil {
|
||||
return nil, fmt.Errorf("pod %s/%s is missing owner", ns, podInfo.Name)
|
||||
}
|
||||
|
||||
rs, err := kubeClient.AppsV1().ReplicaSets(ns).Get(ctx, podOwner.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get replicaset: %w", err)
|
||||
}
|
||||
|
||||
rsOwner := metav1.GetControllerOf(rs)
|
||||
if rsOwner == nil {
|
||||
return nil, fmt.Errorf("replicaset %s/%s is missing owner", ns, podInfo.Name)
|
||||
}
|
||||
|
||||
d, err := kubeClient.AppsV1().Deployments(ns).Get(ctx, rsOwner.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get deployment: %w", err)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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(podInfo *downward.PodInfo, cfg *supervisor.Config) error {
|
||||
serverInstallationNamespace := podInfo.Namespace
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
kubeClient, pinnipedClient, err := newClients()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create k8s client: %w", err)
|
||||
}
|
||||
|
||||
kubeInformers := kubeinformers.NewSharedInformerFactoryWithOptions(
|
||||
kubeClient,
|
||||
defaultResyncInterval,
|
||||
kubeinformers.WithNamespace(serverInstallationNamespace),
|
||||
)
|
||||
|
||||
pinnipedInformers := pinnipedinformers.NewSharedInformerFactoryWithOptions(
|
||||
pinnipedClient,
|
||||
defaultResyncInterval,
|
||||
pinnipedinformers.WithNamespace(serverInstallationNamespace),
|
||||
)
|
||||
|
||||
// Serve the /healthz endpoint and make all other paths result in 404.
|
||||
healthMux := http.NewServeMux()
|
||||
healthMux.Handle("/healthz", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
_, _ = writer.Write([]byte("ok"))
|
||||
}))
|
||||
|
||||
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,
|
||||
dynamicUpstreamIDPProvider,
|
||||
&secretCache,
|
||||
kubeClient.CoreV1().Secrets(serverInstallationNamespace),
|
||||
)
|
||||
|
||||
supervisorDeployment, err := getSupervisorDeployment(ctx, kubeClient, podInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get supervisor deployment: %w", err)
|
||||
}
|
||||
|
||||
startControllers(
|
||||
ctx,
|
||||
cfg,
|
||||
oidProvidersManager,
|
||||
dynamicJWKSProvider,
|
||||
dynamicTLSCertProvider,
|
||||
dynamicUpstreamIDPProvider,
|
||||
&secretCache,
|
||||
supervisorDeployment,
|
||||
kubeClient,
|
||||
pinnipedClient,
|
||||
kubeInformers,
|
||||
pinnipedInformers,
|
||||
)
|
||||
|
||||
//nolint: gosec // Intentionally binding to all network interfaces.
|
||||
httpListener, err := net.Listen("tcp", ":8080")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create listener: %w", err)
|
||||
}
|
||||
defer func() { _ = httpListener.Close() }()
|
||||
start(ctx, httpListener, oidProvidersManager)
|
||||
|
||||
//nolint: gosec // Intentionally binding to all network interfaces.
|
||||
httpsListener, err := tls.Listen("tcp", ":8443", &tls.Config{
|
||||
MinVersion: tls.VersionTLS12, // Allow v1.2 because clients like the default `curl` on MacOS don't support 1.3 yet.
|
||||
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert := dynamicTLSCertProvider.GetTLSCert(strings.ToLower(info.ServerName))
|
||||
defaultCert := dynamicTLSCertProvider.GetDefaultTLSCert()
|
||||
plog.Debug("GetCertificate called for port 8443",
|
||||
"info.ServerName", info.ServerName,
|
||||
"foundSNICert", cert != nil,
|
||||
"foundDefaultCert", defaultCert != nil,
|
||||
)
|
||||
if cert == nil {
|
||||
cert = defaultCert
|
||||
}
|
||||
return cert, nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create listener: %w", err)
|
||||
}
|
||||
defer func() { _ = httpsListener.Close() }()
|
||||
start(ctx, httpsListener, oidProvidersManager)
|
||||
|
||||
plog.Debug("supervisor is ready",
|
||||
"httpAddress", httpListener.Addr().String(),
|
||||
"httpsAddress", httpsListener.Addr().String(),
|
||||
)
|
||||
|
||||
gotSignal := waitForSignal()
|
||||
plog.Debug("supervisor exiting", "signal", gotSignal)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
// Discover in which namespace we are installed.
|
||||
podInfo, err := downward.Load(os.Args[1])
|
||||
if err != nil {
|
||||
klog.Fatal(fmt.Errorf("could not read pod metadata: %w", err))
|
||||
}
|
||||
|
||||
// Read the server config file.
|
||||
cfg, err := supervisor.FromPath(os.Args[2])
|
||||
if err != nil {
|
||||
klog.Fatal(fmt.Errorf("could not load config: %w", err))
|
||||
}
|
||||
|
||||
if err := run(podInfo, cfg); err != nil {
|
||||
klog.Fatal(err)
|
||||
}
|
||||
}
|
||||
22
cmd/pinniped/cmd/alpha.go
Normal file
22
cmd/pinniped/cmd/alpha.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
var alphaCmd = &cobra.Command{
|
||||
Use: "alpha",
|
||||
Short: "alpha",
|
||||
Long: "alpha subcommands (syntax or flags are still subject to change)",
|
||||
SilenceUsage: true, // do not print usage message when commands fail
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(alphaCmd)
|
||||
}
|
||||
24
cmd/pinniped/cmd/cobra_util.go
Normal file
24
cmd/pinniped/cmd/cobra_util.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
// mustMarkRequired marks the given flags as required on the provided cobra.Command. If any of the names are wrong, it panics.
|
||||
func mustMarkRequired(cmd *cobra.Command, flags ...string) {
|
||||
for _, flag := range flags {
|
||||
if err := cmd.MarkFlagRequired(flag); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mustMarkHidden marks the given flags as hidden on the provided cobra.Command. If any of the names are wrong, it panics.
|
||||
func mustMarkHidden(cmd *cobra.Command, flags ...string) {
|
||||
for _, flag := range flags {
|
||||
if err := cmd.Flags().MarkHidden(flag); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
21
cmd/pinniped/cmd/cobra_util_test.go
Normal file
21
cmd/pinniped/cmd/cobra_util_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMustMarkRequired(t *testing.T) {
|
||||
require.NotPanics(t, func() { mustMarkRequired(&cobra.Command{}) })
|
||||
require.NotPanics(t, func() {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("known-flag", "", "")
|
||||
mustMarkRequired(cmd, "known-flag")
|
||||
})
|
||||
require.Panics(t, func() { mustMarkRequired(&cobra.Command{}, "unknown-flag") })
|
||||
}
|
||||
136
cmd/pinniped/cmd/deprecated.go
Normal file
136
cmd/pinniped/cmd/deprecated.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(legacyGetKubeconfigCommand(kubeconfigRealDeps()))
|
||||
rootCmd.AddCommand(legacyExchangeTokenCommand(staticLoginRealDeps()))
|
||||
}
|
||||
|
||||
func legacyGetKubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
|
||||
var (
|
||||
cmd = &cobra.Command{
|
||||
Hidden: true,
|
||||
Deprecated: "Please use `pinniped get kubeconfig` instead.",
|
||||
|
||||
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
|
||||
`),
|
||||
}
|
||||
token string
|
||||
kubeconfig string
|
||||
contextOverride string
|
||||
namespace string
|
||||
authenticatorType string
|
||||
authenticatorName string
|
||||
)
|
||||
|
||||
cmd.Flags().StringVar(&token, "token", "", "Credential to include in the resulting kubeconfig output (Required)")
|
||||
cmd.Flags().StringVar(&kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
|
||||
cmd.Flags().StringVar(&contextOverride, "kubeconfig-context", "", "Kubeconfig context override")
|
||||
cmd.Flags().StringVar(&namespace, "pinniped-namespace", "pinniped-concierge", "Namespace in which Pinniped was installed")
|
||||
cmd.Flags().StringVar(&authenticatorType, "authenticator-type", "", "Authenticator type (e.g., 'webhook', 'jwt')")
|
||||
cmd.Flags().StringVar(&authenticatorName, "authenticator-name", "", "Authenticator name")
|
||||
mustMarkRequired(cmd, "token")
|
||||
plog.RemoveKlogGlobalFlags()
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
return runGetKubeconfig(cmd.OutOrStdout(), deps, getKubeconfigParams{
|
||||
kubeconfigPath: kubeconfig,
|
||||
kubeconfigContextOverride: contextOverride,
|
||||
staticToken: token,
|
||||
concierge: getKubeconfigConciergeParams{
|
||||
namespace: namespace,
|
||||
authenticatorName: authenticatorName,
|
||||
authenticatorType: authenticatorType,
|
||||
},
|
||||
})
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func legacyExchangeTokenCommand(deps staticLoginDeps) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Hidden: true,
|
||||
Deprecated: "Please use `pinniped login static` instead.",
|
||||
|
||||
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", "jwt")
|
||||
- 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
|
||||
`),
|
||||
}
|
||||
plog.RemoveKlogGlobalFlags()
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
// Make a little helper to grab OS environment variables and keep a list that were missing.
|
||||
var missing []string
|
||||
getEnv := func(name string) string {
|
||||
value, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
missing = append(missing, name)
|
||||
}
|
||||
return value
|
||||
}
|
||||
flags := staticLoginParams{
|
||||
staticToken: getEnv("PINNIPED_TOKEN"),
|
||||
conciergeEnabled: true,
|
||||
conciergeNamespace: getEnv("PINNIPED_NAMESPACE"),
|
||||
conciergeAuthenticatorType: getEnv("PINNIPED_AUTHENTICATOR_TYPE"),
|
||||
conciergeAuthenticatorName: getEnv("PINNIPED_AUTHENTICATOR_NAME"),
|
||||
conciergeEndpoint: getEnv("PINNIPED_K8S_API_ENDPOINT"),
|
||||
conciergeCABundle: base64.StdEncoding.EncodeToString([]byte(getEnv("PINNIPED_CA_BUNDLE"))),
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("failed to get credential: required environment variable(s) not set: %v", missing)
|
||||
}
|
||||
return runStaticLogin(cmd.OutOrStdout(), deps, flags)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
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)
|
||||
}
|
||||
364
cmd/pinniped/cmd/kubeconfig.go
Normal file
364
cmd/pinniped/cmd/kubeconfig.go
Normal file
@@ -0,0 +1,364 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"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/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go
|
||||
|
||||
conciergev1alpha1 "go.pinniped.dev/generated/1.19/apis/concierge/authentication/v1alpha1"
|
||||
conciergeclientset "go.pinniped.dev/generated/1.19/client/concierge/clientset/versioned"
|
||||
)
|
||||
|
||||
type kubeconfigDeps struct {
|
||||
getPathToSelf func() (string, error)
|
||||
getClientset func(clientcmd.ClientConfig) (conciergeclientset.Interface, error)
|
||||
}
|
||||
|
||||
func kubeconfigRealDeps() kubeconfigDeps {
|
||||
return kubeconfigDeps{
|
||||
getPathToSelf: os.Executable,
|
||||
getClientset: func(clientConfig clientcmd.ClientConfig) (conciergeclientset.Interface, error) {
|
||||
restConfig, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conciergeclientset.NewForConfig(restConfig)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//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
|
||||
caBundlePaths []string
|
||||
requestAudience string
|
||||
}
|
||||
|
||||
type getKubeconfigConciergeParams struct {
|
||||
disabled bool
|
||||
namespace string
|
||||
authenticatorName string
|
||||
authenticatorType string
|
||||
}
|
||||
|
||||
type getKubeconfigParams struct {
|
||||
kubeconfigPath string
|
||||
kubeconfigContextOverride 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
|
||||
)
|
||||
|
||||
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(&flags.concierge.namespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the concierge was installed")
|
||||
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.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.StringSliceVar(&flags.oidc.caBundlePaths, "oidc-ca-bundle", nil, "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)")
|
||||
|
||||
mustMarkHidden(&cmd, "oidc-debug-session-cache")
|
||||
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runGetKubeconfig(cmd.OutOrStdout(), deps, flags) }
|
||||
return &cmd
|
||||
}
|
||||
|
||||
func runGetKubeconfig(out io.Writer, deps kubeconfigDeps, flags getKubeconfigParams) error {
|
||||
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)
|
||||
}
|
||||
|
||||
oidcCABundle, err := loadCABundlePaths(flags.oidc.caBundlePaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read --oidc-ca-bundle: %w", err)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not configure Kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
if !flags.concierge.disabled {
|
||||
authenticator, err := lookupAuthenticator(
|
||||
clientset,
|
||||
flags.concierge.namespace,
|
||||
flags.concierge.authenticatorType,
|
||||
flags.concierge.authenticatorName,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := configureConcierge(authenticator, &flags, cluster, &oidcCABundle, &execConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
return writeConfigAsYAML(out, newExecKubeconfig(cluster, &execConfig))
|
||||
}
|
||||
|
||||
// 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 oidcCABundle != "" {
|
||||
execConfig.Args = append(execConfig.Args, "--ca-bundle-data="+base64.StdEncoding.EncodeToString([]byte(oidcCABundle)))
|
||||
}
|
||||
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)
|
||||
}
|
||||
return writeConfigAsYAML(out, newExecKubeconfig(cluster, &execConfig))
|
||||
}
|
||||
|
||||
func configureConcierge(authenticator metav1.Object, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, oidcCABundle *string, execConfig *clientcmdapi.ExecConfig) 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 == "" {
|
||||
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 == "" {
|
||||
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 == "" {
|
||||
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 == "" {
|
||||
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 *oidcCABundle == "" && 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/%s has invalid spec.tls.certificateAuthorityData: %w", auth.Namespace, auth.Name, err)
|
||||
}
|
||||
*oidcCABundle = string(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
// Append the flags to configure the Concierge credential exchange at runtime.
|
||||
execConfig.Args = append(execConfig.Args,
|
||||
"--enable-concierge",
|
||||
"--concierge-namespace="+flags.concierge.namespace,
|
||||
"--concierge-authenticator-name="+flags.concierge.authenticatorName,
|
||||
"--concierge-authenticator-type="+flags.concierge.authenticatorType,
|
||||
"--concierge-endpoint="+v1Cluster.Server,
|
||||
"--concierge-ca-bundle-data="+base64.StdEncoding.EncodeToString(v1Cluster.CertificateAuthorityData),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadCABundlePaths(paths []string) (string, error) {
|
||||
if len(paths) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
blobs := make([][]byte, 0, len(paths))
|
||||
for _, p := range paths {
|
||||
pem, err := ioutil.ReadFile(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
blobs = append(blobs, pem)
|
||||
}
|
||||
return string(bytes.Join(blobs, []byte("\n"))), nil
|
||||
}
|
||||
|
||||
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 lookupAuthenticator(clientset conciergeclientset.Interface, namespace, authType, authName string) (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(namespace).Get(ctx, authName, metav1.GetOptions{})
|
||||
case "jwt":
|
||||
return clientset.AuthenticationV1alpha1().JWTAuthenticators(namespace).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(namespace).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(namespace).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 in namespace %q (try setting --concierge-namespace)", namespace)
|
||||
}
|
||||
if len(results) > 1 {
|
||||
return nil, fmt.Errorf("multiple authenticators were found in namespace %q, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified", namespace)
|
||||
}
|
||||
return results[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(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
|
||||
}
|
||||
context := currentKubeConfig.Contexts[contextName]
|
||||
if context == nil {
|
||||
return nil, fmt.Errorf("no such context %q", contextName)
|
||||
}
|
||||
return currentKubeConfig.Clusters[context.Cluster], nil
|
||||
}
|
||||
524
cmd/pinniped/cmd/kubeconfig_test.go
Normal file
524
cmd/pinniped/cmd/kubeconfig_test.go
Normal file
@@ -0,0 +1,524 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
kubetesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
conciergev1alpha1 "go.pinniped.dev/generated/1.19/apis/concierge/authentication/v1alpha1"
|
||||
conciergeclientset "go.pinniped.dev/generated/1.19/client/concierge/clientset/versioned"
|
||||
fakeconciergeclientset "go.pinniped.dev/generated/1.19/client/concierge/clientset/versioned/fake"
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
|
||||
func TestGetKubeconfig(t *testing.T) {
|
||||
testCA, err := certauthority.New(pkix.Name{CommonName: "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
|
||||
getPathToSelfErr error
|
||||
getClientsetErr error
|
||||
conciergeObjects []runtime.Object
|
||||
conciergeReactions []kubetesting.Reactor
|
||||
wantError bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantOptionsCount int
|
||||
}{
|
||||
{
|
||||
name: "help flag passed",
|
||||
args: []string{"--help"},
|
||||
wantStdout: here.Doc(`
|
||||
Generate a Pinniped-based kubeconfig for a cluster
|
||||
|
||||
Usage:
|
||||
kubeconfig [flags]
|
||||
|
||||
Flags:
|
||||
--concierge-authenticator-name string Concierge authenticator name (default: autodiscover)
|
||||
--concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover)
|
||||
--concierge-namespace string Namespace in which the concierge was installed (default "pinniped-concierge")
|
||||
-h, --help help for kubeconfig
|
||||
--kubeconfig string Path to kubeconfig file
|
||||
--kubeconfig-context string Kubeconfig context name (default: current active context)
|
||||
--no-concierge Generate a configuration which does not use the concierge, but sends the credential to the cluster directly
|
||||
--oidc-ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated)
|
||||
--oidc-client-id string OpenID Connect client ID (default: autodiscover) (default "pinniped-cli")
|
||||
--oidc-issuer string OpenID Connect issuer URL (default: autodiscover)
|
||||
--oidc-listen-port uint16 TCP port for localhost listener (authorization code flow only)
|
||||
--oidc-request-audience string Request a token with an alternate audience using RFC8693 token exchange
|
||||
--oidc-scopes strings OpenID Connect scopes to request during login (default [offline_access,openid,pinniped:request-audience])
|
||||
--oidc-session-cache string Path to OpenID Connect session cache file
|
||||
--oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL)
|
||||
--static-token string Instead of doing an OIDC-based login, specify a static token
|
||||
--static-token-env string Instead of doing an OIDC-based login, read a static token from the environment
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "fail to get self-path",
|
||||
args: []string{},
|
||||
getPathToSelfErr: fmt.Errorf("some OS error"),
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not determine the Pinniped executable path: some OS error
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid CA bundle paths",
|
||||
args: []string{
|
||||
"--oidc-ca-bundle", "./does/not/exist",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not read --oidc-ca-bundle: open ./does/not/exist: no such file or directory
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid kubeconfig path",
|
||||
args: []string{
|
||||
"--kubeconfig", "./does/not/exist",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not load --kubeconfig: stat ./does/not/exist: no such file or directory
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid kubeconfig context",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--kubeconfig-context", "invalid",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not load --kubeconfig/--kubeconfig-context: no such context "invalid"
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "clientset creation failure",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
},
|
||||
getClientsetErr: fmt.Errorf("some kube error"),
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not configure Kubernetes client: some kube error
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "webhook authenticator not found",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--concierge-authenticator-type", "webhook",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: webhookauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "JWT authenticator not found",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--concierge-authenticator-type", "jwt",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: jwtauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid authenticator type",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--concierge-authenticator-type", "invalid",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid authenticator type "invalid", supported values are "webhook" and "jwt"
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "fail to autodetect authenticator, listing jwtauthenticators fails",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
},
|
||||
conciergeReactions: []kubetesting.Reactor{
|
||||
&kubetesting.SimpleReactor{
|
||||
Verb: "*",
|
||||
Resource: "jwtauthenticators",
|
||||
Reaction: func(kubetesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, fmt.Errorf("some list error")
|
||||
},
|
||||
},
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: failed to list JWTAuthenticator objects for autodiscovery: some list error
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "fail to autodetect authenticator, listing webhookauthenticators fails",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
},
|
||||
conciergeReactions: []kubetesting.Reactor{
|
||||
&kubetesting.SimpleReactor{
|
||||
Verb: "*",
|
||||
Resource: "webhookauthenticators",
|
||||
Reaction: func(kubetesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, fmt.Errorf("some list error")
|
||||
},
|
||||
},
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: failed to list WebhookAuthenticator objects for autodiscovery: some list error
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "fail to autodetect authenticator, none found",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: no authenticators were found in namespace "pinniped-concierge" (try setting --concierge-namespace)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "fail to autodetect authenticator, multiple found",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--concierge-namespace", "test-namespace",
|
||||
},
|
||||
conciergeObjects: []runtime.Object{
|
||||
&conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-1", Namespace: "test-namespace"}},
|
||||
&conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-2", Namespace: "test-namespace"}},
|
||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-3", Namespace: "test-namespace"}},
|
||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-4", Namespace: "test-namespace"}},
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: multiple authenticators were found in namespace "test-namespace", so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "autodetect webhook authenticator, missing --oidc-issuer",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--concierge-namespace", "test-namespace",
|
||||
},
|
||||
conciergeObjects: []runtime.Object{
|
||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator", Namespace: "test-namespace"}},
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not autodiscover --oidc-issuer, and none was provided
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "autodetect JWT authenticator, invalid TLS bundle",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--concierge-namespace", "test-namespace",
|
||||
},
|
||||
conciergeObjects: []runtime.Object{
|
||||
&conciergev1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator", Namespace: "test-namespace"},
|
||||
Spec: conciergev1alpha1.JWTAuthenticatorSpec{
|
||||
TLS: &conciergev1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: "invalid-base64",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator test-namespace/test-authenticator has invalid spec.tls.certificateAuthorityData: illegal base64 data at input byte 7
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid static token flags",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--concierge-namespace", "test-namespace",
|
||||
"--static-token", "test-token",
|
||||
"--static-token-env", "TEST_TOKEN",
|
||||
},
|
||||
conciergeObjects: []runtime.Object{
|
||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator", Namespace: "test-namespace"}},
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: only one of --static-token and --static-token-env can be specified
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "valid static token",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--concierge-namespace", "test-namespace",
|
||||
"--static-token", "test-token",
|
||||
},
|
||||
conciergeObjects: []runtime.Object{
|
||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator", Namespace: "test-namespace"}},
|
||||
},
|
||||
wantStdout: here.Doc(`
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
server: https://fake-server-url-value
|
||||
name: pinniped
|
||||
contexts:
|
||||
- context:
|
||||
cluster: pinniped
|
||||
user: pinniped
|
||||
name: pinniped
|
||||
current-context: pinniped
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: pinniped
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
args:
|
||||
- login
|
||||
- static
|
||||
- --enable-concierge
|
||||
- --concierge-namespace=test-namespace
|
||||
- --concierge-authenticator-name=test-authenticator
|
||||
- --concierge-authenticator-type=webhook
|
||||
- --concierge-endpoint=https://fake-server-url-value
|
||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
- --token=test-token
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "valid static token from env var",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--concierge-namespace", "test-namespace",
|
||||
"--static-token-env", "TEST_TOKEN",
|
||||
},
|
||||
conciergeObjects: []runtime.Object{
|
||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator", Namespace: "test-namespace"}},
|
||||
},
|
||||
wantStdout: here.Doc(`
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
server: https://fake-server-url-value
|
||||
name: pinniped
|
||||
contexts:
|
||||
- context:
|
||||
cluster: pinniped
|
||||
user: pinniped
|
||||
name: pinniped
|
||||
current-context: pinniped
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: pinniped
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
args:
|
||||
- login
|
||||
- static
|
||||
- --enable-concierge
|
||||
- --concierge-namespace=test-namespace
|
||||
- --concierge-authenticator-name=test-authenticator
|
||||
- --concierge-authenticator-type=webhook
|
||||
- --concierge-endpoint=https://fake-server-url-value
|
||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
- --token-env=TEST_TOKEN
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "autodetect JWT authenticator",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
},
|
||||
conciergeObjects: []runtime.Object{
|
||||
&conciergev1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator", Namespace: "pinniped-concierge"},
|
||||
Spec: conciergev1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: "https://example.com/issuer",
|
||||
Audience: "test-audience",
|
||||
TLS: &conciergev1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString(testCA.Bundle()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantStdout: here.Docf(`
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
server: https://fake-server-url-value
|
||||
name: pinniped
|
||||
contexts:
|
||||
- context:
|
||||
cluster: pinniped
|
||||
user: pinniped
|
||||
name: pinniped
|
||||
current-context: pinniped
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: pinniped
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
args:
|
||||
- login
|
||||
- oidc
|
||||
- --enable-concierge
|
||||
- --concierge-namespace=pinniped-concierge
|
||||
- --concierge-authenticator-name=test-authenticator
|
||||
- --concierge-authenticator-type=jwt
|
||||
- --concierge-endpoint=https://fake-server-url-value
|
||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
- --issuer=https://example.com/issuer
|
||||
- --client-id=pinniped-cli
|
||||
- --scopes=offline_access,openid,pinniped:request-audience
|
||||
- --ca-bundle-data=%s
|
||||
- --request-audience=test-audience
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
`, base64.StdEncoding.EncodeToString(testCA.Bundle())),
|
||||
},
|
||||
{
|
||||
name: "autodetect nothing, set a bunch of options",
|
||||
args: []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--concierge-authenticator-type", "webhook",
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
"--oidc-issuer", "https://example.com/issuer",
|
||||
"--oidc-skip-browser",
|
||||
"--oidc-listen-port", "1234",
|
||||
"--oidc-ca-bundle", testCABundlePath,
|
||||
"--oidc-session-cache", "/path/to/cache/dir/sessions.yaml",
|
||||
"--oidc-debug-session-cache",
|
||||
"--oidc-request-audience", "test-audience",
|
||||
},
|
||||
conciergeObjects: []runtime.Object{
|
||||
&conciergev1alpha1.WebhookAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator", Namespace: "pinniped-concierge"},
|
||||
},
|
||||
},
|
||||
wantStdout: here.Docf(`
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
server: https://fake-server-url-value
|
||||
name: pinniped
|
||||
contexts:
|
||||
- context:
|
||||
cluster: pinniped
|
||||
user: pinniped
|
||||
name: pinniped
|
||||
current-context: pinniped
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: pinniped
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
args:
|
||||
- login
|
||||
- oidc
|
||||
- --enable-concierge
|
||||
- --concierge-namespace=pinniped-concierge
|
||||
- --concierge-authenticator-name=test-authenticator
|
||||
- --concierge-authenticator-type=webhook
|
||||
- --concierge-endpoint=https://fake-server-url-value
|
||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
- --issuer=https://example.com/issuer
|
||||
- --client-id=pinniped-cli
|
||||
- --scopes=offline_access,openid,pinniped:request-audience
|
||||
- --skip-browser
|
||||
- --listen-port=1234
|
||||
- --ca-bundle-data=%s
|
||||
- --session-cache=/path/to/cache/dir/sessions.yaml
|
||||
- --debug-session-cache
|
||||
- --request-audience=test-audience
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
`, base64.StdEncoding.EncodeToString(testCA.Bundle())),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := kubeconfigCommand(kubeconfigDeps{
|
||||
getPathToSelf: func() (string, error) {
|
||||
if tt.getPathToSelfErr != nil {
|
||||
return "", tt.getPathToSelfErr
|
||||
}
|
||||
return ".../path/to/pinniped", nil
|
||||
},
|
||||
getClientset: func(clientConfig clientcmd.ClientConfig) (conciergeclientset.Interface, error) {
|
||||
if tt.getClientsetErr != nil {
|
||||
return nil, tt.getClientsetErr
|
||||
}
|
||||
fake := fakeconciergeclientset.NewSimpleClientset(tt.conciergeObjects...)
|
||||
if len(tt.conciergeReactions) > 0 {
|
||||
fake.ReactionChain = tt.conciergeReactions
|
||||
}
|
||||
return fake, 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
22
cmd/pinniped/cmd/login.go
Normal file
22
cmd/pinniped/cmd/login.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
var loginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "login",
|
||||
Long: "Login to a Pinniped server",
|
||||
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
|
||||
func init() {
|
||||
rootCmd.AddCommand(loginCmd)
|
||||
}
|
||||
237
cmd/pinniped/cmd/login_oidc.go
Normal file
237
cmd/pinniped/cmd/login_oidc.go
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright 2020 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"
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
"k8s.io/klog/v2/klogr"
|
||||
|
||||
"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(oidcLoginCommandRealDeps()))
|
||||
}
|
||||
|
||||
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
|
||||
conciergeNamespace string
|
||||
conciergeAuthenticatorType string
|
||||
conciergeAuthenticatorName string
|
||||
conciergeEndpoint string
|
||||
conciergeCABundle string
|
||||
}
|
||||
|
||||
func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
|
||||
var (
|
||||
cmd = cobra.Command{
|
||||
Args: cobra.NoArgs,
|
||||
Use: "oidc --issuer ISSUER",
|
||||
Short: "Login using an OpenID Connect provider",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
flags oidcLoginFlags
|
||||
)
|
||||
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 endcoded 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, "Exchange the OIDC ID token with the Pinniped concierge during login")
|
||||
cmd.Flags().StringVar(&flags.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 Pinniped concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
|
||||
|
||||
mustMarkHidden(&cmd, "debug-session-cache")
|
||||
mustMarkRequired(&cmd, "issuer")
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runOIDCLogin(cmd, deps, flags) }
|
||||
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.WithNamespace(flags.conciergeNamespace),
|
||||
conciergeclient.WithEndpoint(flags.conciergeEndpoint),
|
||||
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
|
||||
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid concierge parameters: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --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 len(flags.caBundlePaths) > 0 || len(flags.caBundleData) > 0 {
|
||||
client, err := makeClient(flags.caBundlePaths, flags.caBundleData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts = append(opts, oidcclient.WithClient(client))
|
||||
}
|
||||
|
||||
// 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.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if concierge != nil {
|
||||
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:
|
||||
// $XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should
|
||||
// be stored. If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used.
|
||||
// [1] https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||
func mustGetConfigDir() string {
|
||||
const xdgAppName = "pinniped"
|
||||
|
||||
if path := os.Getenv("XDG_CONFIG_HOME"); path != "" {
|
||||
return filepath.Join(path, xdgAppName)
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return filepath.Join(home, ".config", xdgAppName)
|
||||
}
|
||||
237
cmd/pinniped/cmd/login_oidc_test.go
Normal file
237
cmd/pinniped/cmd/login_oidc_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"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/testutil"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
"go.pinniped.dev/pkg/oidcclient"
|
||||
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||
)
|
||||
|
||||
func TestLoginOIDCCommand(t *testing.T) {
|
||||
cfgDir := mustGetConfigDir()
|
||||
|
||||
testCA, err := certauthority.New(pkix.Name{CommonName: "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
|
||||
wantOptionsCount int
|
||||
}{
|
||||
{
|
||||
name: "help flag passed",
|
||||
args: []string{"--help"},
|
||||
wantStdout: here.Doc(`
|
||||
Login using an OpenID Connect provider
|
||||
|
||||
Usage:
|
||||
oidc --issuer ISSUER [flags]
|
||||
|
||||
Flags:
|
||||
--ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated)
|
||||
--ca-bundle-data strings Base64 endcoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated)
|
||||
--client-id string OpenID Connect client ID (default "pinniped-cli")
|
||||
--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 Pinniped concierge endpoint
|
||||
--concierge-namespace string Namespace in which the concierge was installed (default "pinniped-concierge")
|
||||
--enable-concierge Exchange the OIDC ID token with the Pinniped concierge during 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,
|
||||
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: "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
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "success with minimal options",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
},
|
||||
wantOptionsCount: 3,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "success with all options",
|
||||
args: []string{
|
||||
"--client-id", "test-client-id",
|
||||
"--issuer", "test-issuer",
|
||||
"--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-namespace", "test-namespace",
|
||||
"--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()),
|
||||
},
|
||||
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) {
|
||||
var (
|
||||
gotOptions []oidcclient.Option
|
||||
)
|
||||
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)
|
||||
|
||||
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")
|
||||
require.Len(t, gotOptions, tt.wantOptionsCount)
|
||||
})
|
||||
}
|
||||
}
|
||||
120
cmd/pinniped/cmd/login_static.go
Normal file
120
cmd/pinniped/cmd/login_static.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright 2020 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/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
|
||||
conciergeNamespace string
|
||||
conciergeAuthenticatorType string
|
||||
conciergeAuthenticatorName string
|
||||
conciergeEndpoint string
|
||||
conciergeCABundle 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
|
||||
)
|
||||
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, "Exchange the token with the Pinniped concierge during login")
|
||||
cmd.Flags().StringVar(&flags.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 Pinniped concierge endpoint")
|
||||
cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the concierge")
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) }
|
||||
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.WithNamespace(flags.conciergeNamespace),
|
||||
conciergeclient.WithEndpoint(flags.conciergeEndpoint),
|
||||
conciergeclient.WithBase64CABundle(flags.conciergeCABundle),
|
||||
conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName),
|
||||
)
|
||||
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}})
|
||||
|
||||
// Exchange that token with the concierge, if configured.
|
||||
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)
|
||||
}
|
||||
180
cmd/pinniped/cmd/login_static_test.go
Normal file
180
cmd/pinniped/cmd/login_static_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"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(pkix.Name{CommonName: "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-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 Pinniped concierge endpoint
|
||||
--concierge-namespace string Namespace in which the concierge was installed (default "pinniped-concierge")
|
||||
--enable-concierge Exchange the token with the Pinniped concierge during 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: "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")
|
||||
})
|
||||
}
|
||||
}
|
||||
26
cmd/pinniped/cmd/root.go
Normal file
26
cmd/pinniped/cmd/root.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//nolint: gochecknoglobals
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "pinniped",
|
||||
Short: "pinniped",
|
||||
Long: "pinniped is the client-side binary for use with Pinniped-enabled Kubernetes clusters.",
|
||||
SilenceUsage: true, // do not print usage message when commands fail
|
||||
}
|
||||
|
||||
// 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 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
31
cmd/pinniped/cmd/testdata/kubeconfig.yaml
vendored
Normal file
31
cmd/pinniped/cmd/testdata/kubeconfig.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== # fake-certificate-authority-data-value
|
||||
server: https://fake-server-url-value
|
||||
name: kind-kind
|
||||
- cluster:
|
||||
certificate-authority-data: c29tZS1vdGhlci1mYWtlLWNlcnRpZmljYXRlLWF1dGhvcml0eS1kYXRhLXZhbHVl # some-other-fake-certificate-authority-data-value
|
||||
server: https://some-other-fake-server-url-value
|
||||
name: some-other-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kind-kind
|
||||
user: kind-kind
|
||||
name: kind-kind
|
||||
- context:
|
||||
cluster: some-other-cluster
|
||||
user: some-other-user
|
||||
name: some-other-context
|
||||
current-context: kind-kind
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kind-kind
|
||||
user:
|
||||
client-certificate-data: ZmFrZS1jbGllbnQtY2VydGlmaWNhdGUtZGF0YS12YWx1ZQ== # fake-client-certificate-data-value
|
||||
client-key-data: ZmFrZS1jbGllbnQta2V5LWRhdGEtdmFsdWU= # fake-client-key-data-value
|
||||
- name: some-other-user
|
||||
user:
|
||||
client-certificate-data: c29tZS1vdGhlci1mYWtlLWNsaWVudC1jZXJ0aWZpY2F0ZS1kYXRhLXZhbHVl # some-other-fake-client-certificate-data-value
|
||||
client-key-data: c29tZS1vdGhlci1mYWtlLWNsaWVudC1rZXktZGF0YS12YWx1ZQ== # some-other-fake-client-key-data-value
|
||||
28
cmd/pinniped/cmd/version.go
Normal file
28
cmd/pinniped/cmd/version.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/component-base/version"
|
||||
)
|
||||
|
||||
//nolint: gochecknoinits
|
||||
func init() {
|
||||
rootCmd.AddCommand(newVersionCommand())
|
||||
}
|
||||
|
||||
func newVersionCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%#v\n", version.Get())
|
||||
return nil
|
||||
},
|
||||
Args: cobra.NoArgs, // do not accept positional arguments for this command
|
||||
Use: "version",
|
||||
Short: "Print the version of this Pinniped CLI",
|
||||
}
|
||||
}
|
||||
85
cmd/pinniped/cmd/version_test.go
Normal file
85
cmd/pinniped/cmd/version_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/here"
|
||||
)
|
||||
|
||||
var (
|
||||
knownGoodUsageRegexpForVersion = here.Doc(`
|
||||
Usage:
|
||||
version \[flags\]
|
||||
|
||||
Flags:
|
||||
-h, --help help for version
|
||||
|
||||
`)
|
||||
|
||||
knownGoodHelpRegexpForVersion = here.Doc(`
|
||||
Print the version of this Pinniped CLI
|
||||
|
||||
Usage:
|
||||
version \[flags\]
|
||||
|
||||
Flags:
|
||||
-h, --help help for version
|
||||
`)
|
||||
|
||||
emptyVersionRegexp = `version.Info{Major:"", Minor:"", GitVersion:".*", GitCommit:".*", GitTreeState:"", BuildDate:".*", GoVersion:".*", Compiler:".*", Platform:".*/.*"}`
|
||||
)
|
||||
|
||||
func TestNewVersionCmd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantError bool
|
||||
wantStdoutRegexp string
|
||||
wantStderrRegexp string
|
||||
}{
|
||||
{
|
||||
name: "no flags",
|
||||
args: []string{},
|
||||
wantStdoutRegexp: emptyVersionRegexp + "\n",
|
||||
},
|
||||
{
|
||||
name: "help flag passed",
|
||||
args: []string{"--help"},
|
||||
wantStdoutRegexp: knownGoodHelpRegexpForVersion,
|
||||
},
|
||||
{
|
||||
name: "arg passed",
|
||||
args: []string{"tuna"},
|
||||
wantError: true,
|
||||
wantStderrRegexp: `Error: unknown command "tuna" for "version"`,
|
||||
wantStdoutRegexp: knownGoodUsageRegexpForVersion,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := newVersionCommand()
|
||||
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)
|
||||
}
|
||||
assert.Regexp(t, tt.wantStdoutRegexp, stdout.String(), "unexpected stdout")
|
||||
assert.Regexp(t, tt.wantStderrRegexp, stderr.String(), "unexpected stderr")
|
||||
})
|
||||
}
|
||||
}
|
||||
10
cmd/pinniped/main.go
Normal file
10
cmd/pinniped/main.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package main
|
||||
|
||||
import "go.pinniped.dev/cmd/pinniped/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
39
deploy/concierge/README.md
Normal file
39
deploy/concierge/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Deploying
|
||||
|
||||
## 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 -`
|
||||
@@ -0,0 +1,170 @@
|
||||
|
||||
---
|
||||
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: Namespaced
|
||||
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:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
@@ -0,0 +1,146 @@
|
||||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.4.0
|
||||
creationTimestamp: null
|
||||
name: webhookauthenticators.authentication.concierge.pinniped.dev
|
||||
spec:
|
||||
group: authentication.concierge.pinniped.dev
|
||||
names:
|
||||
categories:
|
||||
- pinniped
|
||||
- pinniped-authenticator
|
||||
- pinniped-authenticators
|
||||
kind: WebhookAuthenticator
|
||||
listKind: WebhookAuthenticatorList
|
||||
plural: webhookauthenticators
|
||||
singular: webhookauthenticator
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .spec.endpoint
|
||||
name: Endpoint
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: WebhookAuthenticator describes the configuration of a webhook
|
||||
authenticator.
|
||||
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:
|
||||
endpoint:
|
||||
description: Webhook server endpoint URL.
|
||||
minLength: 1
|
||||
pattern: ^https://
|
||||
type: string
|
||||
tls:
|
||||
description: TLS configuration.
|
||||
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:
|
||||
- endpoint
|
||||
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:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
@@ -0,0 +1,111 @@
|
||||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.4.0
|
||||
creationTimestamp: null
|
||||
name: credentialissuers.config.concierge.pinniped.dev
|
||||
spec:
|
||||
group: config.concierge.pinniped.dev
|
||||
names:
|
||||
categories:
|
||||
- pinniped
|
||||
kind: CredentialIssuer
|
||||
listKind: CredentialIssuerList
|
||||
plural: credentialissuers
|
||||
singular: credentialissuer
|
||||
scope: Namespaced
|
||||
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
|
||||
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
|
||||
status:
|
||||
description: Status of the credential issuer.
|
||||
properties:
|
||||
kubeConfigInfo:
|
||||
description: Information needed to form a valid Pinniped-based kubeconfig
|
||||
using this credential issuer.
|
||||
properties:
|
||||
certificateAuthorityData:
|
||||
description: The K8s API server CA bundle.
|
||||
minLength: 1
|
||||
type: string
|
||||
server:
|
||||
description: The K8s API server URL.
|
||||
minLength: 1
|
||||
pattern: ^https://|^http://
|
||||
type: string
|
||||
required:
|
||||
- certificateAuthorityData
|
||||
- server
|
||||
type: object
|
||||
strategies:
|
||||
description: List of integration strategies that were attempted by
|
||||
Pinniped.
|
||||
items:
|
||||
description: Status of an integration strategy that was attempted
|
||||
by Pinniped.
|
||||
properties:
|
||||
lastUpdateTime:
|
||||
description: When the status was last checked.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: Human-readable description of the current status.
|
||||
minLength: 1
|
||||
type: string
|
||||
reason:
|
||||
description: Reason for the current status.
|
||||
enum:
|
||||
- FetchedKey
|
||||
- CouldNotFetchKey
|
||||
type: string
|
||||
status:
|
||||
description: Status of the attempted integration strategy.
|
||||
enum:
|
||||
- Success
|
||||
- Error
|
||||
type: string
|
||||
type:
|
||||
description: Type of integration attempted.
|
||||
enum:
|
||||
- KubeClusterSigningCertificate
|
||||
type: string
|
||||
required:
|
||||
- lastUpdateTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- strategies
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
202
deploy/concierge/deployment.yaml
Normal file
202
deploy/concierge/deployment.yaml
Normal file
@@ -0,0 +1,202 @@
|
||||
#! Copyright 2020 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", "getAndValidateLogLevel")
|
||||
|
||||
#@ if not data.values.into_namespace:
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: #@ data.values.namespace
|
||||
labels: #@ labels()
|
||||
#@ end
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("config")
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
data:
|
||||
#! If names.apiService is changed in this ConfigMap, must also change name of the ClusterIP Service resource below.
|
||||
#@yaml/text-templated-strings
|
||||
pinniped.yaml: |
|
||||
discovery:
|
||||
url: (@= data.values.discovery_url or "null" @)
|
||||
api:
|
||||
servingCertificate:
|
||||
durationSeconds: (@= str(data.values.api_serving_certificate_duration_seconds) @)
|
||||
renewBeforeSeconds: (@= str(data.values.api_serving_certificate_renew_before_seconds) @)
|
||||
names:
|
||||
servingCertificateSecret: (@= defaultResourceNameWithSuffix("api-tls-serving-certificate") @)
|
||||
credentialIssuer: (@= defaultResourceNameWithSuffix("config") @)
|
||||
apiService: (@= defaultResourceNameWithSuffix("api") @)
|
||||
labels: (@= json.encode(labels()).rstrip() @)
|
||||
kubeCertAgent:
|
||||
namePrefix: (@= defaultResourceNameWithSuffix("kube-cert-agent-") @)
|
||||
(@ if data.values.kube_cert_agent_image: @)
|
||||
image: (@= data.values.kube_cert_agent_image @)
|
||||
(@ else: @)
|
||||
(@ if data.values.image_digest: @)
|
||||
image: (@= data.values.image_repo + "@" + data.values.image_digest @)
|
||||
(@ else: @)
|
||||
image: (@= data.values.image_repo + ":" + data.values.image_tag @)
|
||||
(@ end @)
|
||||
(@ end @)
|
||||
(@ if data.values.image_pull_dockerconfigjson: @)
|
||||
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
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: image-pull-secret
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
data:
|
||||
.dockerconfigjson: #@ data.values.image_pull_dockerconfigjson
|
||||
#@ end
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
spec:
|
||||
replicas: #@ data.values.replicas
|
||||
selector:
|
||||
matchLabels: #@ defaultLabel()
|
||||
template:
|
||||
metadata:
|
||||
labels: #@ defaultLabel()
|
||||
annotations:
|
||||
scheduler.alpha.kubernetes.io/critical-pod: ""
|
||||
spec:
|
||||
securityContext:
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
serviceAccountName: #@ defaultResourceName()
|
||||
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
|
||||
imagePullSecrets:
|
||||
- name: image-pull-secret
|
||||
#@ end
|
||||
containers:
|
||||
- name: #@ defaultResourceName()
|
||||
#@ if data.values.image_digest:
|
||||
image: #@ data.values.image_repo + "@" + data.values.image_digest
|
||||
#@ else:
|
||||
image: #@ data.values.image_repo + ":" + data.values.image_tag
|
||||
#@ end
|
||||
imagePullPolicy: IfNotPresent
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
limits:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
args:
|
||||
- --config=/etc/config/pinniped.yaml
|
||||
- --downward-api-path=/etc/podinfo
|
||||
volumeMounts:
|
||||
- name: config-volume
|
||||
mountPath: /etc/config
|
||||
- name: podinfo
|
||||
mountPath: /etc/podinfo
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8443
|
||||
scheme: HTTPS
|
||||
initialDelaySeconds: 2
|
||||
timeoutSeconds: 15
|
||||
periodSeconds: 10
|
||||
failureThreshold: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8443
|
||||
scheme: HTTPS
|
||||
initialDelaySeconds: 2
|
||||
timeoutSeconds: 3
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
volumes:
|
||||
- name: config-volume
|
||||
configMap:
|
||||
name: #@ defaultResourceNameWithSuffix("config")
|
||||
- name: podinfo
|
||||
downwardAPI:
|
||||
items:
|
||||
- path: "labels"
|
||||
fieldRef:
|
||||
fieldPath: metadata.labels
|
||||
- path: "namespace"
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
tolerations:
|
||||
- key: CriticalAddonsOnly
|
||||
operator: Exists
|
||||
- key: node-role.kubernetes.io/master #! Allow running on master nodes too
|
||||
effect: NoSchedule
|
||||
#! "system-cluster-critical" cannot be used outside the kube-system namespace until Kubernetes >= 1.17,
|
||||
#! so we skip setting this for now (see https://github.com/kubernetes/kubernetes/issues/60596).
|
||||
#!priorityClassName: system-cluster-critical
|
||||
#! This will help make sure our multiple pods run on different nodes, making
|
||||
#! our deployment "more" "HA".
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 50
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels: #@ defaultLabel()
|
||||
topologyKey: kubernetes.io/hostname
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
#! If name is changed, must also change names.apiService in the ConfigMap above and spec.service.name in the APIService below.
|
||||
name: #@ defaultResourceNameWithSuffix("api")
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector: #@ defaultLabel()
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
targetPort: 8443
|
||||
---
|
||||
apiVersion: apiregistration.k8s.io/v1
|
||||
kind: APIService
|
||||
metadata:
|
||||
name: v1alpha1.login.concierge.pinniped.dev
|
||||
labels: #@ labels()
|
||||
spec:
|
||||
version: v1alpha1
|
||||
group: login.concierge.pinniped.dev
|
||||
groupPriorityMinimum: 2500
|
||||
versionPriority: 10
|
||||
#! 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
|
||||
38
deploy/concierge/helpers.lib.yaml
Normal file
38
deploy/concierge/helpers.lib.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
#@ load("@ytt:template", "template")
|
||||
|
||||
#@ def defaultResourceName():
|
||||
#@ return data.values.app_name
|
||||
#@ end
|
||||
|
||||
#@ def defaultResourceNameWithSuffix(suffix):
|
||||
#@ return data.values.app_name + "-" + suffix
|
||||
#@ end
|
||||
|
||||
#@ def namespace():
|
||||
#@ if data.values.into_namespace:
|
||||
#@ return data.values.into_namespace
|
||||
#@ else:
|
||||
#@ return data.values.namespace
|
||||
#@ end
|
||||
#@ end
|
||||
|
||||
#@ def defaultLabel():
|
||||
app: #@ data.values.app_name
|
||||
#@ end
|
||||
|
||||
#@ def labels():
|
||||
_: #@ 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
|
||||
204
deploy/concierge/rbac.yaml
Normal file
204
deploy/concierge/rbac.yaml
Normal file
@@ -0,0 +1,204 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix")
|
||||
|
||||
#! Give permission to various cluster-scoped objects
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("aggregated-api-server")
|
||||
labels: #@ labels()
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ namespaces ]
|
||||
verbs: [ get, list, watch ]
|
||||
- apiGroups: [ apiregistration.k8s.io ]
|
||||
resources: [ apiservices ]
|
||||
verbs: [ create, get, list, patch, update, watch ]
|
||||
- apiGroups: [ admissionregistration.k8s.io ]
|
||||
resources: [ validatingwebhookconfigurations, mutatingwebhookconfigurations ]
|
||||
verbs: [ get, list, watch ]
|
||||
- apiGroups: [ policy ]
|
||||
resources: [ podsecuritypolicies ]
|
||||
verbs: [ use ]
|
||||
- apiGroups: [ security.openshift.io ]
|
||||
resources: [ securitycontextconstraints ]
|
||||
verbs: [ use ]
|
||||
resourceNames: [ nonroot ]
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("aggregated-api-server")
|
||||
labels: #@ labels()
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: #@ defaultResourceNameWithSuffix("aggregated-api-server")
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
#! Give permission to various objects within the app's own namespace
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("aggregated-api-server")
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ services ]
|
||||
verbs: [ create, get, list, patch, update, watch ]
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ secrets ]
|
||||
verbs: [ create, get, list, patch, update, watch, delete ]
|
||||
#! We need to be able to CRUD pods in our namespace so we can reconcile the kube-cert-agent pods.
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ pods ]
|
||||
verbs: [ create, get, list, patch, update, watch, delete ]
|
||||
#! We need to be able to exec into pods in our namespace so we can grab the API server's private key
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ pods/exec ]
|
||||
verbs: [ create ]
|
||||
- apiGroups: [ config.concierge.pinniped.dev, authentication.concierge.pinniped.dev ]
|
||||
resources: [ "*" ]
|
||||
verbs: [ create, get, list, update, watch ]
|
||||
---
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("aggregated-api-server")
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: #@ defaultResourceNameWithSuffix("aggregated-api-server")
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
#! Give permission to read pods in the kube-system namespace so we can find the API server's private key
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("kube-system-pod-read")
|
||||
namespace: kube-system
|
||||
labels: #@ labels()
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ pods ]
|
||||
verbs: [ get, list, watch ]
|
||||
---
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("kube-system-pod-read")
|
||||
namespace: kube-system
|
||||
labels: #@ labels()
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: #@ defaultResourceNameWithSuffix("kube-system-pod-read")
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
#! Allow both authenticated and unauthenticated TokenCredentialRequests (i.e. allow all requests)
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("create-token-credential-requests")
|
||||
labels: #@ labels()
|
||||
rules:
|
||||
- apiGroups: [ login.concierge.pinniped.dev ]
|
||||
resources: [ tokencredentialrequests ]
|
||||
verbs: [ create ]
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("create-token-credential-requests")
|
||||
labels: #@ labels()
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: system:authenticated
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
- kind: Group
|
||||
name: system:unauthenticated
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: #@ defaultResourceNameWithSuffix("create-token-credential-requests")
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
#! Give permissions for subjectaccessreviews, tokenreview that is needed by aggregated api servers
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: #@ defaultResourceName()
|
||||
labels: #@ labels()
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: system:auth-delegator
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
#! Give permissions for a special configmap of CA bundles that is needed by aggregated api servers
|
||||
---
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("extension-apiserver-authentication-reader")
|
||||
namespace: kube-system
|
||||
labels: #@ labels()
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: extension-apiserver-authentication-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
#! Give permission to list and watch ConfigMaps in kube-public
|
||||
---
|
||||
kind: Role
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("cluster-info-lister-watcher")
|
||||
namespace: kube-public
|
||||
labels: #@ labels()
|
||||
rules:
|
||||
- apiGroups: [ "" ]
|
||||
resources: [ configmaps ]
|
||||
verbs: [ list, watch ]
|
||||
---
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("cluster-info-lister-watcher")
|
||||
namespace: kube-public
|
||||
labels: #@ labels()
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: #@ defaultResourceNameWithSuffix("cluster-info-lister-watcher")
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
56
deploy/concierge/values.yaml
Normal file
56
deploy/concierge/values.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@data/values
|
||||
---
|
||||
|
||||
app_name: pinniped-concierge
|
||||
|
||||
#! Creates a new namespace statically in yaml with the given name and installs the app into that namespace.
|
||||
namespace: pinniped-concierge
|
||||
#! If specified, assumes that a namespace of the given name already exists and installs the app into that namespace.
|
||||
#! If both `namespace` and `into_namespace` are specified, then only `into_namespace` is used.
|
||||
into_namespace: #! e.g. my-preexisting-namespace
|
||||
|
||||
#! All resources created statically by yaml at install-time and all resources created dynamically
|
||||
#! by controllers at runtime will be labelled with `app: $app_name` and also with the labels
|
||||
#! specified here. The value of `custom_labels` must be a map of string keys to string values.
|
||||
#! The app can be uninstalled either by:
|
||||
#! 1. Deleting the static install-time yaml resources including the static namespace, which will cascade and also delete
|
||||
#! resources that were dynamically created by controllers at runtime
|
||||
#! 2. Or, deleting all resources by label, which does not assume that there was a static install-time yaml namespace.
|
||||
custom_labels: {} #! e.g. {myCustomLabelName: myCustomLabelValue, otherCustomLabelName: otherCustomLabelValue}
|
||||
|
||||
#! Specify how many replicas of the Pinniped server to run.
|
||||
replicas: 2
|
||||
|
||||
#! Specify either an image_digest or an image_tag. If both are given, only image_digest will be used.
|
||||
image_repo: projects.registry.vmware.com/pinniped/pinniped-server
|
||||
image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8
|
||||
image_tag: latest
|
||||
|
||||
#! Optionally specify a different image for the "kube-cert-agent" pod which is scheduled
|
||||
#! on the control plane. This image needs only to include `sleep` and `cat` binaries.
|
||||
#! By default, the same image specified for image_repo/image_digest/image_tag will be re-used.
|
||||
kube_cert_agent_image:
|
||||
|
||||
#! Specifies a secret to be used when pulling the above `image_repo` container image.
|
||||
#! Can be used when the above image_repo is a private registry.
|
||||
#! 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"}}}
|
||||
|
||||
#! Pinniped will try to guess the right K8s API URL for sharing that information with potential clients.
|
||||
#! This settings allows the guess to be overridden.
|
||||
#! Optional.
|
||||
discovery_url: #! e.g., https://example.com
|
||||
|
||||
#! Specify the duration and renewal interval for the API serving certificate.
|
||||
#! The defaults are set to expire the cert about every 30 days, and to rotate it
|
||||
#! 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.
|
||||
23
deploy/concierge/z0_crd_overlay.yaml
Normal file
23
deploy/concierge/z0_crd_overlay.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:overlay", "overlay")
|
||||
#@ load("helpers.lib.yaml", "labels")
|
||||
|
||||
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"credentialissuers.config.concierge.pinniped.dev"}}), expects=1
|
||||
---
|
||||
metadata:
|
||||
#@overlay/match missing_ok=True
|
||||
labels: #@ labels()
|
||||
|
||||
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"webhookauthenticators.authentication.concierge.pinniped.dev"}}), expects=1
|
||||
---
|
||||
metadata:
|
||||
#@overlay/match missing_ok=True
|
||||
labels: #@ labels()
|
||||
|
||||
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"jwtauthenticators.authentication.concierge.pinniped.dev"}}), expects=1
|
||||
---
|
||||
metadata:
|
||||
#@overlay/match missing_ok=True
|
||||
labels: #@ labels()
|
||||
163
deploy/local-user-authenticator/README.md
Normal file
163
deploy/local-user-authenticator/README.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Deploying local-user-authenticator
|
||||
|
||||
## What is local-user-authenticator?
|
||||
|
||||
The local-user-authenticator app is an identity provider used for integration testing and demos.
|
||||
If you would like to demo Pinniped, but you don't have a compatible identity provider handy,
|
||||
you can use Pinniped's local-user-authenticator identity provider. Note that this is not recommended for
|
||||
production use.
|
||||
|
||||
The local-user-authenticator is a Kubernetes Deployment which runs a webhook server that implements the Kubernetes
|
||||
[Webhook Token Authentication interface](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication).
|
||||
|
||||
User accounts can be created and edited dynamically using `kubectl` commands (see below).
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## 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-local-user-authenticator.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/local-user-authenticator` 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/local-user-authenticator/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/local-user-authenticator` 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 local-user-authenticator --diff-changes --file -`
|
||||
|
||||
## Configuring After Installing
|
||||
|
||||
### Create Users
|
||||
|
||||
Use `kubectl` to create, edit, and delete user accounts by creating a `Secret` for each user account in the same
|
||||
namespace where local-user-authenticator is deployed. The name of the `Secret` resource is the username.
|
||||
Store the user's group membership and `bcrypt` encrypted password as the contents of the `Secret`.
|
||||
For example, to create a user named `pinny-the-seal` with the password `password123`
|
||||
who belongs to the groups `group1` and `group2`, use:
|
||||
|
||||
```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://")
|
||||
```
|
||||
|
||||
Note that the above command requires a tool capable of generating a `bcrypt` hash. It 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`).
|
||||
|
||||
### Get the local-user-authenticator App's Auto-Generated Certificate Authority Bundle
|
||||
|
||||
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} \
|
||||
| base64 -d \
|
||||
| tee /tmp/local-user-authenticator-ca
|
||||
```
|
||||
|
||||
### Configuring Pinniped to Use local-user-authenticator as an Identity Provider
|
||||
|
||||
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 [demo](https://pinniped.dev/docs/demo/) for an example.
|
||||
|
||||
## Optional: Manually Testing the Webhook Endpoint After Installing
|
||||
|
||||
The following steps demonstrate the API of the local-user-authenticator app. Typically, a user would not need to
|
||||
interact with this API directly. Pinniped will automatically integrate with this API if the local-user-authenticator
|
||||
is configured as an identity provider for Pinniped.
|
||||
|
||||
1. Start a pod from which you can curl the endpoint from inside the cluster.
|
||||
|
||||
```bash
|
||||
kubectl run curlpod --image=curlimages/curl --command -- /bin/sh -c "while true; do echo hi; sleep 120; done"
|
||||
```
|
||||
|
||||
1. Copy the CA bundle that was fetched above onto the new pod.
|
||||
|
||||
```bash
|
||||
kubectl cp /tmp/local-user-authenticator-ca curlpod:/tmp/local-user-authenticator-ca
|
||||
```
|
||||
|
||||
1. Run a `curl` command to try to authenticate as the user created above.
|
||||
|
||||
```bash
|
||||
kubectl -it exec curlpod -- curl https://local-user-authenticator.local-user-authenticator.svc/authenticate \
|
||||
--cacert /tmp/local-user-authenticator-ca \
|
||||
-H 'Content-Type: application/json' -H 'Accept: application/json' -d '
|
||||
{
|
||||
"apiVersion": "authentication.k8s.io/v1beta1",
|
||||
"kind": "TokenReview",
|
||||
"spec": {
|
||||
"token": "pinny-the-seal:password123"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
When authentication is successful the above command should return some JSON similar to the following.
|
||||
Note that the value of `authenticated` is `true` to indicate a successful authentication.
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "TokenReview",
|
||||
"apiVersion": "authentication.k8s.io/v1beta1",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"spec": {},
|
||||
"status": {
|
||||
"authenticated": true,
|
||||
"user": {
|
||||
"username": "pinny-the-seal",
|
||||
"uid": "19c433ec-8f58-44ca-9ef0-2d1081ccb876",
|
||||
"groups": [
|
||||
"group1",
|
||||
"group2"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Trying the above `curl` command again with the wrong username or password in the body of the request
|
||||
should result in a JSON response which indicates that the authentication failed.
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "TokenReview",
|
||||
"apiVersion": "authentication.k8s.io/v1beta1",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"spec": {},
|
||||
"status": {
|
||||
"user": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Remove the curl pod.
|
||||
|
||||
```bash
|
||||
kubectl delete pod curlpod
|
||||
```
|
||||
83
deploy/local-user-authenticator/deployment.yaml
Normal file
83
deploy/local-user-authenticator/deployment.yaml
Normal file
@@ -0,0 +1,83 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: local-user-authenticator
|
||||
labels:
|
||||
name: local-user-authenticator
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: local-user-authenticator
|
||||
namespace: local-user-authenticator
|
||||
---
|
||||
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: image-pull-secret
|
||||
namespace: local-user-authenticator
|
||||
labels:
|
||||
app: local-user-authenticator
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
data:
|
||||
.dockerconfigjson: #@ data.values.image_pull_dockerconfigjson
|
||||
#@ end
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: local-user-authenticator
|
||||
namespace: local-user-authenticator
|
||||
labels:
|
||||
app: local-user-authenticator
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: local-user-authenticator
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: local-user-authenticator
|
||||
spec:
|
||||
securityContext:
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
serviceAccountName: local-user-authenticator
|
||||
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
|
||||
imagePullSecrets:
|
||||
- name: image-pull-secret
|
||||
#@ end
|
||||
containers:
|
||||
- name: local-user-authenticator
|
||||
#@ if data.values.image_digest:
|
||||
image: #@ data.values.image_repo + "@" + data.values.image_digest
|
||||
#@ else:
|
||||
image: #@ data.values.image_repo + ":" + data.values.image_tag
|
||||
#@ end
|
||||
imagePullPolicy: IfNotPresent
|
||||
command: #! override the default entrypoint
|
||||
- /usr/local/bin/local-user-authenticator
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: local-user-authenticator
|
||||
namespace: local-user-authenticator
|
||||
labels:
|
||||
app: local-user-authenticator
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: local-user-authenticator
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
targetPort: 8443
|
||||
30
deploy/local-user-authenticator/rbac.yaml
Normal file
30
deploy/local-user-authenticator/rbac.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
|
||||
#! Give permission to various objects within the app's own namespace
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: local-user-authenticator
|
||||
namespace: local-user-authenticator
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: [secrets]
|
||||
verbs: [create, get, list, patch, update, watch]
|
||||
---
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: local-user-authenticator
|
||||
namespace: local-user-authenticator
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: local-user-authenticator
|
||||
namespace: local-user-authenticator
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: local-user-authenticator
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
16
deploy/local-user-authenticator/values.yaml
Normal file
16
deploy/local-user-authenticator/values.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
#! Copyright 2020 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: projects.registry.vmware.com/pinniped/pinniped-server
|
||||
image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8
|
||||
image_tag: latest
|
||||
|
||||
#! Specifies a secret to be used when pulling the above `image_repo` container image.
|
||||
#! Can be used when the above image_repo is a private registry.
|
||||
#! 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"}}}
|
||||
184
deploy/supervisor/README.md
Normal file
184
deploy/supervisor/README.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Deploying the Pinniped Supervisor
|
||||
|
||||
## 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 FederationDomain
|
||||
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 `FederationDomain` resources
|
||||
in the same namespace where the Supervisor app was installed. For example:
|
||||
|
||||
```yaml
|
||||
apiVersion: config.supervisor.pinniped.dev/v1alpha1
|
||||
kind: FederationDomain
|
||||
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 FederationDomain.
|
||||
|
||||
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 `FederationDomain` 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.
|
||||
@@ -0,0 +1,158 @@
|
||||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.4.0
|
||||
creationTimestamp: null
|
||||
name: federationdomains.config.supervisor.pinniped.dev
|
||||
spec:
|
||||
group: config.supervisor.pinniped.dev
|
||||
names:
|
||||
categories:
|
||||
- pinniped
|
||||
kind: FederationDomain
|
||||
listKind: FederationDomainList
|
||||
plural: federationdomains
|
||||
singular: federationdomain
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: FederationDomain describes the configuration of an OIDC 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 of the OIDC provider.
|
||||
properties:
|
||||
issuer:
|
||||
description: "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).
|
||||
\n See https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3
|
||||
for more information."
|
||||
minLength: 1
|
||||
type: string
|
||||
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 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
|
||||
(TLS) supported by all major browsers. \n 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. \n
|
||||
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. \n When your Issuer URL's host
|
||||
is an IP address, then this field is ignored. SNI does not work
|
||||
for IP addresses."
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- issuer
|
||||
type: object
|
||||
status:
|
||||
description: Status of the OIDC provider.
|
||||
properties:
|
||||
lastUpdateTime:
|
||||
description: 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).
|
||||
format: date-time
|
||||
type: string
|
||||
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.
|
||||
enum:
|
||||
- Success
|
||||
- Duplicate
|
||||
- Invalid
|
||||
- SameIssuerHostMustUseSameSecret
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
147
deploy/supervisor/deployment.yaml
Normal file
147
deploy/supervisor/deployment.yaml
Normal file
@@ -0,0 +1,147 @@
|
||||
#! Copyright 2020 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", "getAndValidateLogLevel")
|
||||
|
||||
#@ if not data.values.into_namespace:
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: #@ data.values.namespace
|
||||
labels: #@ labels()
|
||||
#@ end
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("static-config")
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
data:
|
||||
#@yaml/text-templated-strings
|
||||
pinniped.yaml: |
|
||||
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
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: image-pull-secret
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
data:
|
||||
.dockerconfigjson: #@ data.values.image_pull_dockerconfigjson
|
||||
#@ end
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
spec:
|
||||
replicas: #@ data.values.replicas
|
||||
selector:
|
||||
matchLabels: #@ defaultLabel()
|
||||
template:
|
||||
metadata:
|
||||
labels: #@ defaultLabel()
|
||||
spec:
|
||||
securityContext:
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
serviceAccountName: #@ defaultResourceName()
|
||||
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
|
||||
imagePullSecrets:
|
||||
- name: image-pull-secret
|
||||
#@ end
|
||||
containers:
|
||||
- name: #@ defaultResourceName()
|
||||
#@ if data.values.image_digest:
|
||||
image: #@ data.values.image_repo + "@" + data.values.image_digest
|
||||
#@ else:
|
||||
image: #@ data.values.image_repo + ":" + data.values.image_tag
|
||||
#@ end
|
||||
imagePullPolicy: IfNotPresent
|
||||
command: #! override the default entrypoint
|
||||
- /usr/local/bin/pinniped-supervisor
|
||||
args:
|
||||
- /etc/podinfo
|
||||
- /etc/config/pinniped.yaml
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
limits:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
volumeMounts:
|
||||
- name: config-volume
|
||||
mountPath: /etc/config
|
||||
- name: podinfo
|
||||
mountPath: /etc/podinfo
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
- containerPort: 8443
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 2
|
||||
timeoutSeconds: 15
|
||||
periodSeconds: 10
|
||||
failureThreshold: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 2
|
||||
timeoutSeconds: 3
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
volumes:
|
||||
- name: config-volume
|
||||
configMap:
|
||||
name: #@ defaultResourceNameWithSuffix("static-config")
|
||||
- name: podinfo
|
||||
downwardAPI:
|
||||
items:
|
||||
- path: "labels"
|
||||
fieldRef:
|
||||
fieldPath: metadata.labels
|
||||
- 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:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 50
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels: #@ defaultLabel()
|
||||
topologyKey: kubernetes.io/hostname
|
||||
38
deploy/supervisor/helpers.lib.yaml
Normal file
38
deploy/supervisor/helpers.lib.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
#@ load("@ytt:template", "template")
|
||||
|
||||
#@ def defaultResourceName():
|
||||
#@ return data.values.app_name
|
||||
#@ end
|
||||
|
||||
#@ def defaultResourceNameWithSuffix(suffix):
|
||||
#@ return data.values.app_name + "-" + suffix
|
||||
#@ end
|
||||
|
||||
#@ def namespace():
|
||||
#@ if data.values.into_namespace:
|
||||
#@ return data.values.into_namespace
|
||||
#@ else:
|
||||
#@ return data.values.namespace
|
||||
#@ end
|
||||
#@ end
|
||||
|
||||
#@ def defaultLabel():
|
||||
app: #@ data.values.app_name
|
||||
#@ end
|
||||
|
||||
#@ def labels():
|
||||
_: #@ 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: []
|
||||
50
deploy/supervisor/rbac.yaml
Normal file
50
deploy/supervisor/rbac.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
#@ load("helpers.lib.yaml", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix")
|
||||
|
||||
#! Give permission to various objects within the app's own namespace
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: [secrets]
|
||||
verbs: [create, get, list, patch, update, watch, delete]
|
||||
- apiGroups: [config.supervisor.pinniped.dev]
|
||||
resources: [federationdomains]
|
||||
verbs: [update, get, list, watch]
|
||||
- apiGroups: [idp.supervisor.pinniped.dev]
|
||||
resources: [oidcidentityproviders]
|
||||
verbs: [get, list, watch]
|
||||
- apiGroups: [idp.supervisor.pinniped.dev]
|
||||
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
|
||||
metadata:
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: #@ defaultResourceName()
|
||||
namespace: #@ namespace()
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: #@ defaultResourceName()
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
93
deploy/supervisor/service.yaml
Normal file
93
deploy/supervisor/service.yaml
Normal file
@@ -0,0 +1,93 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
#@ load("helpers.lib.yaml", "defaultLabel", "labels", "namespace", "defaultResourceName", "defaultResourceNameWithSuffix")
|
||||
|
||||
#@ if data.values.service_http_nodeport_port or data.values.service_https_nodeport_port:
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("nodeport")
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: #@ data.values.app_name
|
||||
ports:
|
||||
#@ if data.values.service_http_nodeport_port:
|
||||
- name: http
|
||||
protocol: TCP
|
||||
port: #@ data.values.service_http_nodeport_port
|
||||
targetPort: 8080
|
||||
#@ if data.values.service_http_nodeport_nodeport:
|
||||
nodePort: #@ data.values.service_http_nodeport_nodeport
|
||||
#@ end
|
||||
#@ end
|
||||
#@ if data.values.service_https_nodeport_port:
|
||||
- name: https
|
||||
protocol: TCP
|
||||
port: #@ data.values.service_https_nodeport_port
|
||||
targetPort: 8443
|
||||
#@ if data.values.service_https_nodeport_nodeport:
|
||||
nodePort: #@ data.values.service_https_nodeport_nodeport
|
||||
#@ end
|
||||
#@ end
|
||||
#@ end
|
||||
|
||||
#@ if data.values.service_http_clusterip_port or data.values.service_https_clusterip_port:
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("clusterip")
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector: #@ defaultLabel()
|
||||
ports:
|
||||
#@ if data.values.service_http_clusterip_port:
|
||||
- name: http
|
||||
protocol: TCP
|
||||
port: #@ data.values.service_http_clusterip_port
|
||||
targetPort: 8080
|
||||
#@ end
|
||||
#@ if data.values.service_https_clusterip_port:
|
||||
- name: https
|
||||
protocol: TCP
|
||||
port: #@ data.values.service_https_clusterip_port
|
||||
targetPort: 8443
|
||||
#@ end
|
||||
#@ end
|
||||
|
||||
#@ if data.values.service_http_loadbalancer_port or data.values.service_https_loadbalancer_port:
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: #@ defaultResourceNameWithSuffix("loadbalancer")
|
||||
namespace: #@ namespace()
|
||||
labels: #@ labels()
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector: #@ defaultLabel()
|
||||
#@ if data.values.service_loadbalancer_ip:
|
||||
loadBalancerIP: #@ data.values.service_loadbalancer_ip
|
||||
#@ end
|
||||
ports:
|
||||
#@ if data.values.service_http_loadbalancer_port:
|
||||
- name: http
|
||||
protocol: TCP
|
||||
port: #@ data.values.service_http_loadbalancer_port
|
||||
targetPort: 8080
|
||||
#@ end
|
||||
#@ if data.values.service_https_loadbalancer_port:
|
||||
- name: https
|
||||
protocol: TCP
|
||||
port: #@ data.values.service_https_loadbalancer_port
|
||||
targetPort: 8443
|
||||
#@ end
|
||||
#@ end
|
||||
58
deploy/supervisor/values.yaml
Normal file
58
deploy/supervisor/values.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@data/values
|
||||
---
|
||||
|
||||
app_name: pinniped-supervisor
|
||||
|
||||
#! Creates a new namespace statically in yaml with the given name and installs the app into that namespace.
|
||||
namespace: pinniped-supervisor
|
||||
#! If specified, assumes that a namespace of the given name already exists and installs the app into that namespace.
|
||||
#! If both `namespace` and `into_namespace` are specified, then only `into_namespace` is used.
|
||||
into_namespace: #! e.g. my-preexisting-namespace
|
||||
|
||||
#! All resources created statically by yaml at install-time and all resources created dynamically
|
||||
#! by controllers at runtime will be labelled with `app: $app_name` and also with the labels
|
||||
#! specified here. The value of `custom_labels` must be a map of string keys to string values.
|
||||
#! The app can be uninstalled either by:
|
||||
#! 1. Deleting the static install-time yaml resources including the static namespace, which will cascade and also delete
|
||||
#! resources that were dynamically created by controllers at runtime
|
||||
#! 2. Or, deleting all resources by label, which does not assume that there was a static install-time yaml namespace.
|
||||
custom_labels: {} #! e.g. {myCustomLabelName: myCustomLabelValue, otherCustomLabelName: otherCustomLabelValue}
|
||||
|
||||
#! Specify how many replicas of the Pinniped server to run.
|
||||
replicas: 2
|
||||
|
||||
#! Specify either an image_digest or an image_tag. If both are given, only image_digest will be used.
|
||||
image_repo: projects.registry.vmware.com/pinniped/pinniped-server
|
||||
image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8
|
||||
image_tag: latest
|
||||
|
||||
#! Specifies a secret to be used when pulling the above `image_repo` container image.
|
||||
#! Can be used when the above image_repo is a private registry.
|
||||
#! 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"}}}
|
||||
|
||||
#! Specify how to expose the Supervisor app's HTTP and/or HTTPS ports as a Service.
|
||||
#! Typically you would set a value for only one of the following service types, for either HTTP or HTTPS depending on your needs.
|
||||
#! An HTTP service should not be exposed outside the cluster. It would not be secure to serve OIDC endpoints to end users via HTTP.
|
||||
#! Setting any of these values means that a Service of that type will be created.
|
||||
#! Note that all port numbers should be numbers (not strings), i.e. use ytt's `--data-value-yaml` instead of `--data-value`.
|
||||
service_http_nodeport_port: #! when specified, creates a NodePort Service with this `port` value, with port 8080 as its `targetPort`; e.g. 31234
|
||||
service_http_nodeport_nodeport: #! the `nodePort` value of the NodePort Service, optional when `service_http_nodeport_port` is specified; e.g. 31234
|
||||
service_http_loadbalancer_port: #! when specified, creates a LoadBalancer Service with this `port` value, with port 8080 as its `targetPort`; e.g. 8443
|
||||
service_http_clusterip_port: #! when specified, creates a ClusterIP Service with this `port` value, with port 8080 as its `targetPort`; e.g. 8443
|
||||
service_https_nodeport_port: #! when specified, creates a NodePort Service with this `port` value, with port 8443 as its `targetPort`; e.g. 31243
|
||||
service_https_nodeport_nodeport: #! the `nodePort` value of the NodePort Service, optional when `service_http_nodeport_port` is specified; e.g. 31243
|
||||
service_https_loadbalancer_port: #! when specified, creates a LoadBalancer Service with this `port` value, with port 8443 as its `targetPort`; e.g. 8443
|
||||
service_https_clusterip_port: #! when specified, creates a ClusterIP Service with this `port` value, with port 8443 as its `targetPort`; e.g. 8443
|
||||
#! The `loadBalancerIP` value of the LoadBalancer Service.
|
||||
#! 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.
|
||||
17
deploy/supervisor/z0_crd_overlay.yaml
Normal file
17
deploy/supervisor/z0_crd_overlay.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:overlay", "overlay")
|
||||
#@ load("helpers.lib.yaml", "labels")
|
||||
|
||||
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"federationdomains.config.supervisor.pinniped.dev"}}), expects=1
|
||||
---
|
||||
metadata:
|
||||
#@overlay/match missing_ok=True
|
||||
labels: #@ labels()
|
||||
|
||||
#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"oidcidentityproviders.idp.supervisor.pinniped.dev"}}), expects=1
|
||||
---
|
||||
metadata:
|
||||
#@overlay/match missing_ok=True
|
||||
labels: #@ labels()
|
||||
@@ -1,14 +0,0 @@
|
||||
# Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# For running Go linters
|
||||
FROM debian:13.2-slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -sfLo /tmp/codecov https://uploader.codecov.io/latest/linux/codecov
|
||||
RUN chmod +x /tmp/codecov
|
||||
|
||||
FROM golang:1.25.5
|
||||
RUN apt-get update -y && apt-get dist-upgrade -y
|
||||
COPY --from=builder /tmp/codecov /usr/local/bin/codecov
|
||||
@@ -1,10 +0,0 @@
|
||||
# Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM gcr.io/go-containerregistry/crane as crane
|
||||
FROM mikefarah/yq:4.50.1 AS yq
|
||||
|
||||
FROM golang:1.25.5
|
||||
COPY --from=yq /usr/bin/yq /usr/local/bin
|
||||
COPY --from=crane /ko-app/crane /usr/local/bin
|
||||
ENTRYPOINT ["bash"]
|
||||
@@ -1,16 +0,0 @@
|
||||
# Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM mikefarah/yq:4.50.1 AS yq
|
||||
|
||||
FROM debian:13.2-slim
|
||||
|
||||
# Note: libdigest-sha-perl is to get shasum, which is used when installing Carvel tools below.
|
||||
RUN apt-get update && apt-get install -y ca-certificates jq curl libdigest-sha-perl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Carvel tools.
|
||||
RUN bash -c "set -eo pipefail; curl -fsL https://carvel.dev/install.sh | bash" && \
|
||||
ytt version && kapp version && kbld version && kwt version && imgpkg version && vendir version
|
||||
|
||||
# Install yq.
|
||||
COPY --from=yq /usr/bin/yq /usr/local/bin/yq
|
||||
@@ -1,29 +0,0 @@
|
||||
# Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# For deploying an EKS cluster and setting it up to run our tests.
|
||||
|
||||
FROM weaveworks/eksctl:v0.221.0 AS eksctl
|
||||
FROM mikefarah/yq:4.50.1 AS yq
|
||||
FROM amazon/aws-cli:2.32.30
|
||||
RUN yum update -y && yum install -y jq perl-Digest-SHA openssl && yum clean all
|
||||
COPY --from=eksctl eksctl /usr/local/bin/eksctl
|
||||
COPY --from=yq /usr/bin/yq /usr/local/bin/yq
|
||||
|
||||
# Install Carvel tools.
|
||||
RUN bash -c "set -eo pipefail; curl -fsL https://carvel.dev/install.sh | bash" && \
|
||||
ytt version && kapp version && kbld version && kwt version && imgpkg version && vendir version
|
||||
|
||||
# Install latest kubectl.
|
||||
RUN curl -sfL "https://dl.k8s.io/release/$(curl -sfL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
|
||||
-o /bin/kubectl && chmod u+x /bin/kubectl
|
||||
|
||||
# Install aws-iam-authenticator.
|
||||
# This gets installed automatically via eksctl, but currently it downloads v0.5.2,
|
||||
# which will give us a v1alpha1 execcredential rather than a v1beta1 which we want.
|
||||
# When this has changed, we can delete this:
|
||||
# https://github.com/weaveworks/eksctl/blob/main/build/docker/Dockerfile#L49
|
||||
RUN curl -sfL \
|
||||
https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/v0.6.14/aws-iam-authenticator_0.6.14_linux_amd64 \
|
||||
-o /usr/local/bin/aws-iam-authenticator \
|
||||
&& chmod u+x /usr/local/bin/aws-iam-authenticator
|
||||
@@ -1,15 +0,0 @@
|
||||
# Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# For running the GitHub CLI.
|
||||
FROM debian:13.2-slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl \
|
||||
-sfLo /tmp/gh.tar.gz \
|
||||
https://github.com/cli/cli/releases/download/v2.40.0/gh_2.40.0_linux_amd64.tar.gz \
|
||||
&& tar -C /tmp --strip-components=1 -xzvf /tmp/gh.tar.gz
|
||||
|
||||
FROM golang:1.25.5
|
||||
COPY --from=builder /tmp/bin/gh /usr/local/bin/gh
|
||||
@@ -1,80 +0,0 @@
|
||||
# Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# For running the integration tests as a client to a k8s cluster
|
||||
|
||||
FROM mikefarah/yq:4.50.1 AS yq
|
||||
|
||||
# We need gcloud for running integration tests against GKE
|
||||
# because the kubeconfig uses gcloud as an `auth-provider`.
|
||||
# Use FROM gcloud-sdk instead of FROM golang because its
|
||||
# a lot easier to install Go than to install gcloud in the
|
||||
# subsequent commands below.
|
||||
FROM google/cloud-sdk:551.0.0-slim
|
||||
|
||||
# Install apache2-utils (for htpasswd to bcrypt passwords for the
|
||||
# local-user-authenticator) and jq.
|
||||
RUN apt-get update && apt-get install -y apache2-utils jq wget zip procps alien google-cloud-sdk-gke-gcloud-auth-plugin && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Print version of gke-gcloud-auth-plugin
|
||||
RUN gke-gcloud-auth-plugin --version
|
||||
|
||||
# Create a non-root user account that can be used to run the tests.
|
||||
RUN useradd --create-home testrunner
|
||||
|
||||
# Install latest beta chrome.
|
||||
RUN \
|
||||
chown root:root /tmp && \
|
||||
chmod 1777 /tmp && \
|
||||
curl -fsSL -o - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add && \
|
||||
echo "deb https://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list && \
|
||||
apt-get -y update && \
|
||||
apt-get -y install google-chrome-beta
|
||||
|
||||
# Output Chrome version used
|
||||
RUN google-chrome --version
|
||||
|
||||
# Install Go. The download URL that can be used below for any version of Go can be found on https://go.dev/dl/
|
||||
ENV PATH /usr/local/go/bin:$PATH
|
||||
RUN curl -fsSL https://go.dev/dl/go1.25.5.linux-amd64.tar.gz -o /tmp/go.tar.gz && \
|
||||
tar -C /usr/local -xzf /tmp/go.tar.gz && \
|
||||
rm /tmp/go.tar.gz && \
|
||||
go version
|
||||
ENV GOPATH /go
|
||||
ENV PATH $GOPATH/bin:$PATH
|
||||
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
|
||||
WORKDIR $GOPATH
|
||||
|
||||
# Install go tools gotestsum and test2json to record the test output in a nice format.
|
||||
RUN go install gotest.tools/gotestsum@latest
|
||||
RUN env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o test2json -ldflags="-s -w" cmd/test2json && \
|
||||
mv test2json /usr/local/bin/test2json
|
||||
|
||||
# Install Carvel tools.
|
||||
RUN bash -c "set -eo pipefail; curl -fsSL https://carvel.dev/install.sh | bash" && \
|
||||
ytt version && kapp version && kbld version && kwt version && imgpkg version && vendir version
|
||||
|
||||
# Install the latest kubectl as documented here: https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
|
||||
RUN curl -fsSL "https://dl.k8s.io/release/$(curl -fsSL "https://dl.k8s.io/release/stable.txt")/bin/linux/amd64/kubectl" \
|
||||
-o /bin/kubectl && chmod 0755 /bin/kubectl
|
||||
|
||||
# Install aws-iam-authenticator
|
||||
RUN curl -fsSL \
|
||||
https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/v0.6.14/aws-iam-authenticator_0.6.14_linux_amd64 \
|
||||
-o /bin/aws-iam-authenticator \
|
||||
&& chmod 0755 /bin/aws-iam-authenticator
|
||||
|
||||
# Install TMC CLI.
|
||||
# Update: The TMC CLI has been deprecated and replaced by the tanzu CLI. Commenting this out for now.
|
||||
#RUN curl -fsSL https://tanzuuserauthentication.stable.tmc-dev.cloud.vmware.com/v1alpha/system/binaries \
|
||||
# | jq -r .versions[].linuxX64 \
|
||||
# | xargs curl -fsSL -o /bin/tmc && chmod 0755 /bin/tmc && \
|
||||
# tmc version
|
||||
|
||||
# Install yq.
|
||||
COPY --from=yq /usr/bin/yq /usr/local/bin/yq
|
||||
|
||||
# install latest nmap
|
||||
RUN wget https://nmap.org/dist/nmap-7.92-1.x86_64.rpm &&\
|
||||
alien nmap-7.92-1.x86_64.rpm &&\
|
||||
dpkg -i nmap_7.92-2_amd64.deb
|
||||
@@ -1,80 +0,0 @@
|
||||
# Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# For running the integration tests as a client to a k8s cluster
|
||||
|
||||
FROM mikefarah/yq:4.50.1 AS yq
|
||||
|
||||
# We need gcloud for running integration tests against GKE
|
||||
# because the kubeconfig uses gcloud as an `auth-provider`.
|
||||
# Use FROM gcloud-sdk instead of FROM golang because its
|
||||
# a lot easier to install Go than to install gcloud in the
|
||||
# subsequent commands below.
|
||||
FROM google/cloud-sdk:551.0.0-slim
|
||||
|
||||
# Install apache2-utils (for htpasswd to bcrypt passwords for the
|
||||
# local-user-authenticator) and jq.
|
||||
RUN apt-get update && apt-get install -y apache2-utils jq wget zip procps alien google-cloud-sdk-gke-gcloud-auth-plugin && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Print version of gke-gcloud-auth-plugin
|
||||
RUN gke-gcloud-auth-plugin --version
|
||||
|
||||
# Create a non-root user account that can be used to run the tests.
|
||||
RUN useradd --create-home testrunner
|
||||
|
||||
# Install latest stable chrome.
|
||||
RUN \
|
||||
chown root:root /tmp && \
|
||||
chmod 1777 /tmp && \
|
||||
curl -fsSL -o - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add && \
|
||||
echo "deb https://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list && \
|
||||
apt-get -y update && \
|
||||
apt-get -y install google-chrome-stable
|
||||
|
||||
# Output Chrome version used
|
||||
RUN google-chrome --version
|
||||
|
||||
# Install Go. The download URL that can be used below for any version of Go can be found on https://go.dev/dl/
|
||||
ENV PATH /usr/local/go/bin:$PATH
|
||||
RUN curl -fsSL https://go.dev/dl/go1.25.5.linux-amd64.tar.gz -o /tmp/go.tar.gz && \
|
||||
tar -C /usr/local -xzf /tmp/go.tar.gz && \
|
||||
rm /tmp/go.tar.gz && \
|
||||
go version
|
||||
ENV GOPATH /go
|
||||
ENV PATH $GOPATH/bin:$PATH
|
||||
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
|
||||
WORKDIR $GOPATH
|
||||
|
||||
# Install go tools gotestsum and test2json to record the test output in a nice format.
|
||||
RUN go install gotest.tools/gotestsum@latest
|
||||
RUN env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o test2json -ldflags="-s -w" cmd/test2json && \
|
||||
mv test2json /usr/local/bin/test2json
|
||||
|
||||
# Install Carvel tools.
|
||||
RUN bash -c "set -eo pipefail; curl -fsSL https://carvel.dev/install.sh | bash" && \
|
||||
ytt version && kapp version && kbld version && kwt version && imgpkg version && vendir version
|
||||
|
||||
# Install the latest kubectl as documented here: https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
|
||||
RUN curl -fsSL "https://dl.k8s.io/release/$(curl -fsSL "https://dl.k8s.io/release/stable.txt")/bin/linux/amd64/kubectl" \
|
||||
-o /bin/kubectl && chmod 0755 /bin/kubectl
|
||||
|
||||
# Install aws-iam-authenticator
|
||||
RUN curl -fsSL \
|
||||
https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/v0.6.14/aws-iam-authenticator_0.6.14_linux_amd64 \
|
||||
-o /bin/aws-iam-authenticator \
|
||||
&& chmod 0755 /bin/aws-iam-authenticator
|
||||
|
||||
# Install TMC CLI.
|
||||
# Update: The TMC CLI has been deprecated and replaced by the tanzu CLI. Commenting this out for now.
|
||||
#RUN curl -fsSL https://tanzuuserauthentication.stable.tmc-dev.cloud.vmware.com/v1alpha/system/binaries \
|
||||
# | jq -r .versions[].linuxX64 \
|
||||
# | xargs curl -fsSL -o /bin/tmc && chmod 0755 /bin/tmc && \
|
||||
# tmc version
|
||||
|
||||
# Install yq.
|
||||
COPY --from=yq /usr/bin/yq /usr/local/bin/yq
|
||||
|
||||
# install latest nmap
|
||||
RUN wget https://nmap.org/dist/nmap-7.92-1.x86_64.rpm &&\
|
||||
alien nmap-7.92-1.x86_64.rpm &&\
|
||||
dpkg -i nmap_7.92-2_amd64.deb
|
||||
@@ -1,34 +0,0 @@
|
||||
# Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# For deploying apps onto Kubernetes clusters (including GKE)
|
||||
|
||||
FROM google/cloud-sdk:551.0.0-slim
|
||||
|
||||
# Install apache2-utils (for htpasswd to bcrypt passwords for the
|
||||
# local-user-authenticator) and jq.
|
||||
RUN apt-get update && apt-get install -y apache2-utils jq wget zip procps dnsutils google-cloud-sdk-gke-gcloud-auth-plugin && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Print version of gke-gcloud-auth-plugin
|
||||
RUN gke-gcloud-auth-plugin --version
|
||||
|
||||
# Install Carvel tools.
|
||||
RUN bash -c "set -eo pipefail; curl -fsL https://carvel.dev/install.sh | bash" && \
|
||||
ytt version && kapp version && kbld version && kwt version && imgpkg version && vendir version
|
||||
|
||||
# Install latest kubectl.
|
||||
RUN curl -sfL "https://dl.k8s.io/release/$(curl -sfL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
|
||||
-o /bin/kubectl && chmod u+x /bin/kubectl
|
||||
|
||||
# Install aws-iam-authenticator
|
||||
RUN curl -sfL \
|
||||
https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/v0.6.14/aws-iam-authenticator_0.6.14_linux_amd64 \
|
||||
-o /bin/aws-iam-authenticator \
|
||||
&& chmod u+x /bin/aws-iam-authenticator
|
||||
|
||||
# Install TMC CLI.
|
||||
# Update: The TMC CLI has been deprecated and replaced by the tanzu CLI. Commenting this out for now.
|
||||
#RUN curl -sfL https://tanzuuserauthentication.stable.tmc-dev.cloud.vmware.com/v1alpha/system/binaries \
|
||||
# | jq -r .versions[].linuxX64 \
|
||||
# | xargs curl -sfL -o /bin/tmc && chmod +x /bin/tmc && \
|
||||
# tmc version
|
||||
@@ -1,20 +0,0 @@
|
||||
# Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
ARG GO_VERSION
|
||||
|
||||
FROM golang:${GO_VERSION}
|
||||
|
||||
ARG GO_VERSION
|
||||
ARG K8S_PKG_VERSION
|
||||
ARG CONTROLLER_GEN_VERSION
|
||||
ARG CRD_REF_DOCS_COMMIT_SHA
|
||||
|
||||
ENV GO_VERSION=$GO_VERSION
|
||||
ENV K8S_PKG_VERSION=$K8S_PKG_VERSION
|
||||
ENV CONTROLLER_GEN_VERSION=$CONTROLLER_GEN_VERSION
|
||||
ENV CRD_REF_DOCS_COMMIT_SHA=$CRD_REF_DOCS_COMMIT_SHA
|
||||
|
||||
COPY setup.sh /codegen/
|
||||
|
||||
RUN /codegen/setup.sh
|
||||
@@ -1,116 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "$GO_VERSION" ]; then
|
||||
echo "missing GO_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$K8S_PKG_VERSION" ]; then
|
||||
echo "missing K8S_PKG_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$CONTROLLER_GEN_VERSION" ]; then
|
||||
echo "missing CONTROLLER_GEN_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Debugging output for CI...
|
||||
echo "GO_VERSION: $GO_VERSION"
|
||||
echo "K8S_PKG_VERSION: $K8S_PKG_VERSION"
|
||||
echo "CONTROLLER_GEN_VERSION: $CONTROLLER_GEN_VERSION"
|
||||
echo "CRD_REF_DOCS_COMMIT_SHA: $CRD_REF_DOCS_COMMIT_SHA"
|
||||
|
||||
apt-get update -y && apt-get dist-upgrade -y
|
||||
|
||||
cd /codegen/
|
||||
|
||||
cat <<EOF >tools.go
|
||||
package tools
|
||||
|
||||
import (
|
||||
_ "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
_ "k8s.io/api/core/v1"
|
||||
_ "k8s.io/code-generator"
|
||||
)
|
||||
EOF
|
||||
|
||||
cat <<EOF >go.mod
|
||||
module codegen
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
k8s.io/apimachinery v$K8S_PKG_VERSION
|
||||
k8s.io/code-generator v$K8S_PKG_VERSION
|
||||
k8s.io/api v$K8S_PKG_VERSION
|
||||
)
|
||||
EOF
|
||||
|
||||
# Resolve dependencies and download the modules.
|
||||
echo "Running go mod tidy ..."
|
||||
go mod tidy
|
||||
echo "Running go mod download ..."
|
||||
go mod download
|
||||
|
||||
# Copy the downloaded source code of k8s.io/code-generator so we can "go install" all its commands.
|
||||
rm -rf "$(go env GOPATH)/src"
|
||||
mkdir -p "$(go env GOPATH)/src/k8s.io"
|
||||
cp -pr "$(go env GOMODCACHE)/k8s.io/code-generator@v$K8S_PKG_VERSION" "$(go env GOPATH)/src/k8s.io/code-generator"
|
||||
|
||||
# Install the commands to $GOPATH/bin. Also sed the related shell scripts, but leave those in the src dir.
|
||||
# Note that update-codegen.sh invokes these shell scripts at this src path.
|
||||
# The sed is a dirty hack to avoid having the code-generator shell scripts run go install again.
|
||||
# In version 0.23.0 the line inside the shell script that previously said "go install ..." started
|
||||
# to instead say "GO111MODULE=on go install ..." so this sed is a little wrong, but still seems to work.
|
||||
echo "Running go install for all k8s.io/code-generator commands ..."
|
||||
# Using sed to edit the go.mod file (and then running go mod tidy) is a dirty hack to work around
|
||||
# an issue introduced in Go v1.25. See https://github.com/golang/go/issues/74462.
|
||||
# The version of code-generator used by Kube 1.30 depends on x/tools v0.18.0.
|
||||
# The version of code-generator used by Kube 1.31 depends on x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d.
|
||||
# Other versions of Kube use code-generator versions which do not have this problem.
|
||||
(cd "$(go env GOPATH)/src/k8s.io/code-generator" &&
|
||||
sed -i -E -e 's#golang\.org/x/tools v0\.18\.0#golang\.org/x/tools v0\.24\.1#g' ./go.mod &&
|
||||
sed -i -E -e 's#golang\.org/x/tools v0\.21\.1-.*#golang\.org/x/tools v0\.24\.1#g' ./go.mod &&
|
||||
go mod tidy &&
|
||||
go install -v ./cmd/... &&
|
||||
sed -i -E -e 's/(go install.*)/# \1/g' ./*.sh)
|
||||
|
||||
if [[ ! -f "$(go env GOPATH)/bin/openapi-gen" ]]; then
|
||||
# Starting in Kube 1.30, openapi-gen moved from k8s.io/code-generator to k8s.io/kube-openapi.
|
||||
# Assuming that we are still in the /codegen directory, get the specific version of kube-openapi
|
||||
# that is selected as an indirect dependency by the go.mod.
|
||||
kube_openapi_version=$(go list -m k8s.io/kube-openapi | cut -f2 -d' ')
|
||||
# Install that version of its openapi-gen command.
|
||||
echo "Running go install for openapi-gen $kube_openapi_version ..."
|
||||
# Using sed to edit the go.mod file (and then running go mod tidy) is a dirty hack to work around
|
||||
# an issue introduced in Go v1.25. See https://github.com/golang/go/issues/74462.
|
||||
# If this were not needed, then we could just use "go install" directly without
|
||||
# copying the source code or editing the go.mod file (which is what this script used to do),
|
||||
# like this: go install -v "k8s.io/kube-openapi/cmd/openapi-gen@$kube_openapi_version"
|
||||
# The version of kube-openapi used by Kube 1.30 (and maybe 1.31) depends on x/tools v0.18.0.
|
||||
# The version of kube-openapi used by Kube 1.32 depends on x/tools v0.24.0.
|
||||
# Other versions of Kube use kube-openapi versions which do not have this problem.
|
||||
cp -pr "$(go env GOMODCACHE)/k8s.io/kube-openapi@$kube_openapi_version" "$(go env GOPATH)/src/k8s.io/kube-openapi"
|
||||
(cd "$(go env GOPATH)/src/k8s.io/kube-openapi" &&
|
||||
sed -i -E -e 's#golang\.org/x/tools v0\.18\.0#golang\.org/x/tools v0\.24\.1#g' ./go.mod &&
|
||||
sed -i -E -e 's#golang\.org/x/tools v0\.24\.0#golang\.org/x/tools v0\.24\.1#g' ./go.mod &&
|
||||
go mod tidy &&
|
||||
go install -v ./cmd/openapi-gen)
|
||||
fi
|
||||
|
||||
echo "Running go install for controller-gen ..."
|
||||
go install -v sigs.k8s.io/controller-tools/cmd/controller-gen@v$CONTROLLER_GEN_VERSION
|
||||
|
||||
# We use a commit sha instead of a release semver because this project does not create
|
||||
# releases very often. They seem to only release 1-2 times per year, but commit to
|
||||
# main more often.
|
||||
echo "Running go install for crd-ref-docs ..."
|
||||
go install -v github.com/elastic/crd-ref-docs@$CRD_REF_DOCS_COMMIT_SHA
|
||||
|
||||
# List all the commands that we just installed.
|
||||
echo "Installed the following commands to $(go env GOPATH)/bin:"
|
||||
ls "$(go env GOPATH)/bin"
|
||||
@@ -1,17 +0,0 @@
|
||||
# Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# We would like to use https://github.com/cfmobile/pool-trigger-resource for our pool recycle jobs.
|
||||
# Unfortuntely, the pool-trigger-resource repo seems like it is not maintained by anyone. The most recent
|
||||
# commit was six years ago. On the other hand, its implementation is a shell script which basically
|
||||
# just calls some git commands, so it shouldn't need much maintaince if it works.
|
||||
# This is an updated version of https://github.com/cfmobile/pool-trigger-resource/blob/master/Dockerfile
|
||||
# to use newer versions of linux, jq, and git. The "assets" directory's source code is copied from
|
||||
# https://github.com/cfmobile/pool-trigger-resource/tree/master/assets as of commit efefe018c88e937.
|
||||
|
||||
FROM debian:13.2-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates jq git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ADD assets/ /opt/resource/
|
||||
RUN chmod +rx /opt/resource/*
|
||||
@@ -1,219 +0,0 @@
|
||||
#!/bin/sh
|
||||
# vim: set ft=sh
|
||||
|
||||
set -e
|
||||
|
||||
exec 3>&1 # make stdout available as fd 3 for the result
|
||||
exec 1>&2 # redirect all output to stderr for logging
|
||||
|
||||
# shellcheck source=./common.sh
|
||||
. "$(dirname "$0")"/common.sh
|
||||
|
||||
# for jq
|
||||
PATH=/usr/local/bin:$PATH
|
||||
|
||||
payload=$TMPDIR/git-resource-request
|
||||
|
||||
cat > "$payload" <&0
|
||||
|
||||
|
||||
uri=$(jq -r '.source.uri // ""' < "$payload")
|
||||
branch=$(jq -r '.source.branch // ""' < "$payload")
|
||||
pool_name=$(jq -r '.source.pool // ""' < "$payload")
|
||||
ref=$(jq -r '.version.ref // ""' < "$payload")
|
||||
|
||||
if [ -z "$uri" ]; then
|
||||
config_errors="${config_errors}invalid payload (missing uri)
|
||||
"
|
||||
fi
|
||||
|
||||
if [ -z "$branch" ]; then
|
||||
config_errors="${config_errors}invalid payload (missing branch)
|
||||
"
|
||||
fi
|
||||
|
||||
if [ -z "$pool_name" ]; then
|
||||
config_errors="${config_errors}invalid payload (missing pool)
|
||||
"
|
||||
fi
|
||||
|
||||
if [ -n "$config_errors" ]; then
|
||||
echo "$config_errors"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
###########
|
||||
#
|
||||
# end processing inputs
|
||||
#
|
||||
###########
|
||||
|
||||
###########
|
||||
#
|
||||
# start git setup
|
||||
#
|
||||
###########
|
||||
|
||||
load_pubkey "$payload"
|
||||
|
||||
destination=$TMPDIR/git-resource-repo-cache
|
||||
|
||||
if [ -d "$destination" ]; then
|
||||
cd "$destination"
|
||||
git fetch
|
||||
git reset --hard FETCH_HEAD
|
||||
else
|
||||
branchflag=""
|
||||
if [ -n "$branch" ]; then
|
||||
branchflag="--branch $branch"
|
||||
fi
|
||||
|
||||
git clone "$uri" $branchflag "$destination"
|
||||
cd "$destination"
|
||||
fi
|
||||
|
||||
|
||||
git config user.name "CI Pool Trigger Resource"
|
||||
git config user.email "ci-pool-trigger@localhost"
|
||||
|
||||
###########
|
||||
#
|
||||
# end git setup
|
||||
#
|
||||
###########
|
||||
|
||||
|
||||
###########
|
||||
#
|
||||
# start calculating pending triggers
|
||||
#
|
||||
###########
|
||||
|
||||
if [ -n "$ref" ] && git cat-file -e "$ref"; then
|
||||
ref_exists_and_is_valid=yes
|
||||
fi
|
||||
|
||||
if [ -e "$pool_name/.pending-triggers" ] && [ -e "$pool_name/.pending-removals" ]; then
|
||||
tally_files_exist=yes
|
||||
|
||||
#check validity of tally files
|
||||
fi
|
||||
|
||||
if [ -n "$ref_exists_and_is_valid" ] && [ -n "$tally_files_exist" ]; then
|
||||
files_changed=$(git show --pretty="format:" --name-status -r "$ref"..HEAD -- "$pool_name"/unclaimed/)
|
||||
|
||||
set +e
|
||||
added_items=$(echo "$files_changed" | grep "^A")
|
||||
removed_items=$(echo "$files_changed" | grep "^D")
|
||||
set -e
|
||||
|
||||
if [ -n "$added_items" ]; then
|
||||
num_added_items=$(echo "$added_items" | wc -l)
|
||||
else
|
||||
num_added_items=0
|
||||
fi
|
||||
|
||||
if [ -n "$removed_items" ]; then
|
||||
num_removed_items=$(echo "$removed_items" | wc -l)
|
||||
else
|
||||
num_removed_items=0
|
||||
fi
|
||||
|
||||
old_pending_triggers=$(cat "$pool_name"/.pending-triggers)
|
||||
old_pending_removals=$(cat "$pool_name"/.pending-removals)
|
||||
|
||||
pending_triggers=$(( old_pending_triggers + num_added_items ))
|
||||
|
||||
if [ "$num_removed_items" -gt "$old_pending_removals" ]; then
|
||||
extra_removals=$(( num_removed_items - old_pending_removals ))
|
||||
pending_removals=0
|
||||
pending_triggers=$(( pending_triggers - extra_removals ))
|
||||
else
|
||||
pending_removals=$(( old_pending_removals - num_removed_items ))
|
||||
fi
|
||||
else
|
||||
pending_triggers=$(find "$pool_name"/unclaimed -not -path "*/\.*" -path "$pool_name/unclaimed/*"| wc -l)
|
||||
pending_removals=0
|
||||
fi
|
||||
###########
|
||||
#
|
||||
# end calculating pending triggers
|
||||
#
|
||||
###########
|
||||
|
||||
|
||||
###########
|
||||
#
|
||||
# start handling results
|
||||
#
|
||||
###########
|
||||
|
||||
if [ "$pending_triggers" -gt 0 ]; then
|
||||
last_commit=$(git log -1 --pretty='format:%H')
|
||||
result=$(echo "$last_commit" | jq -R '.' | jq -s "map({ref: .})")
|
||||
else
|
||||
result="[]"
|
||||
fi
|
||||
|
||||
###########
|
||||
#
|
||||
# end handling results
|
||||
#
|
||||
###########
|
||||
|
||||
|
||||
|
||||
###########
|
||||
#
|
||||
# start updating triggers
|
||||
#
|
||||
###########
|
||||
|
||||
if [ "$pending_triggers" -gt 0 ]; then
|
||||
new_pending_triggers=$(( pending_triggers - 1 ))
|
||||
new_pending_removals=$(( pending_removals + 1 ))
|
||||
echo "$new_pending_triggers" > "$pool_name"/.pending-triggers
|
||||
echo "$new_pending_removals" > "$pool_name"/.pending-removals
|
||||
git add "$pool_name"/.pending*
|
||||
|
||||
commit_message="triggering build with pending triggers: $new_pending_triggers; pending removals: $new_pending_removals"
|
||||
|
||||
if [ -n "$ref_exists_and_is_valid" ] && [ -z "$tally_files_exist" ]; then
|
||||
commit_message="$commit_message
|
||||
|
||||
.pending-triggers and/or .pending-removals are missing - re-initializing resource"
|
||||
elif [ -z "$ref_exists_and_is_valid" ] && [ -n "$tally_files_exist" ]; then
|
||||
commit_message="$commit_message
|
||||
|
||||
resource initialized with pre-existing .pending-triggers and .pending-removals - ignoring"
|
||||
elif [ -z "$ref_exists_and_is_valid" ]; then
|
||||
commit_message="$commit_message
|
||||
|
||||
initializing tally files"
|
||||
fi
|
||||
|
||||
if [ -n "$added_items" ]; then
|
||||
commit_message="$commit_message
|
||||
|
||||
additions:
|
||||
$added_items"
|
||||
fi
|
||||
|
||||
if [ -n "$removed_items" ]; then
|
||||
commit_message="$commit_message
|
||||
|
||||
removals:
|
||||
$removed_items"
|
||||
fi
|
||||
|
||||
git commit --allow-empty -m "$commit_message"
|
||||
git push
|
||||
fi
|
||||
|
||||
###########
|
||||
#
|
||||
# end updating triggers
|
||||
#
|
||||
###########
|
||||
|
||||
echo "$result" >&3
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export TMPDIR=${TMPDIR:-/tmp}
|
||||
|
||||
load_pubkey() {
|
||||
local private_key_path=$TMPDIR/git-resource-private-key
|
||||
|
||||
(jq -r '.source.private_key // empty' < "$1") > "$private_key_path"
|
||||
|
||||
if [ -s "$private_key_path" ]; then
|
||||
chmod 0600 "$private_key_path"
|
||||
|
||||
eval "$(ssh-agent)" >/dev/null 2>&1
|
||||
trap 'kill $SSH_AGENT_PID' 0
|
||||
|
||||
ssh-add "$private_key_path" >/dev/null 2>&1
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
cat > ~/.ssh/config <<EOF
|
||||
StrictHostKeyChecking no
|
||||
LogLevel quiet
|
||||
EOF
|
||||
chmod 0600 ~/.ssh/config
|
||||
fi
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user