Compare commits
913
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ae6eb6416 | ||
|
|
6833e1253a | ||
|
|
94806b6876 | ||
|
|
96db8706b5 | ||
|
|
188cd35e41 | ||
|
|
2b2cd69428 | ||
|
|
6cee4f8d41 | ||
|
|
08936fd1d7 | ||
|
|
1c86d14a90 | ||
|
|
0f7f812907 | ||
|
|
cad2b157a8 | ||
|
|
989ef61e8e | ||
|
|
7345fa7342 | ||
|
|
45256de506 | ||
|
|
9f4c1fda01 | ||
|
|
8a7e6469d0 | ||
|
|
6b220a414e | ||
|
|
16d1c987cd | ||
|
|
5772d2f3d0 | ||
|
|
067a6baf56 | ||
|
|
bedb1058f9 | ||
|
|
068ef334f5 | ||
|
|
08ed4e22c6 | ||
|
|
75f392df7a | ||
|
|
75c0c4bad3 | ||
|
|
b236ed97ce | ||
|
|
7de60242c5 | ||
|
|
8dd9827602 | ||
|
|
fc9dbaa9d1 | ||
|
|
f35f8a4c00 | ||
|
|
d02154f0d0 | ||
|
|
65fac99336 | ||
|
|
9d331f95c2 | ||
|
|
b0a034b238 | ||
|
|
1aa41d4c34 | ||
|
|
3ad88f6093 | ||
|
|
7f8f3bc42a | ||
|
|
c8b3d519ed | ||
|
|
0a1e82057c | ||
|
|
4f9185ff41 | ||
|
|
1a4ef9a2a0 | ||
|
|
3fbe0b1e5f | ||
|
|
7c9d0f2b84 | ||
|
|
e689ca78b0 | ||
|
|
fb2cd322ad | ||
|
|
ba889e2764 | ||
|
|
4ac6141c62 | ||
|
|
ed803105c0 | ||
|
|
04f4aca415 | ||
|
|
2207d9bb5e | ||
|
|
dd8dfdba8b | ||
|
|
a8b0161956 | ||
|
|
6dd3ac5f7e | ||
|
|
d844c543b8 | ||
|
|
d5dcfa58f9 | ||
|
|
9b19aeb43e | ||
|
|
8242c81420 | ||
|
|
b3ed59443a | ||
|
|
ad7e30d683 | ||
|
|
d82891d8aa | ||
|
|
d2e0f358b6 | ||
|
|
08496d14fb | ||
|
|
02f1dea1a7 | ||
|
|
d8486de967 | ||
|
|
cecd98e8c2 | ||
|
|
9e94d56db6 | ||
|
|
9e259fa1de | ||
|
|
447f224937 | ||
|
|
f04706c825 | ||
|
|
87d41b083e | ||
|
|
f318682ca2 | ||
|
|
1834f3805b | ||
|
|
64fd330886 | ||
|
|
77793e4dac | ||
|
|
d677a9ec66 | ||
|
|
044f33475e | ||
|
|
490845aa76 | ||
|
|
79556e8bba | ||
|
|
6e65776e26 | ||
|
|
512ada635a | ||
|
|
5f2ab3b635 | ||
|
|
6a715dd846 | ||
|
|
9932d8349a | ||
|
|
8e46e14168 | ||
|
|
abe6e54148 | ||
|
|
547713ff17 | ||
|
|
803e2a4e28 | ||
|
|
862f865e41 | ||
|
|
06327d805a | ||
|
|
c889e4ea50 | ||
|
|
21382dae97 | ||
|
|
920ee609ac | ||
|
|
0fc8ca4838 | ||
|
|
0b9753bad2 | ||
|
|
b0adb9d144 | ||
|
|
eba40a0b32 | ||
|
|
0efba2432d | ||
|
|
f3a7487eb0 | ||
|
|
764999867f | ||
|
|
35ca653b65 | ||
|
|
fcdeb19fab | ||
|
|
37f4b9592c | ||
|
|
879aafe92c | ||
|
|
036d1740ba | ||
|
|
faeff4066e | ||
|
|
7723996bec | ||
|
|
0b2afcfc5d | ||
|
|
549e2da497 | ||
|
|
fa45ca4fd5 | ||
|
|
32aa46c7dc | ||
|
|
9230fe7078 | ||
|
|
5a8da59f6c | ||
|
|
ec53059aa5 | ||
|
|
3c9c6298ef | ||
|
|
3634f2a631 | ||
|
|
1e5164b2b8 | ||
|
|
35f37c1889 | ||
|
|
c7dc7bb9e7 | ||
|
|
e606a39b05 | ||
|
|
b35f862a5d | ||
|
|
89e69a2faf | ||
|
|
cc80ccb6b9 | ||
|
|
abb2148262 | ||
|
|
6984e6fff1 | ||
|
|
05880ca9d6 | ||
|
|
37e8af7a1a | ||
|
|
0ff1a7b4f9 | ||
|
|
1016d04dd9 | ||
|
|
f253a06500 | ||
|
|
f7f17b9cd9 | ||
|
|
7c76a83b38 | ||
|
|
f4a501a5e1 | ||
|
|
da0aefe389 | ||
|
|
1c7f9276d0 | ||
|
|
378e359a39 | ||
|
|
259535340e | ||
|
|
20eb3a913c | ||
|
|
7aea121e51 | ||
|
|
241ece5b8d | ||
|
|
9ccaab082e | ||
|
|
cfcbf9da77 | ||
|
|
d08f1ac62f | ||
|
|
e153ebbee0 | ||
|
|
eb786d85d9 | ||
|
|
a1a8ccc81e | ||
|
|
d291674d4b | ||
|
|
8ede28e8b3 | ||
|
|
d935654dda | ||
|
|
3a59f67718 | ||
|
|
028f854d48 | ||
|
|
dadf08a252 | ||
|
|
6dd2957251 | ||
|
|
3eeff2b713 | ||
|
|
b3e1dd8874 | ||
|
|
4ea1f5a948 | ||
|
|
0e5a4de709 | ||
|
|
c7926295e9 | ||
|
|
e04b3ab6b1 | ||
|
|
0e3cf92b03 | ||
|
|
6cda8bb429 | ||
|
|
07966b7f09 | ||
|
|
fd2193d593 | ||
|
|
60f89856e9 | ||
|
|
3257548c70 | ||
|
|
2781ba1cb5 | ||
|
|
41a53da3e7 | ||
|
|
c8d3aee483 | ||
|
|
7e51e69e5a | ||
|
|
efa7311dcc | ||
|
|
7a1b4006d0 | ||
|
|
578c46db63 | ||
|
|
79fb8e0e28 | ||
|
|
971d251559 | ||
|
|
effa3ce27e | ||
|
|
15d3cdeac9 | ||
|
|
106416f499 | ||
|
|
892ad95068 | ||
|
|
b1d13bc483 | ||
|
|
bd135715be | ||
|
|
e9dac31682 | ||
|
|
71a3906451 | ||
|
|
bcc29f320d | ||
|
|
96d1cf5922 | ||
|
|
e22f0b91ef | ||
|
|
77423cd0d2 | ||
|
|
98fe72c6a6 | ||
|
|
14fb6875a0 | ||
|
|
b14b349305 | ||
|
|
58a314591b | ||
|
|
bbc5c1caf5 | ||
|
|
7be4709589 | ||
|
|
314945fe9c | ||
|
|
20f8248f56 | ||
|
|
f6fbdb7cce | ||
|
|
2dba97bae0 | ||
|
|
ae313c0081 | ||
|
|
abb95b301e | ||
|
|
f890756a4d | ||
|
|
f69a5809b6 | ||
|
|
4e1280021f | ||
|
|
2b33eddb54 | ||
|
|
313d19e5f8 | ||
|
|
abce4ffd0d | ||
|
|
f71a157b17 | ||
|
|
0e0ec87ac9 | ||
|
|
a296a734a1 | ||
|
|
3e44077278 | ||
|
|
288b05df27 | ||
|
|
d969b9553a | ||
|
|
6f8d51c4cc | ||
|
|
3cd11977cc | ||
|
|
f4b43e13e7 | ||
|
|
ff1f5abc06 | ||
|
|
90b7d89521 | ||
|
|
78593a0d0f | ||
|
|
47db834995 | ||
|
|
358d1ed14b | ||
|
|
0e74cc7f5e | ||
|
|
d19d4ec83a | ||
|
|
c2990b1cae | ||
|
|
6a46f19367 | ||
|
|
6c15620ca8 | ||
|
|
83501132f8 | ||
|
|
e30e3a13e7 | ||
|
|
bfb12ac4b4 | ||
|
|
f68e754ce5 | ||
|
|
9b35c5348a | ||
|
|
7a407b8d0d | ||
|
|
2f4fee1f3e | ||
|
|
12470dc7c5 | ||
|
|
40ade10f6f | ||
|
|
a30393a3bd | ||
|
|
ae88332b8d | ||
|
|
cb1017c3fc | ||
|
|
eae432f71f | ||
|
|
8a750cb4e7 | ||
|
|
7593af02f9 | ||
|
|
c34655931f | ||
|
|
8d8c732a13 | ||
|
|
e4936e777a | ||
|
|
256bf61b2b | ||
|
|
8acc29b489 | ||
|
|
7d414e78a1 | ||
|
|
4cd8109816 | ||
|
|
048efeece8 | ||
|
|
056d9b4431 | ||
|
|
be37ac7452 | ||
|
|
a70913b502 | ||
|
|
314fc0d8df | ||
|
|
4f68d5e9eb | ||
|
|
399575ad79 | ||
|
|
96a117bb8d | ||
|
|
f6e33dba07 | ||
|
|
0880a6512e | ||
|
|
5634fa9ed2 | ||
|
|
35c0880463 | ||
|
|
07bd50b8dc | ||
|
|
2dbbd32b6f | ||
|
|
b0c3f625f0 | ||
|
|
b641a766cf | ||
|
|
5d057c9a81 | ||
|
|
2736b8b610 | ||
|
|
01429bfe7c | ||
|
|
b3d05f697d | ||
|
|
eb69037821 | ||
|
|
41f4879f1e | ||
|
|
1f047e7969 | ||
|
|
bd7751def1 | ||
|
|
29126e1528 | ||
|
|
ff94196547 | ||
|
|
16f3da789a | ||
|
|
0d5d595801 | ||
|
|
8e9666b1f5 | ||
|
|
f826671af2 | ||
|
|
c986f3690d | ||
|
|
87f42bb267 | ||
|
|
d06f1a8660 | ||
|
|
bd3488243a | ||
|
|
6801d61662 | ||
|
|
1873be591d | ||
|
|
a4f2fcc330 | ||
|
|
86fdcda98b | ||
|
|
e69c2bedbf | ||
|
|
4bf87e5ba3 | ||
|
|
29bed96c93 | ||
|
|
3370f632d5 | ||
|
|
44667c9ac9 | ||
|
|
43fe777b11 | ||
|
|
cba46e8bfe | ||
|
|
f0a24211c8 | ||
|
|
b232dc926d | ||
|
|
0845eadcca | ||
|
|
d0a2700cd1 | ||
|
|
4d0ce22a45 | ||
|
|
a6533accc0 | ||
|
|
06b81dca4e | ||
|
|
a35165bc34 | ||
|
|
5d070854c8 | ||
|
|
cb4acc8686 | ||
|
|
1baaa020d2 | ||
|
|
301c131ff1 | ||
|
|
e9e6c93249 | ||
|
|
5b845d9c16 | ||
|
|
4595681367 | ||
|
|
a7113a98a4 | ||
|
|
251d1d2184 | ||
|
|
20e205f7d6 | ||
|
|
1485bc8c92 | ||
|
|
c75bdbc87c | ||
|
|
ff1c4d7cb2 | ||
|
|
f59174c38c | ||
|
|
2fb2998a73 | ||
|
|
93ffb4da90 | ||
|
|
a66007da4e | ||
|
|
0d18d908d2 | ||
|
|
c43fabbcf5 | ||
|
|
05e1c4bd6b | ||
|
|
071e23d92e | ||
|
|
49fb9f8ce0 | ||
|
|
3054dc6b4b | ||
|
|
8462b9e593 | ||
|
|
c74ed25a4f | ||
|
|
e0448b0683 | ||
|
|
53beb96081 | ||
|
|
d2d214eb0c | ||
|
|
aad2025d6c | ||
|
|
f1aa4e70ec | ||
|
|
9bc70f15e2 | ||
|
|
dd8599e1ca | ||
|
|
979075b4b5 | ||
|
|
3371005e59 | ||
|
|
2982fd079e | ||
|
|
cf64406ca4 | ||
|
|
eab654612f | ||
|
|
264d879498 | ||
|
|
53135d713f | ||
|
|
569e1458b4 | ||
|
|
7c9b497d74 | ||
|
|
622ab4dfd1 | ||
|
|
983f249652 | ||
|
|
28c0793c54 | ||
|
|
e1fe3dd19d | ||
|
|
4cc3b65952 | ||
|
|
85ff7dd292 | ||
|
|
dbf370d1f3 | ||
|
|
fb28799d81 | ||
|
|
bcfc809377 | ||
|
|
75b65ae961 | ||
|
|
b080c251d1 | ||
|
|
5f13a4b0b3 | ||
|
|
7b99e3c357 | ||
|
|
f379cd85a3 | ||
|
|
7efc01cbab | ||
|
|
197af76701 | ||
|
|
4edb33243d | ||
|
|
c23ae3b814 | ||
|
|
cb0f277304 | ||
|
|
c596bfde59 | ||
|
|
efab683385 | ||
|
|
c16365e1af | ||
|
|
b8e6849d93 | ||
|
|
958f49b93c | ||
|
|
57fec14864 | ||
|
|
81e6dd9c7a | ||
|
|
56e6ff710a | ||
|
|
65fdb50b59 | ||
|
|
a35a5f347e | ||
|
|
67b9673ee1 | ||
|
|
f8a7d98557 | ||
|
|
ffc13f4287 | ||
|
|
d25c178938 | ||
|
|
453e102e71 | ||
|
|
f7e7c5617b | ||
|
|
e13ef26120 | ||
|
|
55f7b0f85f | ||
|
|
667d25383c | ||
|
|
78b07c4b2a | ||
|
|
20345ef7d6 | ||
|
|
bddc9e8817 | ||
|
|
4f8b1b735a | ||
|
|
ea2b70c317 | ||
|
|
6297985a6d | ||
|
|
cd88fd9b3d | ||
|
|
0c7f6fc445 | ||
|
|
014e4ee2a4 | ||
|
|
e0a861b20a | ||
|
|
1cfa358feb | ||
|
|
5ab2dafe6d | ||
|
|
00ee1d96cd | ||
|
|
d325c6da66 | ||
|
|
0198e586d8 | ||
|
|
4f8fa6eb53 | ||
|
|
3188d811e3 | ||
|
|
fba81a04b9 | ||
|
|
477eea302f | ||
|
|
ec09239bed | ||
|
|
5584caa12f | ||
|
|
79d1d47daa | ||
|
|
f77e477588 | ||
|
|
74385cf494 | ||
|
|
5664c8925d | ||
|
|
90b5134afa | ||
|
|
4dbbc47abf | ||
|
|
c98c6532d3 | ||
|
|
2713658ad4 | ||
|
|
647bc48917 | ||
|
|
1a15c14242 | ||
|
|
42f538fae2 | ||
|
|
63fe0ae6f5 | ||
|
|
14734e0ced | ||
|
|
4d6bf43df3 | ||
|
|
01bc01e85f | ||
|
|
f193622eff | ||
|
|
803e0bb93a | ||
|
|
ac6d862198 | ||
|
|
f94df2e113 | ||
|
|
9b1d710bfe | ||
|
|
ad3267d39d | ||
|
|
d140c2893e | ||
|
|
82efd19e2f | ||
|
|
e5ff221cf9 | ||
|
|
18720fae0b | ||
|
|
0e8f1e8ecd | ||
|
|
716df72f68 | ||
|
|
910fa32a30 | ||
|
|
49b2d49484 | ||
|
|
7acc06e826 | ||
|
|
d45b41bea9 | ||
|
|
cd539314c9 | ||
|
|
e4467d1167 | ||
|
|
ed628eda6b | ||
|
|
08b3c9d5c2 | ||
|
|
f87172fedf | ||
|
|
8f9aaa8838 | ||
|
|
547f4045f9 | ||
|
|
9a19e06fad | ||
|
|
92491e5f12 | ||
|
|
a206e576b7 | ||
|
|
6fe6741a76 | ||
|
|
ce881e8a17 | ||
|
|
7c2c5ea5f4 | ||
|
|
1267ba9381 | ||
|
|
34b7e75541 | ||
|
|
647815a2c4 | ||
|
|
60049b1e87 | ||
|
|
61912ab1b7 | ||
|
|
cae2d8499e | ||
|
|
e2c4840f71 | ||
|
|
54a451188c | ||
|
|
6fff19d18c | ||
|
|
a6db1a33f9 | ||
|
|
ba968eaf69 | ||
|
|
c105c23ebd | ||
|
|
0d3c37bff2 | ||
|
|
af4e1d6e83 | ||
|
|
fcc42ba95e | ||
|
|
c9c737ed3a | ||
|
|
dab729dbaa | ||
|
|
487e8f7a3e | ||
|
|
7a5fba66ff | ||
|
|
661c0e1603 | ||
|
|
3205171911 | ||
|
|
6e45ac97c3 | ||
|
|
86f3a14e6b | ||
|
|
258bba7dc7 | ||
|
|
984d19d881 | ||
|
|
2fa318499d | ||
|
|
8582b417c4 | ||
|
|
aa7f738f37 | ||
|
|
a910455aec | ||
|
|
4080b5bfd9 | ||
|
|
821a0341fc | ||
|
|
77814f2885 | ||
|
|
666b6588b3 | ||
|
|
ec57834cf6 | ||
|
|
4bf4679d98 | ||
|
|
43891a6adb | ||
|
|
41f886ddff | ||
|
|
994ac449b8 | ||
|
|
42e15e3b02 | ||
|
|
29fef27eb6 | ||
|
|
8471dacc7d | ||
|
|
e1638d2b3f | ||
|
|
abddd47c6f | ||
|
|
bbca9122d0 | ||
|
|
d52a868954 | ||
|
|
6e77973404 | ||
|
|
008ff68f9e | ||
|
|
f234d1c156 | ||
|
|
935c4b846b | ||
|
|
333ad39b05 | ||
|
|
01dfeab9bf | ||
|
|
92dcccbe66 | ||
|
|
52cd098e29 | ||
|
|
403e2367a6 | ||
|
|
65d04830fb | ||
|
|
2ea831135c | ||
|
|
fb0bfbce69 | ||
|
|
ff94ade43e | ||
|
|
8984e05b13 | ||
|
|
51ede02a93 | ||
|
|
b81ce74681 | ||
|
|
c15a2484d5 | ||
|
|
f30781a0c0 | ||
|
|
45159b8f54 | ||
|
|
3d1ed9f93f | ||
|
|
07abbc81dd | ||
|
|
9ef4d11885 | ||
|
|
37717d53cf | ||
|
|
f36277a8d0 | ||
|
|
35df30fdeb | ||
|
|
027d306c05 | ||
|
|
94cfd30aee | ||
|
|
ebd5b89ecd | ||
|
|
3eab8bdc1f | ||
|
|
654e28a17f | ||
|
|
087cd93d05 | ||
|
|
e400e98db4 | ||
|
|
1ebd0a60dc | ||
|
|
19d018aba5 | ||
|
|
1cb28579b0 | ||
|
|
573b172e2f | ||
|
|
711d9ee6f1 | ||
|
|
ce242e7f70 | ||
|
|
c9d0a97ab4 | ||
|
|
eb92f3b92a | ||
|
|
15fa67353e | ||
|
|
4d1db958fd | ||
|
|
530e69374a | ||
|
|
3ab99aa940 | ||
|
|
75d317a737 | ||
|
|
3192f5fe55 | ||
|
|
caea6e517b | ||
|
|
42d4f812a1 | ||
|
|
e80cab384d | ||
|
|
e6cf453808 | ||
|
|
1087ed710e | ||
|
|
636c125505 | ||
|
|
cb8d00f355 | ||
|
|
2f86185ab6 | ||
|
|
554962cb46 | ||
|
|
543cd00330 | ||
|
|
8f37ba3cce | ||
|
|
f174fb973f | ||
|
|
fc7be892be | ||
|
|
ec344255ee | ||
|
|
9663e9a55e | ||
|
|
33fbb86a37 | ||
|
|
ed165c04c4 | ||
|
|
de9f59e90f | ||
|
|
73a10db14f | ||
|
|
07d623bbcc | ||
|
|
b8c2bb844c | ||
|
|
74adb2951c | ||
|
|
ec514ce115 | ||
|
|
62ef4a1625 | ||
|
|
01224182d0 | ||
|
|
23ebdcfe91 | ||
|
|
c1a9be8fcc | ||
|
|
328ee2f9ca | ||
|
|
f03143dee9 | ||
|
|
ad042aa2c2 | ||
|
|
c740f747d4 | ||
|
|
4c6cb51039 | ||
|
|
acae7bc225 | ||
|
|
ace1a2bf06 | ||
|
|
55770afbce | ||
|
|
e10ef8df49 | ||
|
|
ef889debf0 | ||
|
|
48a12b20a4 | ||
|
|
91e4d17e44 | ||
|
|
5b22ef97de | ||
|
|
fa86e8a5cd | ||
|
|
54770b123f | ||
|
|
505f302aed | ||
|
|
bf484b9c52 | ||
|
|
efda619d98 | ||
|
|
e07eee8907 | ||
|
|
88e7d92100 | ||
|
|
f223d822dd | ||
|
|
9c17d287b7 | ||
|
|
0b4ad6fa6b | ||
|
|
f7986f5c10 | ||
|
|
a1642bd331 | ||
|
|
2f28c91ec6 | ||
|
|
548d3cb507 | ||
|
|
68856b8442 | ||
|
|
47049c2593 | ||
|
|
6c94bdfa45 | ||
|
|
cecec3f11c | ||
|
|
a4dd89d991 | ||
|
|
f2238cca7e | ||
|
|
e7acfef3d3 | ||
|
|
318c33e113 | ||
|
|
ec21b5161e | ||
|
|
eac3165f5d | ||
|
|
8bba2e7f3b | ||
|
|
946faf7feb | ||
|
|
06e9d00024 | ||
|
|
4098b5c798 | ||
|
|
038d34b31a | ||
|
|
d1a295de45 | ||
|
|
257cd32f6d | ||
|
|
f2ed03bcb8 | ||
|
|
072d10c50e | ||
|
|
1efd3321fd | ||
|
|
8d095203e3 | ||
|
|
f9b722ae8c | ||
|
|
97209830d3 | ||
|
|
4f4beae55e | ||
|
|
f00e67ddef | ||
|
|
35a08e6a57 | ||
|
|
2f25d82785 | ||
|
|
b94092034c | ||
|
|
d36875e4c8 | ||
|
|
30bc53d4bb | ||
|
|
749cb5f747 | ||
|
|
1965543be0 | ||
|
|
7941d73335 | ||
|
|
837b65d4e8 | ||
|
|
e03bca2d49 | ||
|
|
e2a334b02b | ||
|
|
fa5faab8eb | ||
|
|
6cad875d67 | ||
|
|
f8b439089a | ||
|
|
2438371d7f | ||
|
|
50857d9a60 | ||
|
|
e4ca10b6c6 | ||
|
|
d0f6d394ee | ||
|
|
52ae7e6f49 | ||
|
|
0fe06230c3 | ||
|
|
dfa9b337b8 | ||
|
|
77b0a09ef8 | ||
|
|
cb2930cc60 | ||
|
|
45bbfe31e6 | ||
|
|
a85ba83ea9 | ||
|
|
f7a5cec94f | ||
|
|
9d0c4c4c24 | ||
|
|
8cbded85ab | ||
|
|
ab7090bcdc | ||
|
|
0489ed9d94 | ||
|
|
887d9223ba | ||
|
|
86251e1dd3 | ||
|
|
96d7f04b56 | ||
|
|
d1ba9f78bf | ||
|
|
d16451795c | ||
|
|
fa9c3c1164 | ||
|
|
bab5baeada | ||
|
|
13d987316d | ||
|
|
e3d8c1aa3c | ||
|
|
80cf11ed17 | ||
|
|
79643e0d9e | ||
|
|
0b1838a17d | ||
|
|
f1aaf469b8 | ||
|
|
c1471efb8e | ||
|
|
813f700ae6 | ||
|
|
9fd2338a2d | ||
|
|
d79cc349dc | ||
|
|
ce7e3a0c7d | ||
|
|
cbeac70d19 | ||
|
|
eb8808df47 | ||
|
|
15f1a4930f | ||
|
|
1cfa95f7e9 | ||
|
|
ec8f460bae | ||
|
|
e3b789b281 | ||
|
|
45ec70d979 | ||
|
|
d1672c0b2a | ||
|
|
6ce9d4a0e8 | ||
|
|
eb596e24c6 | ||
|
|
5c1dfb39e1 | ||
|
|
f2ffc2a5d6 | ||
|
|
bd5c64e36a | ||
|
|
d4d98c577b | ||
|
|
06c75cc374 | ||
|
|
bd79fec11b | ||
|
|
e9089f24de | ||
|
|
ca5b20ea6f | ||
|
|
98896e060a | ||
|
|
33c96049fa | ||
|
|
471f7d5279 | ||
|
|
e5826501cd | ||
|
|
25aad8c4d4 | ||
|
|
ce0e960b30 | ||
|
|
b636c70896 | ||
|
|
0e25d54f8a | ||
|
|
263fde696c | ||
|
|
6682b7888d | ||
|
|
fbb7c5dfb3 | ||
|
|
6fc67fbffe | ||
|
|
5323abaf8e | ||
|
|
115c5df5d7 | ||
|
|
a8ae64fde7 | ||
|
|
eb12380f5e | ||
|
|
b9f7d687e1 | ||
|
|
fad824d902 | ||
|
|
c7a93de129 | ||
|
|
25e2f9c949 | ||
|
|
a5d3289d42 | ||
|
|
ec8420d49e | ||
|
|
d2e8d59dd9 | ||
|
|
4ded8f6723 | ||
|
|
dfd93ae8b0 | ||
|
|
193c40d1cc | ||
|
|
466fe2ad36 | ||
|
|
55fc6d81c2 | ||
|
|
20d84010e8 | ||
|
|
83ebf85934 | ||
|
|
b6bdc0bdee | ||
|
|
6df7a8217f | ||
|
|
5532b28266 | ||
|
|
632490bc09 | ||
|
|
7adcf10040 | ||
|
|
1780f0a5b4 | ||
|
|
f759285784 | ||
|
|
6a8a829840 | ||
|
|
747bffcbdc | ||
|
|
74136a4363 | ||
|
|
4465d1cb8e | ||
|
|
a2ba18944d | ||
|
|
47d4b74b3c | ||
|
|
d32292cc9b | ||
|
|
72c6414dfa | ||
|
|
f5efb1d111 | ||
|
|
3f79959371 | ||
|
|
98d4caeaa9 | ||
|
|
b99fb83d46 | ||
|
|
ae43f40e7b | ||
|
|
e23a9ea5f3 | ||
|
|
6d197cc608 | ||
|
|
d400d44dba | ||
|
|
154365dcb0 | ||
|
|
888153836d | ||
|
|
797ddc3566 | ||
|
|
8f319aacd8 | ||
|
|
c035fd62d0 | ||
|
|
2015e48026 | ||
|
|
be976b90e6 | ||
|
|
311774230f | ||
|
|
ec50198cde | ||
|
|
dd5fc2ec48 | ||
|
|
694f98974d | ||
|
|
a014e6affe | ||
|
|
9a9d54f23a | ||
|
|
51735328d4 | ||
|
|
697bdb4f11 | ||
|
|
3b52e741d6 | ||
|
|
a46c9a8e8e | ||
|
|
3a78882d29 | ||
|
|
c6414e460d | ||
|
|
d66ab3c4aa | ||
|
|
bb0e71556c | ||
|
|
c82e825096 | ||
|
|
4ff90701e2 | ||
|
|
4da726df97 | ||
|
|
e4500fe46f | ||
|
|
a1d11f35d0 | ||
|
|
5c4126db3b | ||
|
|
f3a6ea161f | ||
|
|
dc2650c551 | ||
|
|
677127d7f7 | ||
|
|
beae975ba8 | ||
|
|
7c18a770b0 | ||
|
|
d1df001f37 | ||
|
|
c3dc108b71 | ||
|
|
722d24d60e | ||
|
|
60166a6873 | ||
|
|
ccdab6f4cb | ||
|
|
44466710f6 | ||
|
|
38935eaaf1 | ||
|
|
d200bedc50 | ||
|
|
e792307e6a | ||
|
|
52d7df356c | ||
|
|
760b68cbc0 | ||
|
|
2ffdc0a5fd | ||
|
|
7cf9388c9f | ||
|
|
d3e20aab01 | ||
|
|
4833601a4d | ||
|
|
3fd29d91d3 | ||
|
|
f95ac7dd15 | ||
|
|
aa54360b3e | ||
|
|
bff67fe448 | ||
|
|
12156c8d05 | ||
|
|
60740a2d5d | ||
|
|
fb37808821 | ||
|
|
1bc4a74d49 | ||
|
|
94e5f8b950 | ||
|
|
c565fa784d | ||
|
|
ade72ff83f | ||
|
|
9fc88205e5 | ||
|
|
5771d92b46 | ||
|
|
f2ce00b927 | ||
|
|
9bdd67f4d2 | ||
|
|
93a48766a0 | ||
|
|
8b5e24838e | ||
|
|
1d34a4f283 | ||
|
|
c5f974b95b | ||
|
|
918e78734b | ||
|
|
2bc2181c4b | ||
|
|
fc18837a3e | ||
|
|
2687660581 | ||
|
|
7e3a1d65ad | ||
|
|
1b47e74a59 | ||
|
|
2551a32979 | ||
|
|
1a5003b4ae | ||
|
|
53959571e5 | ||
|
|
1e37dabb7a | ||
|
|
6f10894396 | ||
|
|
b3ed3ce5c9 | ||
|
|
f93e753537 | ||
|
|
fa60c44d7d | ||
|
|
dd185318a0 | ||
|
|
8489ba3cfe | ||
|
|
655c404b43 | ||
|
|
219594eee7 | ||
|
|
de86eaff0d | ||
|
|
d16ec47dbc | ||
|
|
e8e700daa1 | ||
|
|
348f9518d5 | ||
|
|
4eaea7ac40 | ||
|
|
c2ea288d8f | ||
|
|
050c985ca3 | ||
|
|
11cea625cf | ||
|
|
dc18ad0530 | ||
|
|
7ace376c65 | ||
|
|
dc34eafd2c | ||
|
|
7e6cd4cc26 | ||
|
|
06ec423b3b | ||
|
|
9d7d91cc2d | ||
|
|
b3a50efe74 | ||
|
|
e4f1545840 | ||
|
|
012d4c26ca | ||
|
|
08b854fffb | ||
|
|
0712427491 | ||
|
|
66d7e27988 | ||
|
|
ce01d56bb9 | ||
|
|
67f286772d | ||
|
|
6cd1cd6775 | ||
|
|
2d7e47a878 | ||
|
|
1457e6bd23 | ||
|
|
02fa2afa21 | ||
|
|
bdccbb2b34 | ||
|
|
36472f2f77 | ||
|
|
3008896710 | ||
|
|
b11a2f737b | ||
|
|
b57810858b | ||
|
|
0b65195817 | ||
|
|
ee5553370d | ||
|
|
31322114a2 | ||
|
|
4e5ab00fe7 | ||
|
|
a582ba3f9b | ||
|
|
3983ba89ff | ||
|
|
5375ca4855 | ||
|
|
47a2ca07ed | ||
|
|
68bda70ac6 | ||
|
|
2bfeab9cb7 | ||
|
|
f142eb7528 | ||
|
|
ba72dd7cab | ||
|
|
d5d749207d | ||
|
|
3461778a78 | ||
|
|
5701ffafb3 | ||
|
|
1610c3a215 | ||
|
|
63946224c1 | ||
|
|
a4cd9562dc | ||
|
|
84468ebefb | ||
|
|
802fac17db | ||
|
|
20fcafe089 | ||
|
|
2ae216f41a | ||
|
|
eb10a8dd79 | ||
|
|
f16d8ef709 | ||
|
|
24de83d6dd | ||
|
|
3bdd71e0ce | ||
|
|
a59607380d | ||
|
|
49de4b6491 | ||
|
|
5541ce7711 | ||
|
|
7bce28ab5c | ||
|
|
6ebdad4069 | ||
|
|
0f75eb6f98 | ||
|
|
6f04191d38 | ||
|
|
21bc0cd93d | ||
|
|
0ad5d650a9 | ||
|
|
6b29dbf04e | ||
|
|
c9f06da301 | ||
|
|
4403b43b12 | ||
|
|
0c70ee2c08 | ||
|
|
f5b9d0977b | ||
|
|
e6d6b2704d | ||
|
|
75f0a9cb82 | ||
|
|
07a8763210 | ||
|
|
1804342652 | ||
|
|
c25c9b608e | ||
|
|
1abbe6fe04 | ||
|
|
f8ccd6e51a | ||
|
|
bb105927a4 | ||
|
|
70a71a2479 | ||
|
|
66b75e92b0 | ||
|
|
3da30ebd89 | ||
|
|
caef7eae4d | ||
|
|
74bc6c1ca7 | ||
|
|
6b0ea31c37 | ||
|
|
ece73f7da7 | ||
|
|
07ce83ebb5 | ||
|
|
7a3e58c043 | ||
|
|
4c9a5be242 | ||
|
|
b6ff594ae3 | ||
|
|
0be67c4883 | ||
|
|
7edd52346b | ||
|
|
390fb92508 | ||
|
|
f28dfd157c | ||
|
|
edd372b8d8 | ||
|
|
6acfb1eeac | ||
|
|
291a82352f | ||
|
|
a1d3571a06 | ||
|
|
1117279550 |
@@ -0,0 +1,24 @@
|
||||
Refs https://github.com/OWNER/REPOSITORY/issues/NUMBER
|
||||
|
||||
<!-- LJWX Health operations paths require a full cross-repository issue URL. -->
|
||||
|
||||
## Scope
|
||||
|
||||
-
|
||||
|
||||
## Validation Evidence
|
||||
|
||||
- Run / artifact URI:
|
||||
- Expected versus observed:
|
||||
|
||||
## Risk and Rollback
|
||||
|
||||
- Risk:
|
||||
- Rollback:
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Linked issue has an owner, scope, rollback, and acceptance/evidence contract
|
||||
- [ ] GitOps render and relevant policy gates pass
|
||||
- [ ] No plaintext Secret is committed
|
||||
- [ ] Emergency rollback exception, if used, has the controlled label and approval
|
||||
@@ -15,17 +15,17 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
|
||||
runs-on: arc-build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Build and push promoter image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: scripts/promoter/Dockerfile
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
name: deploy-controller-v2
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types:
|
||||
- deploy-resolution-v2
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_state_repo:
|
||||
description: "Release state repository"
|
||||
required: false
|
||||
default: "lingjingwanxiang/ljwx-release-state"
|
||||
type: string
|
||||
release_state_ref:
|
||||
description: "Release state branch"
|
||||
required: false
|
||||
default: "main"
|
||||
type: string
|
||||
env_allowlist:
|
||||
description: "Comma-separated environment allowlist"
|
||||
required: false
|
||||
default: "dev,demo,prod"
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: deploy-controller-v2-${{ github.event_name == 'repository_dispatch' && (github.event.client_payload.release_state_ref || 'main') || inputs.release_state_ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
apply:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Checkout release-state repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.release_state_repo || inputs.release_state_repo }}
|
||||
ref: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.release_state_ref || inputs.release_state_ref }}
|
||||
path: release-state
|
||||
token: ${{ secrets.RELEASE_STATE_REPO_TOKEN != '' && secrets.RELEASE_STATE_REPO_TOKEN || secrets.DEPLOY_REPO_TOKEN != '' && secrets.DEPLOY_REPO_TOKEN || github.token }}
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Login EU Harbor
|
||||
shell: bash
|
||||
env:
|
||||
HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||
HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${HARBOR_USERNAME}" || -z "${HARBOR_PASSWORD}" ]]; then
|
||||
echo "Skip EU Harbor login"
|
||||
exit 0
|
||||
fi
|
||||
printf '%s' "${HARBOR_PASSWORD}" | docker login harbor.eu.lingjingwanxiang.cn --username "${HARBOR_USERNAME}" --password-stdin
|
||||
|
||||
- name: Apply resolutions
|
||||
shell: bash
|
||||
env:
|
||||
ENV_ALLOWLIST: ${{ github.event_name == 'repository_dispatch' && (github.event.client_payload.env_allowlist || 'dev,demo,prod') || inputs.env_allowlist }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
uvx --with pyyaml python scripts/gitops_v2/apply_resolutions.py \
|
||||
--repo-root . \
|
||||
--state-root ./release-state \
|
||||
--env-allowlist "${ENV_ALLOWLIST}" \
|
||||
--result-out /tmp/deploy-results.json
|
||||
|
||||
- name: Copy prod images
|
||||
shell: bash
|
||||
env:
|
||||
HARBOR_PROD_USERNAME: ${{ secrets.HARBOR_PROD_USERNAME }}
|
||||
HARBOR_PROD_PASSWORD: ${{ secrets.HARBOR_PROD_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NEEDS_COPY="$(python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
payload = json.loads(Path("/tmp/deploy-results.json").read_text(encoding="utf-8"))
|
||||
needs_copy = False
|
||||
for item in payload.get("results", []):
|
||||
if item.get("env") != "prod":
|
||||
continue
|
||||
for fact in item.get("facts", []):
|
||||
if not isinstance(fact, dict) or fact.get("type") != "component":
|
||||
continue
|
||||
value = fact.get("value", {})
|
||||
if isinstance(value, dict) and value.get("copyRequired") is True:
|
||||
needs_copy = True
|
||||
print("true" if needs_copy else "false")
|
||||
PY
|
||||
)"
|
||||
if [[ "${NEEDS_COPY}" != "true" ]]; then
|
||||
echo "No prod registry copy required"
|
||||
exit 0
|
||||
fi
|
||||
if [[ -z "${HARBOR_PROD_USERNAME}" || -z "${HARBOR_PROD_PASSWORD}" ]]; then
|
||||
echo "缺少 HARBOR_PROD_USERNAME/HARBOR_PROD_PASSWORD,无法执行 prod controller 镜像复制" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s' "${HARBOR_PROD_PASSWORD}" | docker login harbor.omniverseai.net --username "${HARBOR_PROD_USERNAME}" --password-stdin
|
||||
python3 scripts/gitops_v2/copy_resolution_images.py --result-file /tmp/deploy-results.json
|
||||
|
||||
- name: Commit deploy changes
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git diff --quiet; then
|
||||
echo "No deploy overlay changes"
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "deploy-controller-v2[bot]"
|
||||
git config user.email "deploy-controller-v2[bot]@users.noreply.github.com"
|
||||
git add apps argocd-apps cluster cluster-prod release
|
||||
git commit -m "chore(deploy): apply release-state resolutions [skip ci]"
|
||||
git pull --rebase origin "${GITHUB_REF_NAME}"
|
||||
git push origin "HEAD:${GITHUB_REF_NAME}"
|
||||
|
||||
- name: Dispatch release results
|
||||
shell: bash
|
||||
env:
|
||||
RELEASE_STATE_REPO: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.release_state_repo || inputs.release_state_repo }}
|
||||
RELEASE_STATE_TOKEN: ${{ secrets.RELEASE_STATE_REPO_TOKEN != '' && secrets.RELEASE_STATE_REPO_TOKEN || secrets.DEPLOY_REPO_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RESULT_COUNT="$(python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
payload = json.loads(Path("/tmp/deploy-results.json").read_text(encoding="utf-8"))
|
||||
print(len(payload.get("results", [])))
|
||||
PY
|
||||
)"
|
||||
if [[ "${RESULT_COUNT}" == "0" ]]; then
|
||||
echo "No deploy results to dispatch"
|
||||
exit 0
|
||||
fi
|
||||
if [[ -z "${RELEASE_STATE_TOKEN}" ]]; then
|
||||
echo "缺少 RELEASE_STATE_REPO_TOKEN 或 DEPLOY_REPO_TOKEN,deploy result 无法回写到 release-state" >&2
|
||||
exit 1
|
||||
fi
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
owner, repo = os.environ["RELEASE_STATE_REPO"].split("/", 1)
|
||||
payload = json.loads(Path("/tmp/deploy-results.json").read_text(encoding="utf-8"))
|
||||
for item in payload.get("results", []):
|
||||
body = {
|
||||
"event_type": "deploy-result-v2",
|
||||
"client_payload": {
|
||||
"result": item,
|
||||
},
|
||||
}
|
||||
request = urllib.request.Request(
|
||||
f"https://api.github.com/repos/{owner}/{repo}/dispatches",
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
method="POST",
|
||||
headers={
|
||||
"Accept": "application/vnd.github+json",
|
||||
"Authorization": f"Bearer {os.environ['RELEASE_STATE_TOKEN']}",
|
||||
"Content-Type": "application/json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"User-Agent": "ljwx-deploy",
|
||||
},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request) as response:
|
||||
if response.status not in (200, 201, 204):
|
||||
raise SystemExit(f"result dispatch failed http={response.status}")
|
||||
except urllib.error.HTTPError as exc:
|
||||
body_text = exc.read().decode("utf-8", errors="ignore")
|
||||
raise SystemExit(f"result dispatch failed http={exc.code}: {body_text}")
|
||||
PY
|
||||
@@ -10,11 +10,11 @@ permissions:
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
runs-on: arc-ci
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -64,13 +64,27 @@ jobs:
|
||||
--changed-files .changed-files.txt \
|
||||
--event-path "${{ github.event_path }}"
|
||||
|
||||
- name: Enforce LJWX Health operations issue contract
|
||||
env:
|
||||
# DEPLOY_REPO_TOKEN is the existing cross-repository automation token.
|
||||
# Fork PRs do not receive it and therefore remain fail-closed.
|
||||
GITHUB_TOKEN: ${{ secrets.DEPLOY_REPO_TOKEN || github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 -m unittest discover -s scripts/tests -p 'test_ops_issue_link_gate.py' -v
|
||||
python3 scripts/ci/ops_issue_link_gate.py \
|
||||
--profile deploy \
|
||||
--repository "${GITHUB_REPOSITORY}" \
|
||||
--event-path "${GITHUB_EVENT_PATH}" \
|
||||
--changed-files .changed-files.txt
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Smart CS contract gate
|
||||
run: bash scripts/ci/check-smart-cs-contract.sh
|
||||
@@ -81,6 +95,37 @@ jobs:
|
||||
- name: Plaintext secret scan
|
||||
run: bash scripts/ci/check-no-plain-secrets.sh .changed-files.txt
|
||||
|
||||
- name: Validate registry pull secret source mapping
|
||||
run: python3 -m unittest scripts.tests.test_registry_pull_secret_sync -v
|
||||
|
||||
- name: Validate Cloudflare DNS token contract
|
||||
run: python3 -m unittest scripts.tests.test_cloudflare_dns_token_contract -v
|
||||
|
||||
- name: Validate LJWX Health alert acknowledgement relay
|
||||
run: |
|
||||
node --check apps/openclaw/base/alert-relay/server.mjs
|
||||
node --test apps/openclaw/base/alert-relay/server.test.mjs
|
||||
python3 -m py_compile \
|
||||
scripts/operations/ack-alert-delivery.py \
|
||||
scripts/operations/run-alert-delivery-drill.py
|
||||
kubectl kustomize apps/openclaw/overlays/openclaw-dev >/tmp/openclaw-dev.yaml
|
||||
|
||||
- name: Validate LJWX Health failure-domain scheduling
|
||||
run: |
|
||||
uvx --with pyyaml python -m unittest \
|
||||
scripts.tests.test_ljwx_health_failure_domain_scheduling -v
|
||||
uvx --with pyyaml python -m unittest \
|
||||
scripts.tests.test_ljwx_health_contabo_migration -v
|
||||
|
||||
- name: Validate LJWX Health schema rollout
|
||||
run: |
|
||||
uvx --with pyyaml python -m unittest scripts.tests.test_ljwx_health_release_promotion -v
|
||||
uvx --with pyyaml python -m unittest scripts.tests.test_ljwx_health_schema_restore_drill -v
|
||||
shellcheck apps/ljwx-health/schema-restore-drill/scripts/*.sh
|
||||
kubectl kustomize apps/ljwx-health/overlays/k3s-eu >/tmp/ljwx-health-k3s-eu.yaml
|
||||
kubectl kustomize apps/ljwx-health/overlays/contabo-prod >/tmp/ljwx-health-contabo-prod.yaml
|
||||
kubectl kustomize apps/ljwx-health/overlays/contabo-prod-staging >/tmp/ljwx-health-contabo-prod-staging.yaml
|
||||
|
||||
- name: Detect pgvector related changes
|
||||
id: pgvector
|
||||
run: |
|
||||
|
||||
@@ -48,23 +48,23 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
runs-on: arc-ci
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
KNOWLEDGE_DIFY_BASE_URL: ${{ secrets.KNOWLEDGE_DIFY_BASE_URL }}
|
||||
KNOWLEDGE_DIFY_API_KEY: ${{ secrets.KNOWLEDGE_DIFY_API_KEY }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Resolve inputs
|
||||
id: prep
|
||||
@@ -72,17 +72,21 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
|
||||
echo "target_env=dev" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_ref=${GITHUB_REF}" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_sha=${GITHUB_SHA}" >> "${GITHUB_OUTPUT}"
|
||||
echo "diff_from=${{ github.event.before }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "skip_dify=false" >> "${GITHUB_OUTPUT}"
|
||||
{
|
||||
echo "target_env=dev"
|
||||
echo "source_ref=${GITHUB_REF}"
|
||||
echo "source_sha=${GITHUB_SHA}"
|
||||
echo "diff_from=${{ github.event.before }}"
|
||||
echo "skip_dify=false"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "target_env=${{ inputs.target_env }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_ref=${{ inputs.source_ref }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_sha=${{ inputs.source_sha || github.sha }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "diff_from=${{ inputs.diff_from }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "skip_dify=${{ inputs.skip_dify }}" >> "${GITHUB_OUTPUT}"
|
||||
{
|
||||
echo "target_env=${{ inputs.target_env }}"
|
||||
echo "source_ref=${{ inputs.source_ref }}"
|
||||
echo "source_sha=${{ inputs.source_sha || github.sha }}"
|
||||
echo "diff_from=${{ inputs.diff_from }}"
|
||||
echo "skip_dify=${{ inputs.skip_dify }}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
- name: Validate staged candidate artifacts
|
||||
|
||||
@@ -36,22 +36,22 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: arc-ci
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
KNOWLEDGE_GITHUB_TOKEN: ${{ secrets.DEPLOY_REPO_TOKEN || github.token }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Resolve inputs
|
||||
id: prep
|
||||
@@ -59,17 +59,21 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "${GITHUB_EVENT_NAME}" = "repository_dispatch" ]; then
|
||||
echo "source_repo=${{ github.event.client_payload.source_repo }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_run_id=${{ github.event.client_payload.source_run_id }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "artifact_name=${{ github.event.client_payload.artifact_name }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "target_env=${{ github.event.client_payload.target_env || 'dev' }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_sha=${{ github.event.client_payload.source_sha || '' }}" >> "${GITHUB_OUTPUT}"
|
||||
{
|
||||
echo "source_repo=${{ github.event.client_payload.source_repo }}"
|
||||
echo "source_run_id=${{ github.event.client_payload.source_run_id }}"
|
||||
echo "artifact_name=${{ github.event.client_payload.artifact_name }}"
|
||||
echo "target_env=${{ github.event.client_payload.target_env || 'dev' }}"
|
||||
echo "source_sha=${{ github.event.client_payload.source_sha || '' }}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "source_repo=${{ inputs.source_repo }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_run_id=${{ inputs.source_run_id }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "artifact_name=${{ inputs.artifact_name }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "target_env=${{ inputs.target_env }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_sha=" >> "${GITHUB_OUTPUT}"
|
||||
{
|
||||
echo "source_repo=${{ inputs.source_repo }}"
|
||||
echo "source_run_id=${{ inputs.source_run_id }}"
|
||||
echo "artifact_name=${{ inputs.artifact_name }}"
|
||||
echo "target_env=${{ inputs.target_env }}"
|
||||
echo "source_sha="
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
- name: Stage candidate artifact
|
||||
|
||||
@@ -43,24 +43,24 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
ingest:
|
||||
runs-on: arc-ci
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
KNOWLEDGE_DIFY_BASE_URL: ${{ secrets.KNOWLEDGE_DIFY_BASE_URL }}
|
||||
KNOWLEDGE_DIFY_API_KEY: ${{ secrets.KNOWLEDGE_DIFY_API_KEY }}
|
||||
KNOWLEDGE_GITHUB_TOKEN: ${{ secrets.DEPLOY_REPO_TOKEN || github.token }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Resolve inputs
|
||||
id: prep
|
||||
@@ -68,19 +68,23 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "${GITHUB_EVENT_NAME}" = "repository_dispatch" ]; then
|
||||
echo "source_repo=${{ github.event.client_payload.source_repo }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_run_id=${{ github.event.client_payload.source_run_id }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "artifact_name=${{ github.event.client_payload.artifact_name }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "target_env=${{ github.event.client_payload.target_env || 'dev' }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "skip_dify=${{ github.event.client_payload.skip_dify || 'false' }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_sha=${{ github.event.client_payload.source_sha || '' }}" >> "${GITHUB_OUTPUT}"
|
||||
{
|
||||
echo "source_repo=${{ github.event.client_payload.source_repo }}"
|
||||
echo "source_run_id=${{ github.event.client_payload.source_run_id }}"
|
||||
echo "artifact_name=${{ github.event.client_payload.artifact_name }}"
|
||||
echo "target_env=${{ github.event.client_payload.target_env || 'dev' }}"
|
||||
echo "skip_dify=${{ github.event.client_payload.skip_dify || 'false' }}"
|
||||
echo "source_sha=${{ github.event.client_payload.source_sha || '' }}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "source_repo=${{ inputs.source_repo }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_run_id=${{ inputs.source_run_id }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "artifact_name=${{ inputs.artifact_name }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "target_env=${{ inputs.target_env }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "skip_dify=${{ inputs.skip_dify }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "source_sha=" >> "${GITHUB_OUTPUT}"
|
||||
{
|
||||
echo "source_repo=${{ inputs.source_repo }}"
|
||||
echo "source_run_id=${{ inputs.source_run_id }}"
|
||||
echo "artifact_name=${{ inputs.artifact_name }}"
|
||||
echo "target_env=${{ inputs.target_env }}"
|
||||
echo "skip_dify=${{ inputs.skip_dify }}"
|
||||
echo "source_sha="
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
- name: Ingest artifact
|
||||
|
||||
@@ -36,23 +36,23 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
reconcile:
|
||||
runs-on: arc-ci
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
KNOWLEDGE_DIFY_BASE_URL: ${{ secrets.KNOWLEDGE_DIFY_BASE_URL }}
|
||||
KNOWLEDGE_DIFY_API_KEY: ${{ secrets.KNOWLEDGE_DIFY_API_KEY }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Reconcile publish records
|
||||
run: |
|
||||
|
||||
@@ -41,23 +41,23 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
unpublish:
|
||||
runs-on: arc-ci
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
KNOWLEDGE_DIFY_BASE_URL: ${{ secrets.KNOWLEDGE_DIFY_BASE_URL }}
|
||||
KNOWLEDGE_DIFY_API_KEY: ${{ secrets.KNOWLEDGE_DIFY_API_KEY }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Unpublish record
|
||||
run: |
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
name: monthly-rollback-drill
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Monthly on day 1, 18:00 UTC
|
||||
- cron: "0 18 1 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
drill:
|
||||
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
|
||||
runs-on: arc-ci
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Run rollback drill (dry-run only)
|
||||
run: bash scripts/ops/run_rollback_drill.sh
|
||||
@@ -1,36 +0,0 @@
|
||||
name: nightly-auto-repair
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore: [gh-pages]
|
||||
paths-ignore: ['**']
|
||||
schedule:
|
||||
# 01:30 Asia/Tokyo (JST) = 16:30 UTC previous day
|
||||
- cron: "30 16 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
repair:
|
||||
runs-on: arc-ci
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Run auto-repair loop
|
||||
run: |
|
||||
bash scripts/repair/run.sh \
|
||||
--allow-main \
|
||||
--check-cmd "bash scripts/ci/run_checks.sh" \
|
||||
--max-attempts 3 \
|
||||
--recipes repairs/recipes.yaml \
|
||||
--issue-repo "${{ github.repository }}"
|
||||
@@ -1,108 +0,0 @@
|
||||
name: nightly-evidence
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore: [gh-pages]
|
||||
paths-ignore: ['**']
|
||||
schedule:
|
||||
# 02:00 Asia/Tokyo (JST) = 17:00 UTC previous day
|
||||
- cron: "0 17 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: nightly-evidence-publish
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-ci
|
||||
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Pgvector static health check
|
||||
run: bash scripts/ops/check-pgvector.sh
|
||||
|
||||
- name: Validate and collect evidence
|
||||
run: |
|
||||
uvx --with pyyaml --with jsonschema python scripts/evidence/validate.py
|
||||
uvx --with pyyaml --with jsonschema python scripts/evidence/collect.py --out evidence/index.json --summary evidence/summary/latest.md
|
||||
uvx --with pyyaml python scripts/promoter/queue_metrics.py --queue release/queue.yaml --out evidence/metrics/queue-health.json
|
||||
|
||||
- name: Build publish payload (atomic staging)
|
||||
run: |
|
||||
rm -rf .publish-tmp
|
||||
mkdir -p .publish-tmp/evidence/summary .publish-tmp/evidence/metrics
|
||||
cp pages/index.html pages/app.js .publish-tmp/
|
||||
cp evidence/index.json .publish-tmp/evidence/index.json
|
||||
cp evidence/summary/latest.md .publish-tmp/evidence/summary/latest.md
|
||||
cp evidence/metrics/queue-health.json .publish-tmp/evidence/metrics/queue-health.json
|
||||
python3 - <<'PY'
|
||||
import json, pathlib
|
||||
feed = pathlib.Path('.publish-tmp/evidence/index.json')
|
||||
data = json.loads(feed.read_text(encoding='utf-8'))
|
||||
if not isinstance(data, list):
|
||||
raise SystemExit('evidence/index.json must be an array')
|
||||
print(f'staged records: {len(data)}')
|
||||
PY
|
||||
|
||||
- name: Publish to gh-pages branch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf .gh-pages-wt
|
||||
if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then
|
||||
git worktree add --force -B gh-pages .gh-pages-wt origin/gh-pages
|
||||
else
|
||||
git worktree add --force -B gh-pages .gh-pages-wt
|
||||
fi
|
||||
|
||||
cd .gh-pages-wt
|
||||
find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +
|
||||
cp -R ../.publish-tmp/. .
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
index_path = Path("evidence/index.json")
|
||||
if not index_path.exists():
|
||||
raise SystemExit("missing evidence/index.json in gh-pages payload")
|
||||
data = json.loads(index_path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, list):
|
||||
raise SystemExit("evidence/index.json in gh-pages payload must be an array")
|
||||
print(f"publish records: {len(data)}")
|
||||
PY
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add -A
|
||||
extra_tracked="$(git ls-files | grep -Ev '^(index\.html|app\.js|evidence/index\.json|evidence/summary/latest\.md|evidence/metrics/queue-health\.json)$' || true)"
|
||||
if [ -n "${extra_tracked}" ]; then
|
||||
echo "Unexpected tracked files in gh-pages branch:"
|
||||
echo "${extra_tracked}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to publish"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "pages: update evidence feed [skip ci]"
|
||||
git push origin HEAD:gh-pages
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Ops Emergency Follow-up
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
create-followup:
|
||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'ops-emergency-rollback')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create follow-up when emergency PR has no issue link
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const tracked = /^\s*(Closes|Fixes|Refs)\s*:?\s+.*(#\d+|github\.com\/[^/]+\/[^/]+\/issues\/\d+)/im.test(pr.body || '');
|
||||
if (tracked) {
|
||||
core.notice(`Emergency PR #${pr.number} already links a follow-up issue.`);
|
||||
return;
|
||||
}
|
||||
const created = await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `[post-emergency] Audit rollback PR #${pr.number}`,
|
||||
body: `## Owner\n@${pr.user.login}\n\n## Scope\nAudit emergency rollback PR #${pr.number}.\n\n## Evidence\n- PR: ${pr.html_url}\n- Merged commit: ${pr.merge_commit_sha}\n\n## Acceptance\n- [ ] Confirm incident timeline and production impact\n- [ ] Attach validation evidence\n- [ ] Record permanent corrective action\n\n## Rollback\nDocument whether the emergency state remains active or was superseded.`,
|
||||
});
|
||||
core.notice(`Created auditable follow-up issue ${created.data.html_url}`);
|
||||
@@ -5,6 +5,7 @@ GitOps 配置仓库,使用 ArgoCD 管理 Kubernetes 集群。
|
||||
## Start Here
|
||||
|
||||
- 新进程/新项目/老项目统一接入入口:`docs/START-HERE-GITOPS-ONBOARDING.md`
|
||||
- LJWX 业务部署权威流程:`docs/ljwx-business-deployment-flow.md`
|
||||
- 自动扫描当前仓库是否已接入:`bash scripts/ops/scan-gitops-context.sh --repo "$PWD"`
|
||||
|
||||
## Platform Blueprint
|
||||
@@ -19,7 +20,8 @@ GitOps 配置仓库,使用 ArgoCD 管理 Kubernetes 集群。
|
||||
|
||||
- 运行验证:`bash scripts/verify.sh`
|
||||
- Harbor 复制:`docs/harbor-replication-ghcr.md`
|
||||
- Promoter/Argo/Smoke 运维:`docs/ops-runbook.md`
|
||||
- Controller/Argo 运维:`docs/ops-runbook.md`
|
||||
- GitOps v2 设计入口:`docs/gitops-v2/README.md`
|
||||
- 生产集群 Bootstrap(仅生产集群执行):`argocd-apps/02-cluster-prod-bootstrap.yaml`
|
||||
- 生产镜像拉取凭据自动同步:`cluster-prod/registry-pull-secret-sync-*.yaml`
|
||||
- 回滚演练:`docs/rollback-drill.md`
|
||||
@@ -41,14 +43,14 @@ GitOps 配置仓库,使用 ArgoCD 管理 Kubernetes 集群。
|
||||
- `platform/` - 平台装配、契约、路由、知识治理配置
|
||||
- `runbooks/` - 平台级操作手册
|
||||
|
||||
## 生产发布闭环(当前)
|
||||
## 生产发布闭环(当前口径)
|
||||
|
||||
1. 服务仓 CI push GHCR(不可变 digest)。
|
||||
2. 本地 Harbor 同步 GHCR;`deploy-promoter` 仅处理 `dev,demo`。
|
||||
3. smoke 通过后自动给本地 Harbor 打 `prod-*` 标签,并自动 enqueue `env=prod`。
|
||||
4. 生产 Harbor 仅同步 `prod-*` 标签。
|
||||
5. 生产集群 `deploy-promoter-prod` 等待生产 Harbor digest 就绪后更新 prod overlay。
|
||||
6. `registry-pull-secret-sync` 自动把 `default/harbor-registry` 下发到标记 namespace(`registry-sync.ljwx.io/enabled=true`),避免 prod Pod 拉镜像失败。
|
||||
1. 服务仓在 GitHub / Runner 构建,并把候选镜像 push 到 profile 指定的 candidate registry;当前标准优先 GHCR。
|
||||
2. 服务仓上传 `ReleaseBundle` artifact,并向 `ljwx-release-state` 发 `repository_dispatch`。
|
||||
3. `ljwx-release-state` 生成 `Event -> ReleaseBundle -> ReleaseRequest -> DeployResolution`。
|
||||
4. `deploy-controller-v2` 从 `ljwx-release-state` 拉取 resolution,并作为 `ljwx-deploy` 的唯一写入者更新 overlay。
|
||||
5. 如目标为 `prod`,controller 按 `release/services*.yaml` 的 `deployImage` 和 resolution 的 `copyRequired` 决定是否复制镜像;`deployImage` 为 GHCR 且 `copyRequired=false` 时不经过本地 Harbor。
|
||||
6. controller 把 `ReleaseResult` 回写到 `ljwx-release-state`;legacy promoter/smoke CronJob 已 suspend,不再作为默认发布入口。
|
||||
|
||||
## 组件
|
||||
|
||||
@@ -56,7 +58,7 @@ GitOps 配置仓库,使用 ArgoCD 管理 Kubernetes 集群。
|
||||
|
||||
- 支持 Gitea Actions 的弹性 runner
|
||||
- 自动伸缩: min 1, max 5 (基于 CPU/Memory)
|
||||
- 支持 Docker build/push
|
||||
- 适合轻量校验、部署编排和运维任务;不再作为标准重 CI 主力
|
||||
- Harbor registry: https://harbor.omniverseai.net
|
||||
|
||||
#### 配置说明
|
||||
|
||||
@@ -5,5 +5,5 @@ resources:
|
||||
- ../../base
|
||||
images:
|
||||
- name: harbor.eu.lingjingwanxiang.cn/ljwx/agent-platform-demo
|
||||
newName: harbor.eu.lingjingwanxiang.cn/ljwx/agent-platform-demo
|
||||
digest: sha256:27b0fc823384125e8d3fbe357a0fe3382cb405222a28708889227416aa318c68
|
||||
newName: ghcr.io/lingjingwanxiang/agent-platform-demo
|
||||
digest: sha256:4f007ee49dbc5c175eb734d6f81b647f37b591ffc56c737a25ee5c34fa931476
|
||||
|
||||
@@ -5,5 +5,5 @@ resources:
|
||||
- ../../base
|
||||
images:
|
||||
- name: ghcr.io/lingjingwanxiang/agent-platform-api
|
||||
newName: harbor.eu.lingjingwanxiang.cn/ljwx/agent-platform-api
|
||||
digest: sha256:90e6c9eb2c9babb7eaa95c9f387083a6e49de727abbc55d1c50580982f35f3d7
|
||||
newName: ghcr.io/lingjingwanxiang/agent-platform-api
|
||||
digest: sha256:bb9b65cc570b155cf9d2b24020ce8d2fa39400d36f19863fb84b0baa43c80ec2
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: cloudflare-letsencrypt-staging
|
||||
labels:
|
||||
app.kubernetes.io/part-of: cert-manager-cloudflare-dns01
|
||||
annotations:
|
||||
gitops.ljwx.io/credential-mode: account-api-token
|
||||
spec:
|
||||
acme:
|
||||
email: admin@omniverseai.net
|
||||
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
privateKeySecretRef:
|
||||
name: cloudflare-letsencrypt-staging-account-key
|
||||
solvers:
|
||||
- dns01:
|
||||
cloudflare:
|
||||
apiTokenSecretRef:
|
||||
name: cloudflare-api-token
|
||||
key: api-token
|
||||
selector:
|
||||
dnsZones:
|
||||
- omniverseai.net
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: cloudflare-letsencrypt
|
||||
labels:
|
||||
app.kubernetes.io/part-of: cert-manager-cloudflare-dns01
|
||||
annotations:
|
||||
gitops.ljwx.io/credential-mode: account-api-token
|
||||
spec:
|
||||
acme:
|
||||
email: admin@omniverseai.net
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
privateKeySecretRef:
|
||||
name: cloudflare-letsencrypt-account-key
|
||||
solvers:
|
||||
- dns01:
|
||||
cloudflare:
|
||||
apiTokenSecretRef:
|
||||
name: cloudflare-api-token
|
||||
key: api-token
|
||||
selector:
|
||||
dnsZones:
|
||||
- omniverseai.net
|
||||
@@ -3,7 +3,7 @@ kind: ClusterIssuer
|
||||
metadata:
|
||||
name: dnspod-letsencrypt-staging
|
||||
annotations:
|
||||
gitops.ljwx.io/issuer-implementation: http01-traefik
|
||||
gitops.ljwx.io/issuer-implementation: http01-nginx
|
||||
spec:
|
||||
acme:
|
||||
email: platform@lingjingwanxiang.cn
|
||||
@@ -13,4 +13,4 @@ spec:
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
ingressClassName: traefik
|
||||
ingressClassName: nginx
|
||||
|
||||
@@ -3,7 +3,7 @@ kind: ClusterIssuer
|
||||
metadata:
|
||||
name: dnspod-letsencrypt
|
||||
annotations:
|
||||
gitops.ljwx.io/issuer-implementation: http01-traefik
|
||||
gitops.ljwx.io/issuer-implementation: http01-nginx
|
||||
spec:
|
||||
acme:
|
||||
email: platform@lingjingwanxiang.cn
|
||||
@@ -13,4 +13,4 @@ spec:
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
ingressClassName: traefik
|
||||
ingressClassName: nginx
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
labels:
|
||||
app.kubernetes.io/part-of: cert-manager-cloudflare-dns01
|
||||
baseline.macos-k3/compatibility-alias: "true"
|
||||
annotations:
|
||||
gitops.ljwx.io/credential-mode: account-api-token
|
||||
spec:
|
||||
acme:
|
||||
email: admin@omniverseai.net
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod-account-key
|
||||
solvers:
|
||||
- dns01:
|
||||
cloudflare:
|
||||
apiTokenSecretRef:
|
||||
name: cloudflare-api-token
|
||||
key: api-token
|
||||
selector:
|
||||
dnsZones:
|
||||
- omniverseai.net
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-staging
|
||||
labels:
|
||||
app.kubernetes.io/part-of: cert-manager-cloudflare-dns01
|
||||
baseline.macos-k3/compatibility-alias: "true"
|
||||
annotations:
|
||||
gitops.ljwx.io/credential-mode: account-api-token
|
||||
spec:
|
||||
acme:
|
||||
email: admin@omniverseai.net
|
||||
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-staging-account-key
|
||||
solvers:
|
||||
- dns01:
|
||||
cloudflare:
|
||||
apiTokenSecretRef:
|
||||
name: cloudflare-api-token
|
||||
key: api-token
|
||||
selector:
|
||||
dnsZones:
|
||||
- omniverseai.net
|
||||
@@ -1,5 +1,9 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- clusterissuer-cloudflare-letsencrypt.yaml
|
||||
- clusterissuer-cloudflare-letsencrypt-staging.yaml
|
||||
- clusterissuer-letsencrypt-prod.yaml
|
||||
- clusterissuer-letsencrypt-staging.yaml
|
||||
- clusterissuer-dnspod-letsencrypt.yaml
|
||||
- clusterissuer-dnspod-letsencrypt-staging.yaml
|
||||
|
||||
@@ -5,8 +5,8 @@ resources:
|
||||
- ../../base
|
||||
images:
|
||||
- name: harbor.omniverseai.net/ljwx/ljwx-bookstore
|
||||
newName: ghcr.io/brunogaosz/ljwx-bookstore
|
||||
digest: sha256:87d09e8ff42a834699e61b7ddfdcc4d19184a42e1653e9d1d7ca2c36eb70ee05
|
||||
newName: ghcr.io/lingjingwanxiang/ljwx-bookstore
|
||||
digest: sha256:4e74338de6dbbe574d1050250e435aa47c2760ccb32bfb214ae8a95f40e57a1f
|
||||
replicas:
|
||||
- name: ljwx-bookstore
|
||||
count: 1
|
||||
|
||||
@@ -14,7 +14,7 @@ data:
|
||||
NEXT_AUTH_SSO_SESSION_STRATEGY: "jwt"
|
||||
NEXT_AUTH_SSO_PROVIDERS: ""
|
||||
AUTH_TRUST_HOST: "true"
|
||||
DEFAULT_AGENT_CONFIG: 'model=lingjingwanxiang:32b;provider=ollama;systemRole="默认使用中文回答,除非用户明确要求使用其他语言。"'
|
||||
DEFAULT_AGENT_CONFIG: 'model=local-rag-default;provider=openai;systemRole="默认使用中文回答,除非用户明确要求使用其他语言。"'
|
||||
|
||||
EMAIL_ENABLED: "false"
|
||||
|
||||
@@ -26,6 +26,10 @@ data:
|
||||
|
||||
MIDDLEWARE_REWRITE_THROUGH_LOCAL: "1"
|
||||
|
||||
ENABLED_OPENAI: "1"
|
||||
OPENAI_API_KEY: "local"
|
||||
OPENAI_PROXY_URL: "http://mlx-qwen:18088/v1"
|
||||
OPENAI_MODEL_LIST: "-all,+local-rag-default,+local-rag-quality,+local-review"
|
||||
ENABLED_OLLAMA: "0"
|
||||
OPENCLAW_GATEWAY_URL: "https://openclaw.lingjingwanxiang.cn/v1"
|
||||
OPENCLAW_GATEWAY_ORIGIN: "https://openclaw.lingjingwanxiang.cn"
|
||||
|
||||
@@ -3,21 +3,20 @@ kind: Ingress
|
||||
metadata:
|
||||
name: ljwx-chat
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
cert-manager.io/cluster-issuer: dnspod-letsencrypt
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
|
||||
nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- chat.lingjingwanxiang.cn
|
||||
secretName: chat-lingjingwanxiang-cn-tls
|
||||
- chat.omniverseai.net
|
||||
secretName: chat-omniverseai-net-tls
|
||||
rules:
|
||||
- host: chat.lingjingwanxiang.cn
|
||||
- host: chat.omniverseai.net
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
|
||||
@@ -11,5 +11,5 @@ patchesStrategicMerge:
|
||||
- deployment-routing-patch.yaml
|
||||
images:
|
||||
- name: ghcr.io/brunogao/ljwx-chat
|
||||
newName: ghcr.io/brunogao/ljwx-chat
|
||||
newTag: sha-976fea8
|
||||
newName: ghcr.io/lingjingwanxiang/ljwx-chat
|
||||
digest: sha256:f1acba4b77166a3241941da1b90e6c1309fd7e2a3c8acc8370732940bfe2965b
|
||||
|
||||
@@ -5,5 +5,5 @@ resources:
|
||||
- ../../base
|
||||
images:
|
||||
- name: ghcr.io/brunogao/ljwx-core-api
|
||||
newName: ghcr.io/brunogao/ljwx-core-api
|
||||
digest: sha256:b7a9760defea2b10eabf468b20342ef6775185e147cc384b37fe7dc331b0c5cc
|
||||
newName: ghcr.io/lingjingwanxiang/ljwx-core-api
|
||||
digest: sha256:299a6f33744356c0ec6ee32da60a482e261f2c53e45e880706812ae45ed7ca6c
|
||||
|
||||
@@ -21,6 +21,8 @@ spec:
|
||||
app: ljwx-dify-api
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
ljwx.omniverseai.net/config-revision: local-llm-alias-20260517
|
||||
labels:
|
||||
app: ljwx-dify-api
|
||||
spec:
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ljwx-dify-brand-assets
|
||||
data:
|
||||
lingjing-icon.svg: |
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="lingjing-icon-gradient" x1="8" y1="7" x2="40" y2="41" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#667EEA"/>
|
||||
<stop offset="1" stop-color="#06B6D4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="4" width="40" height="40" rx="10" fill="#111827"/>
|
||||
<path d="M24 8L36 15V29L24 36L12 29V15L24 8Z" fill="url(#lingjing-icon-gradient)"/>
|
||||
<path d="M24 14V30M17.5 18H30.5M17.5 24H30.5" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="32" cy="14" r="2.5" fill="#22D3EE"/>
|
||||
</svg>
|
||||
lingjing-logo-light.svg: |
|
||||
<svg width="174" height="32" viewBox="0 0 174 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="lingjing-logo-light-gradient" x1="3" y1="3" x2="29" y2="29" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#667EEA"/>
|
||||
<stop offset="1" stop-color="#06B6D4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="1" y="1" width="30" height="30" rx="8" fill="#111827"/>
|
||||
<path d="M16 5L25 10.5V21.5L16 27L7 21.5V10.5L16 5Z" fill="url(#lingjing-logo-light-gradient)"/>
|
||||
<path d="M16 10V22M11 13.5H21M11 18.5H21" stroke="white" stroke-width="1.6" stroke-linecap="round"/>
|
||||
<circle cx="22.5" cy="9.5" r="1.8" fill="#22D3EE"/>
|
||||
<text x="42" y="22" fill="#111827" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif" font-size="18" font-weight="700">灵境万象</text>
|
||||
</svg>
|
||||
lingjing-logo-dark.svg: |
|
||||
<svg width="174" height="32" viewBox="0 0 174 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="lingjing-logo-dark-gradient" x1="3" y1="3" x2="29" y2="29" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8B9BFF"/>
|
||||
<stop offset="1" stop-color="#22D3EE"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="1" y="1" width="30" height="30" rx="8" fill="#1F2937"/>
|
||||
<path d="M16 5L25 10.5V21.5L16 27L7 21.5V10.5L16 5Z" fill="url(#lingjing-logo-dark-gradient)"/>
|
||||
<path d="M16 10V22M11 13.5H21M11 18.5H21" stroke="white" stroke-width="1.6" stroke-linecap="round"/>
|
||||
<circle cx="22.5" cy="9.5" r="1.8" fill="#67E8F9"/>
|
||||
<text x="42" y="22" fill="#F9FAFB" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif" font-size="18" font-weight="700">灵境万象</text>
|
||||
</svg>
|
||||
@@ -38,5 +38,12 @@ data:
|
||||
DIFY_CONSOLE_API_BASE: "http://api:5001/console/api"
|
||||
DIFY_INIT_WAIT_SECONDS: "300"
|
||||
DIFY_INIT_REQUEST_TIMEOUT_SECONDS: "15"
|
||||
DIFY_DEFAULT_LLM_PROVIDER: "anthropic"
|
||||
DIFY_DEFAULT_LLM_MODEL: "claude-sonnet-4-6"
|
||||
DIFY_DEFAULT_LLM_PROVIDER: "openai_api_compatible"
|
||||
DIFY_DEFAULT_LLM_MODEL: "local-rag-default"
|
||||
DIFY_DEFAULT_LLM_CONTEXT_SIZE: "32768"
|
||||
DEFAULT_MODEL_PROVIDER: "openai_api_compatible"
|
||||
DEFAULT_MODEL_NAME: "local-rag-default"
|
||||
APP_DEFAULT_LLM_PROVIDER: "openai_api_compatible"
|
||||
APP_DEFAULT_LLM_MODEL: "local-rag-default"
|
||||
OPENAI_API_BASE: "http://mlx-qwen:18088/v1"
|
||||
OPENAI_API_KEY: "local"
|
||||
|
||||
@@ -3,19 +3,17 @@ kind: Ingress
|
||||
metadata:
|
||||
name: ljwx-dify
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: dnspod-letsencrypt
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
cert-manager.io/cluster-issuer: cloudflare-letsencrypt
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
|
||||
nginx.ingress.kubernetes.io/websocket-services: "api"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- dify.lingjingwanxiang.cn
|
||||
secretName: dify-lingjingwanxiang-cn-tls
|
||||
- dify.omniverseai.net
|
||||
secretName: dify-omniverseai-net-tls
|
||||
rules:
|
||||
- host: dify.lingjingwanxiang.cn
|
||||
- host: dify.omniverseai.net
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
|
||||
@@ -56,8 +56,11 @@ spec:
|
||||
|
||||
PROVIDER_HINT = os.getenv("DIFY_DEFAULT_LLM_PROVIDER", "anthropic").strip() or "anthropic"
|
||||
DEFAULT_MODEL = os.getenv("DIFY_DEFAULT_LLM_MODEL", "claude-sonnet-4-6").strip()
|
||||
DEFAULT_CONTEXT_SIZE = os.getenv("DIFY_DEFAULT_LLM_CONTEXT_SIZE", "32768").strip() or "32768"
|
||||
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "").strip()
|
||||
ANTHROPIC_BASE_URL = os.getenv("ANTHROPIC_BASE_URL", "https://api.anthropic.com").strip()
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "local").strip() or "local"
|
||||
OPENAI_API_BASE = os.getenv("OPENAI_API_BASE", "").strip().rstrip("/")
|
||||
|
||||
DB_HOST = os.getenv("DB_HOST", "").strip()
|
||||
DB_PORT = int(os.getenv("DB_PORT", "5432"))
|
||||
@@ -77,8 +80,10 @@ spec:
|
||||
fail("缺少 DIFY_ADMIN_PASSWORD,无法登录 Dify 控制台。")
|
||||
if not DEFAULT_MODEL:
|
||||
fail("缺少 DIFY_DEFAULT_LLM_MODEL,无法设置默认模型。")
|
||||
if not ANTHROPIC_API_KEY:
|
||||
if "anthropic" in PROVIDER_HINT.lower() and not ANTHROPIC_API_KEY:
|
||||
fail("缺少 ANTHROPIC_API_KEY,无法配置 Anthropic Provider。")
|
||||
if "openai_api_compatible" in PROVIDER_HINT.lower() and not OPENAI_API_BASE:
|
||||
fail("缺少 OPENAI_API_BASE,无法配置 OpenAI-compatible Provider。")
|
||||
|
||||
cookie_jar = CookieJar()
|
||||
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie_jar))
|
||||
@@ -451,6 +456,68 @@ spec:
|
||||
fail("Anthropic provider 在超时时间内仍不可用。")
|
||||
|
||||
|
||||
def ensure_openai_api_compatible_plugin_ready() -> None:
|
||||
plugin_id = "langgenius/openai_api_compatible"
|
||||
existing_providers, provider_error = try_list_llm_providers()
|
||||
if existing_providers is not None and find_provider(existing_providers, PROVIDER_HINT) is not None:
|
||||
print("[INFO] OpenAI-compatible provider 已存在,跳过插件安装检查。")
|
||||
return
|
||||
if provider_error:
|
||||
print(f"[WARN] LLM provider 列表暂不可用,继续执行插件探测: {provider_error}")
|
||||
|
||||
_, latest_body, _ = request_json(
|
||||
"POST",
|
||||
"/workspaces/current/plugin/list/latest-versions",
|
||||
payload={"plugin_ids": [plugin_id]},
|
||||
expected_status={200},
|
||||
)
|
||||
latest_versions = latest_body.get("versions", {}) if isinstance(latest_body, dict) else {}
|
||||
latest_info = latest_versions.get(plugin_id) if isinstance(latest_versions, dict) else None
|
||||
if not isinstance(latest_info, dict):
|
||||
fail("无法获取 OpenAI-compatible 插件最新版本信息。")
|
||||
|
||||
latest_unique = str(latest_info.get("unique_identifier", "")).strip()
|
||||
if not latest_unique:
|
||||
fail("OpenAI-compatible 插件最新 unique_identifier 为空。")
|
||||
|
||||
print(f"[INFO] 安装或确认 OpenAI-compatible 插件: {latest_unique}")
|
||||
install_status, install_body, install_text = request_json(
|
||||
"POST",
|
||||
"/workspaces/current/plugin/install/marketplace",
|
||||
payload={"plugin_unique_identifiers": [latest_unique]},
|
||||
expected_status={200, 400, 409, 500},
|
||||
)
|
||||
task_id = install_body.get("task_id") if isinstance(install_body, dict) else None
|
||||
all_installed = bool(install_body.get("all_installed")) if isinstance(install_body, dict) else False
|
||||
if install_status == 200 and task_id:
|
||||
wait_plugin_task(str(task_id))
|
||||
elif install_status == 200 and all_installed:
|
||||
print("[INFO] OpenAI-compatible 插件已经安装。")
|
||||
elif install_status == 200:
|
||||
print("[WARN] 安装 OpenAI-compatible 插件返回成功但没有 task_id,继续等待 provider 就绪。")
|
||||
elif is_ignorable_plugin_response(install_status, install_body, install_text):
|
||||
print("[WARN] OpenAI-compatible 插件已存在或处于可忽略状态,继续等待 provider 就绪。")
|
||||
else:
|
||||
fail(
|
||||
f"安装 OpenAI-compatible 插件失败,status={install_status},"
|
||||
f"{_error_text(install_body, install_text)}"
|
||||
)
|
||||
|
||||
deadline = time.time() + WAIT_SECONDS
|
||||
while time.time() < deadline:
|
||||
providers, provider_error = try_list_llm_providers()
|
||||
if providers is not None and find_provider(providers, PROVIDER_HINT) is not None:
|
||||
print("[INFO] OpenAI-compatible provider 已可用。")
|
||||
return
|
||||
if provider_error:
|
||||
print(f"[INFO] 等待 OpenAI-compatible provider 注册到模型列表... {provider_error}")
|
||||
else:
|
||||
print("[INFO] 等待 OpenAI-compatible provider 注册到模型列表...")
|
||||
time.sleep(3)
|
||||
|
||||
fail("OpenAI-compatible provider 在超时时间内仍不可用。")
|
||||
|
||||
|
||||
def build_provider_credentials(provider_item: dict[str, Any]) -> dict[str, str]:
|
||||
schema = provider_item.get("provider_credential_schema") or {}
|
||||
forms = schema.get("credential_form_schemas") if isinstance(schema, dict) else None
|
||||
@@ -695,6 +762,214 @@ spec:
|
||||
print("[WARN] 设置 provider preferred_provider_type 返回 400,已忽略。")
|
||||
|
||||
|
||||
def build_openai_api_compatible_model_credentials() -> dict[str, str]:
|
||||
return {
|
||||
"display_name": DEFAULT_MODEL,
|
||||
"api_key": OPENAI_API_KEY,
|
||||
"endpoint_url": OPENAI_API_BASE,
|
||||
"endpoint_model_name": DEFAULT_MODEL,
|
||||
"mode": "chat",
|
||||
"context_size": DEFAULT_CONTEXT_SIZE,
|
||||
}
|
||||
|
||||
|
||||
def upsert_custom_model_via_db(
|
||||
provider_name: str,
|
||||
model_name: str,
|
||||
credentials: dict[str, str],
|
||||
) -> None:
|
||||
try:
|
||||
import psycopg2
|
||||
except Exception as exc: # noqa: BLE001
|
||||
fail(f"未安装 psycopg2,无法执行数据库回退写入 custom model: {exc}")
|
||||
|
||||
required = {
|
||||
"DB_HOST": DB_HOST,
|
||||
"DB_DATABASE": DB_DATABASE,
|
||||
"DB_USERNAME": DB_USERNAME,
|
||||
"DB_PASSWORD": DB_PASSWORD,
|
||||
}
|
||||
missing = [k for k, v in required.items() if not v]
|
||||
if missing:
|
||||
fail(f"数据库回退写入 custom model 缺少配置: {missing}")
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
dbname=DB_DATABASE,
|
||||
user=DB_USERNAME,
|
||||
password=DB_PASSWORD,
|
||||
connect_timeout=REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
fail(f"连接数据库失败,无法回退写入 custom model: {exc}")
|
||||
|
||||
try:
|
||||
with conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
select taj.tenant_id
|
||||
from tenant_account_joins taj
|
||||
join accounts a on a.id = taj.account_id
|
||||
where lower(a.email) = lower(%s)
|
||||
order by taj.created_at asc
|
||||
limit 1
|
||||
""",
|
||||
(ADMIN_EMAIL,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row or not row[0]:
|
||||
fail(f"未找到管理员账号 {ADMIN_EMAIL} 关联的 tenant_id。")
|
||||
tenant_id = str(row[0])
|
||||
|
||||
credential_id = str(uuid.uuid4())
|
||||
provider_model_id = str(uuid.uuid4())
|
||||
encrypted_config = json.dumps(credentials, ensure_ascii=False)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
delete from provider_model_credentials
|
||||
where tenant_id = %s
|
||||
and provider_name = %s
|
||||
and model_name = %s
|
||||
and model_type = 'text-generation'
|
||||
""",
|
||||
(tenant_id, provider_name, model_name),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
delete from provider_models
|
||||
where tenant_id = %s
|
||||
and provider_name = %s
|
||||
and model_name = %s
|
||||
and model_type = 'text-generation'
|
||||
""",
|
||||
(tenant_id, provider_name, model_name),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
insert into provider_model_credentials
|
||||
(id, tenant_id, provider_name, model_name, model_type,
|
||||
credential_name, encrypted_config, created_at, updated_at)
|
||||
values (%s, %s, %s, %s, 'text-generation',
|
||||
%s, %s, now(), now())
|
||||
""",
|
||||
(
|
||||
credential_id,
|
||||
tenant_id,
|
||||
provider_name,
|
||||
model_name,
|
||||
model_name,
|
||||
encrypted_config,
|
||||
),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
insert into provider_models
|
||||
(id, tenant_id, provider_name, model_name, model_type,
|
||||
credential_id, is_valid, created_at, updated_at)
|
||||
values (%s, %s, %s, %s, 'text-generation',
|
||||
%s, true, now(), now())
|
||||
""",
|
||||
(
|
||||
provider_model_id,
|
||||
tenant_id,
|
||||
provider_name,
|
||||
model_name,
|
||||
credential_id,
|
||||
),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
delete from tenant_default_models
|
||||
where tenant_id = %s and model_type in ('text-generation', 'llm')
|
||||
""",
|
||||
(tenant_id,),
|
||||
)
|
||||
for default_model_type in ("text-generation", "llm"):
|
||||
cur.execute(
|
||||
"""
|
||||
insert into tenant_default_models
|
||||
(id, tenant_id, provider_name, model_name, model_type,
|
||||
created_at, updated_at)
|
||||
values (%s, %s, %s, %s, %s, now(), now())
|
||||
""",
|
||||
(
|
||||
str(uuid.uuid4()),
|
||||
tenant_id,
|
||||
provider_name,
|
||||
model_name,
|
||||
default_model_type,
|
||||
),
|
||||
)
|
||||
print("[WARN] custom model API 校验失败,已通过数据库回退写入 MLX 模型。")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
fail(f"数据库回退写入 custom model 失败: {exc}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def ensure_custom_model_credentials(
|
||||
provider_name: str,
|
||||
model_name: str,
|
||||
credentials: dict[str, str],
|
||||
) -> None:
|
||||
quoted_provider = quote_provider_path(provider_name)
|
||||
get_status, get_body, _ = request_json(
|
||||
"GET",
|
||||
f"/workspaces/current/model-providers/{quoted_provider}/models/credentials"
|
||||
f"?model={urllib.parse.quote(model_name)}&model_type=llm&config_from=custom-model",
|
||||
expected_status={200, 400, 500},
|
||||
)
|
||||
current_credential_id = None
|
||||
if get_status == 200 and isinstance(get_body, dict):
|
||||
current_credential_id = get_body.get("current_credential_id")
|
||||
|
||||
if current_credential_id:
|
||||
status, body, text = request_json(
|
||||
"PUT",
|
||||
f"/workspaces/current/model-providers/{quoted_provider}/models/credentials",
|
||||
payload={
|
||||
"credential_id": current_credential_id,
|
||||
"model": model_name,
|
||||
"model_type": "llm",
|
||||
"credentials": credentials,
|
||||
"name": model_name,
|
||||
},
|
||||
expected_status={200, 400, 500},
|
||||
)
|
||||
if status == 200:
|
||||
print("[INFO] 已更新 OpenAI-compatible custom model 凭据。")
|
||||
return
|
||||
print(
|
||||
f"[WARN] custom model 凭据更新校验失败,status={status},"
|
||||
f"{_error_text(body, text)}"
|
||||
)
|
||||
else:
|
||||
status, body, text = request_json(
|
||||
"POST",
|
||||
f"/workspaces/current/model-providers/{quoted_provider}/models/credentials",
|
||||
payload={
|
||||
"model": model_name,
|
||||
"model_type": "llm",
|
||||
"credentials": credentials,
|
||||
"name": model_name,
|
||||
},
|
||||
expected_status={201, 400, 500},
|
||||
)
|
||||
if status == 201:
|
||||
print("[INFO] 已创建 OpenAI-compatible custom model 凭据。")
|
||||
return
|
||||
print(
|
||||
f"[WARN] custom model 凭据创建校验失败,status={status},"
|
||||
f"{_error_text(body, text)}"
|
||||
)
|
||||
|
||||
upsert_custom_model_via_db(provider_name, model_name, credentials)
|
||||
|
||||
|
||||
def set_default_model(provider_name: str, model_name: str) -> None:
|
||||
quoted_provider = quote_provider_path(provider_name)
|
||||
_, model_body, model_text = request_json(
|
||||
@@ -766,15 +1041,27 @@ spec:
|
||||
ensure_setup()
|
||||
login_console()
|
||||
|
||||
if "anthropic" in PROVIDER_HINT.lower():
|
||||
provider_hint_lower = PROVIDER_HINT.lower()
|
||||
if "anthropic" in provider_hint_lower:
|
||||
ensure_anthropic_plugin_ready()
|
||||
elif "openai_api_compatible" in provider_hint_lower:
|
||||
ensure_openai_api_compatible_plugin_ready()
|
||||
|
||||
providers = list_llm_providers()
|
||||
provider_item = pick_provider(providers, PROVIDER_HINT)
|
||||
selected_provider_name = str(provider_item.get("provider", PROVIDER_HINT))
|
||||
provider_credentials = build_provider_credentials(provider_item)
|
||||
|
||||
ensure_provider_credentials(selected_provider_name, provider_credentials)
|
||||
if "openai_api_compatible" in provider_hint_lower:
|
||||
custom_model_credentials = build_openai_api_compatible_model_credentials()
|
||||
ensure_custom_model_credentials(
|
||||
selected_provider_name,
|
||||
DEFAULT_MODEL,
|
||||
custom_model_credentials,
|
||||
)
|
||||
else:
|
||||
provider_credentials = build_provider_credentials(provider_item)
|
||||
ensure_provider_credentials(selected_provider_name, provider_credentials)
|
||||
|
||||
set_default_model(selected_provider_name, DEFAULT_MODEL)
|
||||
print("[INFO] Dify 默认 LLM 自动化配置完成。")
|
||||
PY
|
||||
|
||||
@@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- brand-assets.yaml
|
||||
- configmap.yaml
|
||||
- job-upgrade-db.yaml
|
||||
- job-configure-default-llm.yaml
|
||||
|
||||
@@ -26,15 +26,68 @@ spec:
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
initContainers:
|
||||
- name: migrate-plugin-packages
|
||||
image: docker.io/langgenius/dify-plugin-daemon:0.5.2-local
|
||||
command:
|
||||
- /bin/sh
|
||||
- -lc
|
||||
- |
|
||||
set -eu
|
||||
python3 - <<'PY'
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
root = Path("/app/storage/plugin")
|
||||
package_root = Path("/app/storage/plugin_packages")
|
||||
package_root.mkdir(parents=True, exist_ok=True)
|
||||
skip_dirs = {".venv", "__pycache__", ".git", ".pytest_cache"}
|
||||
migrated = 0
|
||||
for publisher_dir in root.iterdir() if root.exists() else []:
|
||||
if not publisher_dir.is_dir():
|
||||
continue
|
||||
for source in publisher_dir.iterdir():
|
||||
if not source.is_dir() or "-" not in source.name or "@" not in source.name:
|
||||
continue
|
||||
plugin_name, version_hash = source.name.rsplit("-", 1)
|
||||
unique_name = f"{plugin_name}:{version_hash}"
|
||||
targets = [
|
||||
publisher_dir / unique_name,
|
||||
package_root / publisher_dir.name / unique_name,
|
||||
]
|
||||
for target in targets:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
for path in source.rglob("*"):
|
||||
relative = path.relative_to(source)
|
||||
if path.is_dir() or any(part in skip_dirs for part in relative.parts):
|
||||
continue
|
||||
archive.write(path, relative.as_posix())
|
||||
migrated += 1
|
||||
print(f"migrated legacy plugin directories: {migrated}")
|
||||
PY
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /app/storage/plugin
|
||||
- name: plugin-packages
|
||||
mountPath: /app/storage/plugin_packages
|
||||
containers:
|
||||
- name: plugin-daemon
|
||||
image: langgenius/dify-plugin-daemon:0.5.1-local
|
||||
image: docker.io/langgenius/dify-plugin-daemon:0.5.2-local
|
||||
ports:
|
||||
- containerPort: 5002
|
||||
name: http
|
||||
env:
|
||||
- name: PLUGIN_DAEMON_PORT
|
||||
value: "5002"
|
||||
- name: SERVER_PORT
|
||||
value: "5002"
|
||||
- name: MAX_PLUGIN_PACKAGE_SIZE
|
||||
value: "52428800"
|
||||
- name: PYTHON_ENV_INIT_TIMEOUT
|
||||
value: "120"
|
||||
- name: PLUGIN_MAX_EXECUTION_TIMEOUT
|
||||
value: "600"
|
||||
- name: REDIS_HOST
|
||||
value: "redis-lb.infra.svc.cluster.local"
|
||||
- name: REDIS_PORT
|
||||
@@ -77,11 +130,27 @@ spec:
|
||||
name: ljwx-dify-secrets
|
||||
key: DIFY_INNER_API_KEY
|
||||
- name: PLUGIN_REMOTE_INSTALLING_HOST
|
||||
value: "https://dify.lingjingwanxiang.cn"
|
||||
value: "0.0.0.0"
|
||||
- name: PLUGIN_REMOTE_INSTALLING_PORT
|
||||
value: "443"
|
||||
value: "5003"
|
||||
- name: PLUGIN_STORAGE_TYPE
|
||||
value: "local"
|
||||
- name: PLUGIN_STORAGE_LOCAL_ROOT
|
||||
value: "/app/storage"
|
||||
- name: PLUGIN_WORKING_PATH
|
||||
value: "/app/plugins"
|
||||
value: "/app/storage/cwd"
|
||||
- name: PLUGIN_INSTALLED_PATH
|
||||
value: "plugin"
|
||||
- name: PLUGIN_PACKAGE_CACHE_PATH
|
||||
value: "plugin_packages"
|
||||
- name: PLUGIN_MEDIA_CACHE_PATH
|
||||
value: "assets"
|
||||
- name: UV_HTTP_TIMEOUT
|
||||
value: "300"
|
||||
- name: UV_DEFAULT_INDEX
|
||||
value: "https://mirrors.ustc.edu.cn/pypi/simple"
|
||||
- name: UV_INDEX_URL
|
||||
value: "https://mirrors.ustc.edu.cn/pypi/simple"
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ljwx-dify-config
|
||||
@@ -90,6 +159,14 @@ spec:
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /app/plugins
|
||||
- name: plugins
|
||||
mountPath: /app/plugin
|
||||
- name: plugins
|
||||
mountPath: /app/storage/plugin
|
||||
- name: plugin-cwd
|
||||
mountPath: /app/storage/cwd
|
||||
- name: plugin-packages
|
||||
mountPath: /app/storage/plugin_packages
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
@@ -109,4 +186,9 @@ spec:
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: plugins
|
||||
persistentVolumeClaim:
|
||||
claimName: ljwx-dify-plugins
|
||||
- name: plugin-cwd
|
||||
emptyDir: {}
|
||||
- name: plugin-packages
|
||||
emptyDir: {}
|
||||
|
||||
@@ -8,3 +8,14 @@ spec:
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: ljwx-dify-plugins
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
|
||||
@@ -36,6 +36,19 @@ spec:
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ljwx-dify-config
|
||||
volumeMounts:
|
||||
- name: brand-assets
|
||||
mountPath: /app/web/public/logo/lingjing-icon.svg
|
||||
subPath: lingjing-icon.svg
|
||||
readOnly: true
|
||||
- name: brand-assets
|
||||
mountPath: /app/web/public/logo/lingjing-logo-light.svg
|
||||
subPath: lingjing-logo-light.svg
|
||||
readOnly: true
|
||||
- name: brand-assets
|
||||
mountPath: /app/web/public/logo/lingjing-logo-dark.svg
|
||||
subPath: lingjing-logo-dark.svg
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
@@ -55,3 +68,7 @@ spec:
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: brand-assets
|
||||
configMap:
|
||||
name: ljwx-dify-brand-assets
|
||||
|
||||
@@ -9,6 +9,8 @@ spec:
|
||||
app: ljwx-dify-worker
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
ljwx.omniverseai.net/config-revision: local-llm-alias-20260517
|
||||
labels:
|
||||
app: ljwx-dify-worker
|
||||
spec:
|
||||
|
||||
@@ -3,12 +3,13 @@ kind: Kustomization
|
||||
namespace: ljwx-dify-dev
|
||||
resources:
|
||||
- ../../base
|
||||
- mlx-qwen.yaml
|
||||
images:
|
||||
- name: ghcr.io/brunogao/ljwx-dify-api
|
||||
newName: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-dify-api
|
||||
digest: sha256:508adb624b9199170e622120c5c5afae6a387a21e55ff45e3fb0eb26f3b89811
|
||||
newName: ghcr.io/lingjingwanxiang/ljwx-dify-api
|
||||
digest: sha256:0c4c4cf61fb29dc2dcae8a53478f23f260838cc0a9e61b860ef8ecee56215154
|
||||
- name: ghcr.io/brunogao/ljwx-dify-web
|
||||
newName: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-dify-web
|
||||
digest: sha256:15cd5c698296c22d8536dca5206a6b67d5f556f824db3e2208b581ed095378d2
|
||||
newName: ghcr.io/lingjingwanxiang/ljwx-dify-web
|
||||
digest: sha256:c5634c14f3c485c8ef22f03f51564c29d3558bc505b798f29585138c5b189b94
|
||||
- name: qdrant/qdrant
|
||||
newTag: v1.12.6
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mlx-qwen
|
||||
labels:
|
||||
app.kubernetes.io/name: mlx-qwen
|
||||
app.kubernetes.io/part-of: ljwx-chat-suite
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: 18088
|
||||
targetPort: 18088
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: discovery.k8s.io/v1
|
||||
kind: EndpointSlice
|
||||
metadata:
|
||||
name: mlx-qwen
|
||||
labels:
|
||||
kubernetes.io/service-name: mlx-qwen
|
||||
app.kubernetes.io/name: mlx-qwen
|
||||
app.kubernetes.io/part-of: ljwx-chat-suite
|
||||
addressType: IPv4
|
||||
ports:
|
||||
- name: http
|
||||
port: 18088
|
||||
protocol: TCP
|
||||
endpoints:
|
||||
- addresses:
|
||||
- 192.168.1.83
|
||||
conditions:
|
||||
ready: true
|
||||
@@ -18,11 +18,11 @@
|
||||
|
||||
## Public endpoint
|
||||
|
||||
- ingress profile: `traefik-letsencrypt-public`
|
||||
- host: `https://dify.lingjingwanxiang.cn`
|
||||
- ingress class: `traefik`
|
||||
- cluster issuer: `dnspod-letsencrypt`
|
||||
- tls secret: `dify-lingjingwanxiang-cn-tls`
|
||||
- ingress profile: `nginx-cloudflare-public`
|
||||
- host: `https://dify.omniverseai.net`
|
||||
- ingress class: `nginx`
|
||||
- cluster issuer: `cloudflare-letsencrypt`
|
||||
- tls secret: `dify-omniverseai-net-tls`
|
||||
- ingress artifact generation: `disabled (managed by existing manifests)`
|
||||
|
||||
## Bootstrap commands
|
||||
|
||||
@@ -2,12 +2,12 @@ service: ljwx-dify
|
||||
environment: dev
|
||||
namespace: ljwx-dify-dev
|
||||
cluster_bootstrap: true
|
||||
ingress_profile: traefik-letsencrypt-public
|
||||
public_host: dify.lingjingwanxiang.cn
|
||||
ingress_profile: nginx-cloudflare-public
|
||||
public_host: dify.omniverseai.net
|
||||
generate_ingress: false
|
||||
ingress_class_name: traefik
|
||||
cluster_issuer: dnspod-letsencrypt
|
||||
tls_secret_name: dify-lingjingwanxiang-cn-tls
|
||||
ingress_class_name: nginx
|
||||
cluster_issuer: cloudflare-letsencrypt
|
||||
tls_secret_name: dify-omniverseai-net-tls
|
||||
namespace_profile: dev-default
|
||||
registry_profile: ghcr-and-harbor
|
||||
runtime_secret_profile: app-runtime
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: alertmanager-monitoring-kube-prometheus-alertmanager
|
||||
labels:
|
||||
app.kubernetes.io/name: alertmanager
|
||||
app.kubernetes.io/component: configuration
|
||||
type: Opaque
|
||||
stringData:
|
||||
alertmanager.yaml: |
|
||||
global:
|
||||
resolve_timeout: 5m
|
||||
|
||||
inhibit_rules:
|
||||
- source_matchers:
|
||||
- severity="critical"
|
||||
target_matchers:
|
||||
- severity="warning"
|
||||
equal:
|
||||
- alertname
|
||||
- cluster
|
||||
- service
|
||||
|
||||
receivers:
|
||||
- name: "null"
|
||||
- name: critical-notifications
|
||||
webhook_configs:
|
||||
- url: http://ljwx-health-alert-relay.openclaw.svc.cluster.local:8080/alerts
|
||||
send_resolved: true
|
||||
max_alerts: 50
|
||||
- name: warning-notifications
|
||||
webhook_configs:
|
||||
- url: http://ljwx-health-alert-relay.openclaw.svc.cluster.local:8080/alerts
|
||||
send_resolved: true
|
||||
max_alerts: 50
|
||||
|
||||
route:
|
||||
receiver: "null"
|
||||
group_by:
|
||||
- alertname
|
||||
- cluster
|
||||
- service
|
||||
- severity
|
||||
group_wait: 30s
|
||||
group_interval: 5m
|
||||
repeat_interval: 12h
|
||||
routes:
|
||||
- matchers:
|
||||
- alertname="Watchdog"
|
||||
receiver: "null"
|
||||
- matchers:
|
||||
- alertname="InfoInhibitor"
|
||||
receiver: "null"
|
||||
- matchers:
|
||||
- alertname=~"KubeSchedulerDown|KubeProxyDown|KubeControllerManagerDown"
|
||||
receiver: "null"
|
||||
- matchers:
|
||||
- severity="critical"
|
||||
receiver: critical-notifications
|
||||
group_wait: 10s
|
||||
group_interval: 30s
|
||||
repeat_interval: 1h
|
||||
- matchers:
|
||||
- severity="warning"
|
||||
receiver: warning-notifications
|
||||
group_interval: 30s
|
||||
repeat_interval: 4h
|
||||
|
||||
templates:
|
||||
- /etc/alertmanager/config/*.tmpl
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: observability
|
||||
resources:
|
||||
- alertmanager-config.yaml
|
||||
- prometheusrule.yaml
|
||||
labels:
|
||||
- includeSelectors: false
|
||||
includeTemplates: true
|
||||
pairs:
|
||||
app.kubernetes.io/part-of: ljwx-health-operations
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
@@ -0,0 +1,65 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: ljwx-health-operations
|
||||
labels:
|
||||
release: monitoring
|
||||
spec:
|
||||
groups:
|
||||
- name: ljwx_health_notification_delivery
|
||||
interval: 30s
|
||||
rules:
|
||||
- alert: LjwxHealthAlertRelayUnavailable
|
||||
expr: kube_deployment_status_replicas_available{namespace="openclaw",deployment="ljwx-health-alert-relay"} < 1
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
category: notifications
|
||||
annotations:
|
||||
summary: "LJWX Health alert relay is unavailable"
|
||||
description: "No alert relay replica has been available for two minutes; Feishu notifications cannot be delivered."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/alert-delivery.md"
|
||||
|
||||
- alert: LjwxHealthAlertRelayDeliveryFailure
|
||||
expr: sum(increase(ljwx_alert_relay_deliveries_total{result="failure"}[15m])) > 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
category: notifications
|
||||
annotations:
|
||||
summary: "LJWX Health alert delivery failed"
|
||||
description: "At least one Feishu delivery attempt failed in the last 15 minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/alert-delivery.md"
|
||||
|
||||
- alert: LjwxHealthAlertRelayMetricsMissing
|
||||
expr: absent(up{namespace="openclaw",service="ljwx-health-alert-relay"})
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: observability
|
||||
annotations:
|
||||
summary: "LJWX Health alert relay metrics are missing"
|
||||
description: "Prometheus has not discovered alert relay metrics for five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/alert-delivery.md"
|
||||
|
||||
- alert: LjwxHealthAlertRelayEscalationUnconfigured
|
||||
expr: ljwx_alert_relay_escalation_configured == 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: notifications
|
||||
annotations:
|
||||
summary: "LJWX Health alert escalation target is not configured"
|
||||
description: "The primary receiver works, but SLA-expired unread alerts cannot reach the backup on-call target."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/alert-delivery.md"
|
||||
|
||||
- alert: LjwxHealthAlertRelayEscalationFailure
|
||||
expr: sum(increase(ljwx_alert_relay_escalations_total{result="failure"}[15m])) > 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
category: notifications
|
||||
annotations:
|
||||
summary: "LJWX Health alert escalation failed"
|
||||
description: "At least one unread alert exceeded its acknowledgement SLA but could not be delivered to the backup on-call target."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/alert-delivery.md"
|
||||
@@ -0,0 +1,64 @@
|
||||
# LJWX Health Secret Management
|
||||
|
||||
`ljwx-health-secret` is a runtime-only Kubernetes Secret. Values must never be committed to this repository, printed in CI, or copied into issue evidence.
|
||||
|
||||
## MinIO credentials
|
||||
|
||||
`OSS_ENDPOINT` targets `minio.infra.svc.cluster.local:9000`, so the runtime
|
||||
credential must match `infra/minio-credentials`; placeholder defaults are not
|
||||
valid production configuration. Synchronize and verify the two keys before a
|
||||
rollout or after MinIO credential rotation:
|
||||
|
||||
```bash
|
||||
apps/ljwx-health/base/sync-minio-credentials.sh
|
||||
```
|
||||
|
||||
The script copies the base64 Secret fields without decoding or printing them,
|
||||
merge-patches only `OSS_ACCESS_KEY` and `OSS_SECRET_KEY`, verifies exact parity,
|
||||
and removes `kubectl.kubernetes.io/last-applied-configuration` from runtime
|
||||
Secrets because that annotation contains plaintext-equivalent values. It does
|
||||
not restart workloads; advance the pod-template revision through GitOps, wait
|
||||
for both Boot replicas, then exercise an authenticated object write/read.
|
||||
|
||||
`create-secret.sh` also reads the same in-cluster source when accessible and no
|
||||
explicit OSS environment values are supplied. It no longer supplies a
|
||||
`minioadmin` fallback.
|
||||
|
||||
## Restricted PostgreSQL runtime role
|
||||
|
||||
`ljwx-boot` connects as `ljwx_app` and reads `DB_APP_PASSWORD`. Schema hooks remain isolated on `DB_MIGRATION_USER=postgres` with the existing `DB_PASSWORD`; do not point runtime traffic at that administrator credential.
|
||||
|
||||
Provision or rotate the application password from a protected operator shell before the GitOps cutover:
|
||||
|
||||
```bash
|
||||
apps/ljwx-health/base/provision-db-app-password.sh
|
||||
```
|
||||
|
||||
The script generates a value when `DB_APP_PASSWORD` is absent, sends the role DDL over stdin, verifies TCP authentication plus `NOSUPERUSER/NOBYPASSRLS`, and merges only the new Secret key. It never prints the password. Rotation does not restart workloads.
|
||||
|
||||
Cutover order: provision the key, reconcile the GitOps RLS policy bundle, run a restricted-role canary, merge the runtime identity change, wait for both Boot replicas, then verify `pg_stat_activity` contains `ljwx_app` and no Boot-owned `postgres` JDBC sessions. Roll back by reverting `DB_USER`/`SPRING_DATASOURCE_USERNAME` to `postgres`, both Deployment password selectors to `DB_PASSWORD`, and advancing the pod-template revision. Policy reconciliation is forward-compatible with either role and does not need to be undone.
|
||||
|
||||
## Sa-Token JWT signing key
|
||||
|
||||
The `ljwx-boot` Deployment reads key `SA_TOKEN_JWT_SECRET` as environment variable `SA_TOKEN_JWT_SECRET`. The application requires at least 32 characters and fails closed in the `k8s` profile when the key is absent.
|
||||
|
||||
Rotate only this key from a protected operator shell:
|
||||
|
||||
```bash
|
||||
apps/ljwx-health/base/rotate-sa-token-jwt-secret.sh
|
||||
```
|
||||
|
||||
The script generates 32 random bytes, patches only the named key, withholds the value from output, and preserves every other key in the Secret. A caller may supply a value through the process environment, but must avoid shell history and CI output.
|
||||
|
||||
Rotation alone does not restart workloads. Merge the GitOps manifest and application changes, allow Argo CD to roll out `ljwx-boot`, then verify both replicas. Every token or session signed with the old key becomes invalid; Admin, Phone, Watch proxy, and Bigscreen users must sign in again.
|
||||
|
||||
Use the application repository's redaction-safe guard for post-rollout verification:
|
||||
|
||||
```bash
|
||||
kubectl logs -n ljwx-health deployment/ljwx-boot --all-pods=true --since=30m \
|
||||
| python3 scripts/testops/auth_secret_guard.py logs --input -
|
||||
```
|
||||
|
||||
`ljwx-health-config` must keep `LOGGING_LEVEL_COM_LJWX_STARTER_SATOKEN_LOG=OFF`. Sa-Token's SLF4J adapter serializes the global configuration independently of the application-level `isLog` switch, so removing this logger policy reopens the startup disclosure path. The Deployment pod-template annotation deliberately changes when this policy changes so ConfigMap updates trigger a rolling restart.
|
||||
|
||||
Evidence may include only revision, image digest, readiness, smoke-test status, guard status, operator, and timestamps.
|
||||
@@ -0,0 +1,53 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
policyTypes: ["Ingress", "Egress"]
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: observability
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: prometheus
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 2000
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: infra
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: ingress-nginx
|
||||
app.kubernetes.io/component: controller
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 7844
|
||||
- protocol: UDP
|
||||
port: 7844
|
||||
@@ -0,0 +1,178 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared-config
|
||||
labels:
|
||||
app.kubernetes.io/name: ljwx-health-cloudflared
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
app.kubernetes.io/component: edge
|
||||
data:
|
||||
config.yaml: |
|
||||
tunnel: 70479f3e-caaf-4528-b713-413cc27e8ad0
|
||||
credentials-file: /etc/cloudflared/credentials/credentials.json
|
||||
metrics: 0.0.0.0:2000
|
||||
loglevel: info
|
||||
ingress:
|
||||
- hostname: health-admin.omniverseai.net
|
||||
service: http://ingress-nginx-controller.infra.svc.cluster.local:80
|
||||
originRequest:
|
||||
httpHostHeader: health-admin.omniverseai.net
|
||||
connectTimeout: 10s
|
||||
- hostname: health-dashboard.omniverseai.net
|
||||
service: http://ingress-nginx-controller.infra.svc.cluster.local:80
|
||||
originRequest:
|
||||
httpHostHeader: health-dashboard.omniverseai.net
|
||||
connectTimeout: 10s
|
||||
- service: http_status:404
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared
|
||||
labels:
|
||||
app.kubernetes.io/name: ljwx-health-cloudflared
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
app.kubernetes.io/component: edge
|
||||
spec:
|
||||
replicas: 2
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: ljwx-health-cloudflared
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
app.kubernetes.io/component: edge
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
topologyKey: topology.kubernetes.io/zone
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
- weight: 50
|
||||
podAffinityTerm:
|
||||
topologyKey: kubernetes.io/hostname
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
containers:
|
||||
- name: cloudflared
|
||||
image: cloudflare/cloudflared@sha256:6d91c121b803126f7a5344005d17a9324788fc09d305b6e2560ec6040a7ae283
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- tunnel
|
||||
- --no-autoupdate
|
||||
- --config
|
||||
- /etc/cloudflared/config/config.yaml
|
||||
- run
|
||||
ports:
|
||||
- name: metrics
|
||||
containerPort: 2000
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: metrics
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: metrics
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: metrics
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: tunnel-config
|
||||
mountPath: /etc/cloudflared/config
|
||||
readOnly: true
|
||||
- name: tunnel-credentials
|
||||
mountPath: /etc/cloudflared/credentials
|
||||
readOnly: true
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: tunnel-config
|
||||
configMap:
|
||||
name: ljwx-health-cloudflared-config
|
||||
- name: tunnel-credentials
|
||||
secret:
|
||||
secretName: ljwx-health-cloudflared-credentials
|
||||
items:
|
||||
- key: credentials.json
|
||||
path: credentials.json
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared
|
||||
labels:
|
||||
app.kubernetes.io/name: ljwx-health-cloudflared
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
app.kubernetes.io/component: edge
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
ports:
|
||||
- name: metrics
|
||||
port: 2000
|
||||
targetPort: metrics
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
---
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared
|
||||
labels:
|
||||
release: monitoring
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: ljwx-health-cloudflared
|
||||
endpoints:
|
||||
- port: metrics
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
@@ -13,17 +13,28 @@ metadata:
|
||||
data:
|
||||
# Spring 配置
|
||||
SPRING_PROFILES_ACTIVE: "k8s"
|
||||
# Sa-Token does not provide Spring Security's Actuator authorization context;
|
||||
# when_authorized therefore exposed dependency details. Hide them explicitly.
|
||||
MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS: "never"
|
||||
MANAGEMENT_ENDPOINT_HEALTH_SHOW_COMPONENTS: "never"
|
||||
|
||||
# 数据库配置(连接 infra namespace)
|
||||
DB_HOST: "mysql-lb.infra.svc.cluster.local"
|
||||
DB_PORT: "3306"
|
||||
# 数据库配置(连接 infra namespace - PostgreSQL)
|
||||
DB_HOST: "postgres-lb.infra.svc.cluster.local"
|
||||
DB_PORT: "5432"
|
||||
DB_NAME: "ljwx_health"
|
||||
DB_USER: "root"
|
||||
# Runtime traffic must cross PostgreSQL RLS. Schema hooks keep a separate
|
||||
# administrator identity and never inherit this application role.
|
||||
DB_USER: "ljwx_app"
|
||||
DB_MIGRATION_USER: "postgres"
|
||||
|
||||
# Spring DataSource 配置
|
||||
SPRING_DATASOURCE_URL: "jdbc:mysql://mysql-lb.infra.svc.cluster.local:3306/ljwx_health?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"
|
||||
SPRING_DATASOURCE_USERNAME: "root"
|
||||
SPRING_DATASOURCE_DRIVER_CLASS_NAME: "com.mysql.cj.jdbc.Driver"
|
||||
# #383 IA-09 staged authorization versions. The PreSync hook creates and
|
||||
# verifies all PostgreSQL facts before this flag rolls the backend forward.
|
||||
AUTHORIZATION_VERSION_ENABLED: "true"
|
||||
|
||||
# Spring DataSource 配置(PostgreSQL)
|
||||
SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres-lb.infra.svc.cluster.local:5432/ljwx_health?currentSchema=public&stringtype=unspecified"
|
||||
SPRING_DATASOURCE_USERNAME: "ljwx_app"
|
||||
SPRING_DATASOURCE_DRIVER_CLASS_NAME: "org.postgresql.Driver"
|
||||
|
||||
# Redis 配置(连接 infra namespace)
|
||||
REDIS_HOST: "redis-lb.infra.svc.cluster.local"
|
||||
@@ -37,6 +48,9 @@ data:
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL: "info"
|
||||
# Sa-Token adapter serializes the global config (including jwtSecretKey)
|
||||
# independently of SaTokenConfig.isLog. Suppress that logger package.
|
||||
LOGGING_LEVEL_COM_LJWX_STARTER_SATOKEN_LOG: "OFF"
|
||||
|
||||
# OSS 配置
|
||||
OSS_BUCKET_NAME: "ljwx-health"
|
||||
@@ -51,14 +65,30 @@ data:
|
||||
LJWX_LICENSE_ENABLED: "false"
|
||||
LJWX_LICENSE_TRIAL_MODE: "false"
|
||||
|
||||
# OpenTelemetry: traces go through OTLP/HTTP. Metrics and logs already have
|
||||
# dedicated Prometheus and Loki pipelines, so do not duplicate them via OTel.
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector-opentelemetry-collector.tracing.svc.cluster.local:4318"
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf"
|
||||
OTEL_TRACES_EXPORTER: "otlp"
|
||||
OTEL_METRICS_EXPORTER: "none"
|
||||
OTEL_LOGS_EXPORTER: "none"
|
||||
OTEL_RESOURCE_ATTRIBUTES: "deployment.environment.name=production,service.namespace=ljwx-health"
|
||||
|
||||
# 时区配置
|
||||
TZ: "Asia/Shanghai"
|
||||
|
||||
# Flyway 数据库迁移配置
|
||||
# ✅ 启用 Flyway - 对已有生产库使用统一基线 V3.0.0
|
||||
SPRING_FLYWAY_ENABLED: "true"
|
||||
SPRING_FLYWAY_BASELINE_ON_MIGRATE: "true" # 已有数据库直接标记到统一基线
|
||||
SPRING_FLYWAY_BASELINE_VERSION: "3.0.0"
|
||||
SPRING_FLYWAY_LOCATIONS: "classpath:db/baseline"
|
||||
SPRING_FLYWAY_VALIDATE_ON_MIGRATE: "true"
|
||||
SPRING_FLYWAY_CLEAN_DISABLED: "true" # 生产环境禁用 clean
|
||||
# Flyway 数据库迁移配置(PostgreSQL)
|
||||
# Runtime migrations are disabled for K3s; schema is managed by infra/DB
|
||||
# rollout tasks so application startup is not coupled to Flyway history drift.
|
||||
SPRING_FLYWAY_ENABLED: "false"
|
||||
SPRING_FLYWAY_BASELINE_ON_MIGRATE: "false"
|
||||
SPRING_FLYWAY_LOCATIONS: "classpath:db/migration-pg"
|
||||
SPRING_FLYWAY_VALIDATE_ON_MIGRATE: "false"
|
||||
SPRING_FLYWAY_CLEAN_DISABLED: "true"
|
||||
|
||||
# #349 governed OLAP shadow. Defaults are deliberately fail closed; the
|
||||
# production canary overlay owns the one-tenant activation.
|
||||
LJWX_HEALTH_OUTBOX_KAFKA_ENABLED: "false"
|
||||
LJWX_HEALTH_OUTBOX_KAFKA_ALLOWED_CUSTOMER_IDS: ""
|
||||
LJWX_HEALTH_OUTBOX_KAFKA_ALLOW_ALL_TENANTS: "false"
|
||||
LJWX_HEALTH_OUTBOX_KAFKA_BOOTSTRAP_SERVERS: "kafka-cluster-kafka-bootstrap.shared-base.svc.cluster.local:9092"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# Usage: ./create-secret.sh
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
@@ -16,6 +16,8 @@ NC='\033[0m' # No Color
|
||||
# 默认值
|
||||
NAMESPACE="ljwx-health"
|
||||
SECRET_NAME="ljwx-health-secret"
|
||||
MINIO_SOURCE_NAMESPACE="${MINIO_SOURCE_NAMESPACE:-infra}"
|
||||
MINIO_SOURCE_SECRET="${MINIO_SOURCE_SECRET:-minio-credentials}"
|
||||
|
||||
# 从环境变量或提示用户输入
|
||||
read_secret() {
|
||||
@@ -23,13 +25,13 @@ read_secret() {
|
||||
local prompt=$2
|
||||
local default_value=$3
|
||||
|
||||
if [ -n "${!var_name}" ]; then
|
||||
if [ -n "${!var_name:-}" ]; then
|
||||
echo "${!var_name}"
|
||||
elif [ -n "$default_value" ]; then
|
||||
read -p "$prompt [$default_value]: " value
|
||||
read -r -p "$prompt [$default_value]: " value
|
||||
echo "${value:-$default_value}"
|
||||
else
|
||||
read -p "$prompt: " value
|
||||
read -r -p "$prompt: " value
|
||||
echo "$value"
|
||||
fi
|
||||
}
|
||||
@@ -37,8 +39,13 @@ read_secret() {
|
||||
echo -e "${GREEN}=== LJWX Health Secret 创建工具 ===${NC}\n"
|
||||
|
||||
# 数据库配置
|
||||
echo -e "${YELLOW}数据库配置(连接 infra namespace MySQL)${NC}"
|
||||
DB_PASSWORD=$(read_secret "DB_PASSWORD" "数据库密码" "123456")
|
||||
echo -e "${YELLOW}数据库配置(连接 infra namespace PostgreSQL)${NC}"
|
||||
DB_PASSWORD=$(read_secret "DB_PASSWORD" "Schema 管理账号密码" "123456")
|
||||
DB_APP_PASSWORD=$(read_secret "DB_APP_PASSWORD" "ljwx_app 运行时账号密码" "")
|
||||
if [ -z "$DB_APP_PASSWORD" ]; then
|
||||
echo -e "${RED}DB_APP_PASSWORD 不得为空;请先为 ljwx_app 配置独立密码${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Redis 配置
|
||||
echo -e "\n${YELLOW}Redis 配置(连接 infra namespace Redis)${NC}"
|
||||
@@ -46,25 +53,59 @@ REDIS_PASSWORD=$(read_secret "REDIS_PASSWORD" "Redis 密码" "123456")
|
||||
|
||||
# OSS 配置
|
||||
echo -e "\n${YELLOW}OSS 配置(MinIO)${NC}"
|
||||
OSS_ACCESS_KEY=$(read_secret "OSS_ACCESS_KEY" "OSS Access Key" "minioadmin")
|
||||
OSS_SECRET_KEY=$(read_secret "OSS_SECRET_KEY" "OSS Secret Key" "minioadmin")
|
||||
if [ -z "${OSS_ACCESS_KEY:-}" ] && [ -z "${OSS_SECRET_KEY:-}" ] \
|
||||
&& kubectl -n "$MINIO_SOURCE_NAMESPACE" get secret "$MINIO_SOURCE_SECRET" >/dev/null 2>&1; then
|
||||
OSS_ACCESS_KEY="$(kubectl -n "$MINIO_SOURCE_NAMESPACE" get secret "$MINIO_SOURCE_SECRET" \
|
||||
-o jsonpath='{.data.root-user}' | base64 -d)"
|
||||
OSS_SECRET_KEY="$(kubectl -n "$MINIO_SOURCE_NAMESPACE" get secret "$MINIO_SOURCE_SECRET" \
|
||||
-o jsonpath='{.data.root-password}' | base64 -d)"
|
||||
echo -e "${GREEN}已从 ${MINIO_SOURCE_NAMESPACE}/${MINIO_SOURCE_SECRET} 读取凭据(值不显示)${NC}"
|
||||
else
|
||||
OSS_ACCESS_KEY=$(read_secret "OSS_ACCESS_KEY" "OSS Access Key" "")
|
||||
OSS_SECRET_KEY=$(read_secret "OSS_SECRET_KEY" "OSS Secret Key" "")
|
||||
fi
|
||||
if [ -z "$OSS_ACCESS_KEY" ] || [ -z "$OSS_SECRET_KEY" ]; then
|
||||
echo -e "${RED}OSS_ACCESS_KEY 与 OSS_SECRET_KEY 不得为空${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "\n${YELLOW}应用安全配置${NC}"
|
||||
SECRET_KEY=$(read_secret "SECRET_KEY" "Flask SECRET_KEY(留空则自动生成)" "")
|
||||
if [ -z "$SECRET_KEY" ]; then
|
||||
SECRET_KEY="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||||
echo -e "${YELLOW}未提供 SECRET_KEY,已自动生成随机值${NC}"
|
||||
fi
|
||||
|
||||
SA_TOKEN_JWT_SECRET=$(read_secret "SA_TOKEN_JWT_SECRET" "Sa-Token JWT Secret(留空则自动生成)" "")
|
||||
if [ -z "$SA_TOKEN_JWT_SECRET" ]; then
|
||||
SA_TOKEN_JWT_SECRET="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||||
echo -e "${YELLOW}未提供 SA_TOKEN_JWT_SECRET,已自动生成随机值${NC}"
|
||||
fi
|
||||
if [ "${#SA_TOKEN_JWT_SECRET}" -lt 32 ]; then
|
||||
echo -e "${RED}SA_TOKEN_JWT_SECRET 至少需要 32 个字符${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建 namespace(如果不存在)
|
||||
echo -e "\n${GREEN}创建 namespace: $NAMESPACE${NC}"
|
||||
kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# 删除旧 Secret(如果存在)
|
||||
echo -e "${GREEN}删除旧 Secret(如果存在)${NC}"
|
||||
kubectl delete secret $SECRET_NAME -n $NAMESPACE --ignore-not-found=true
|
||||
|
||||
# 创建新 Secret
|
||||
echo -e "${GREEN}创建 Secret: $SECRET_NAME${NC}"
|
||||
# 声明式创建或更新 Secret,避免先删除造成不可用窗口。
|
||||
echo -e "${GREEN}创建或更新 Secret: $SECRET_NAME${NC}"
|
||||
kubectl create secret generic $SECRET_NAME \
|
||||
-n $NAMESPACE \
|
||||
--from-literal=DB_PASSWORD="$DB_PASSWORD" \
|
||||
--from-literal=DB_APP_PASSWORD="$DB_APP_PASSWORD" \
|
||||
--from-literal=REDIS_PASSWORD="$REDIS_PASSWORD" \
|
||||
--from-literal=OSS_ACCESS_KEY="$OSS_ACCESS_KEY" \
|
||||
--from-literal=OSS_SECRET_KEY="$OSS_SECRET_KEY"
|
||||
--from-literal=OSS_SECRET_KEY="$OSS_SECRET_KEY" \
|
||||
--from-literal=SECRET_KEY="$SECRET_KEY" \
|
||||
--from-literal=SA_TOKEN_JWT_SECRET="$SA_TOKEN_JWT_SECRET" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# Remove the apply annotation because it contains base64-encoded Secret values.
|
||||
kubectl -n "$NAMESPACE" annotate secret "$SECRET_NAME" \
|
||||
kubectl.kubernetes.io/last-applied-configuration- >/dev/null 2>&1 || true
|
||||
|
||||
# 验证
|
||||
echo -e "\n${GREEN}验证 Secret:${NC}"
|
||||
@@ -73,8 +114,10 @@ kubectl get secret $SECRET_NAME -n $NAMESPACE
|
||||
echo -e "\n${GREEN}✅ Secret 创建成功!${NC}"
|
||||
echo -e "\n${YELLOW}提示:${NC}"
|
||||
echo " 1. Secret 包含敏感信息,请妥善保管"
|
||||
echo " 2. 数据库连接地址: mysql-infra.infra.svc.cluster.local:3306"
|
||||
echo " 2. 数据库连接地址: postgres-lb.infra.svc.cluster.local:5432"
|
||||
echo " 3. Redis 连接地址: redis-infra.infra.svc.cluster.local:6379"
|
||||
echo " 4. Argo CD 应用将通过 GitOps 自动部署"
|
||||
echo " 5. 访问地址: http://health-admin.omniverseai.net (HTTP)"
|
||||
echo " 6. 大屏地址: http://health-dashboard.omniverseai.net (HTTP)"
|
||||
echo " 7. SECRET_KEY 已写入 Secret,可直接供 bigscreen 生产模式使用"
|
||||
echo " 8. SA_TOKEN_JWT_SECRET 已写入 Secret;轮换后所有既有登录态失效"
|
||||
|
||||
@@ -13,7 +13,6 @@ metadata:
|
||||
component: frontend
|
||||
version: v1.0.0
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
@@ -22,6 +21,8 @@ spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ljwx-admin
|
||||
app.kubernetes.io/name: ljwx-health
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
component: frontend
|
||||
template:
|
||||
metadata:
|
||||
@@ -32,10 +33,26 @@ spec:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/restartedAt: "20260103-072537"
|
||||
spec:
|
||||
# Harbor 镜像拉取凭证
|
||||
imagePullSecrets:
|
||||
- name: ghcr-pull
|
||||
- name: regcred
|
||||
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
topologyKey: topology.kubernetes.io/zone
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: ljwx-admin
|
||||
- weight: 50
|
||||
podAffinityTerm:
|
||||
topologyKey: kubernetes.io/hostname
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: ljwx-admin
|
||||
|
||||
containers:
|
||||
- name: ljwx-admin
|
||||
image: harbor.omniverseai.net/ljwx-health/ljwx-admin:latest
|
||||
|
||||
@@ -32,32 +32,10 @@ spec:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/restartedAt: "20260103-072537"
|
||||
spec:
|
||||
# Harbor 镜像拉取凭证
|
||||
imagePullSecrets:
|
||||
- name: ghcr-pull
|
||||
- name: regcred
|
||||
|
||||
# 初始化容器 - 等待数据库就绪
|
||||
initContainers:
|
||||
- name: wait-for-mysql
|
||||
image: busybox:1.35
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
echo "Waiting for MySQL to be ready..."
|
||||
until nc -z mysql-lb.infra.svc.cluster.local 3306; do
|
||||
echo "MySQL not ready, sleeping for 2 seconds..."
|
||||
sleep 2
|
||||
done
|
||||
echo "MySQL is ready!"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
|
||||
# 主容器
|
||||
containers:
|
||||
- name: ljwx-bigscreen
|
||||
@@ -75,20 +53,18 @@ spec:
|
||||
|
||||
# 环境变量 - 从 Secret(敏感信息)
|
||||
env:
|
||||
# 数据库配置
|
||||
- name: MYSQL_HOST
|
||||
value: "mysql-lb.infra.svc.cluster.local"
|
||||
- name: MYSQL_PORT
|
||||
value: "3306"
|
||||
- name: MYSQL_DATABASE
|
||||
value: "ljwx_health"
|
||||
- name: MYSQL_USER
|
||||
value: "root"
|
||||
- name: MYSQL_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-secret
|
||||
key: DB_PASSWORD
|
||||
# Backend API
|
||||
- name: BOOT_API_URL
|
||||
value: "http://ljwx-boot:9998"
|
||||
|
||||
# Legacy 5225 bookmark cutover (#172). Keep the base fail-safe;
|
||||
# environment overlays can progress legacy -> allowlist -> redirect.
|
||||
- name: BIGSCREEN_CUTOVER_MODE
|
||||
value: "legacy"
|
||||
- name: BIGSCREEN_CUTOVER_ADMIN_BASE_URL
|
||||
value: "https://health-dashboard.omniverseai.net"
|
||||
- name: BIGSCREEN_CUTOVER_TENANTS
|
||||
value: ""
|
||||
|
||||
# Redis 配置
|
||||
- name: REDIS_HOST
|
||||
@@ -102,6 +78,11 @@ spec:
|
||||
key: REDIS_PASSWORD
|
||||
|
||||
# Flask 配置
|
||||
- name: SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-secret
|
||||
key: SECRET_KEY
|
||||
- name: FLASK_ENV
|
||||
value: "production"
|
||||
- name: FLASK_APP
|
||||
@@ -119,7 +100,7 @@ spec:
|
||||
# 健康检查
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
path: /bigscreen/api/health
|
||||
port: 5225
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 60
|
||||
@@ -130,7 +111,7 @@ spec:
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
path: /bigscreen/api/health
|
||||
port: 5225
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 30
|
||||
@@ -141,7 +122,7 @@ spec:
|
||||
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
path: /bigscreen/api/health
|
||||
port: 5225
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 15
|
||||
|
||||
@@ -13,7 +13,6 @@ metadata:
|
||||
component: backend
|
||||
version: v1.0.0
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
@@ -22,6 +21,8 @@ spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ljwx-boot
|
||||
app.kubernetes.io/name: ljwx-health
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
component: backend
|
||||
template:
|
||||
metadata:
|
||||
@@ -33,26 +34,44 @@ spec:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "9998"
|
||||
prometheus.io/path: "/actuator/prometheus"
|
||||
ops.ljwx.com/auth-log-policy: "sa-token-log-off-v1"
|
||||
kubectl.kubernetes.io/restartedAt: "20260103-080347"
|
||||
ops.ljwx.io/config-revision: "rls-restricted-role-minio-v2"
|
||||
spec:
|
||||
# Harbor 镜像拉取凭证
|
||||
imagePullSecrets:
|
||||
- name: ghcr-pull
|
||||
- name: regcred
|
||||
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
topologyKey: topology.kubernetes.io/zone
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: ljwx-boot
|
||||
- weight: 50
|
||||
podAffinityTerm:
|
||||
topologyKey: kubernetes.io/hostname
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: ljwx-boot
|
||||
|
||||
# 初始化容器 - 等待数据库就绪
|
||||
initContainers:
|
||||
- name: wait-for-mysql
|
||||
- name: wait-for-db
|
||||
image: busybox:1.35
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
echo "Waiting for MySQL to be ready..."
|
||||
until nc -z mysql-lb.infra.svc.cluster.local 3306; do
|
||||
echo "MySQL not ready, sleeping for 2 seconds..."
|
||||
echo "Waiting for PostgreSQL to be ready..."
|
||||
until nc -z postgres-lb.infra.svc.cluster.local 5432; do
|
||||
echo "PostgreSQL not ready, sleeping for 2 seconds..."
|
||||
sleep 2
|
||||
done
|
||||
echo "MySQL is ready!"
|
||||
echo "PostgreSQL is ready!"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
@@ -83,14 +102,14 @@ spec:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-secret
|
||||
key: DB_PASSWORD
|
||||
key: DB_APP_PASSWORD
|
||||
|
||||
# Spring 数据库密码(新格式)
|
||||
- name: SPRING_DATASOURCE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-secret
|
||||
key: DB_PASSWORD
|
||||
key: DB_APP_PASSWORD
|
||||
|
||||
# Redis 密码(旧格式)
|
||||
- name: REDIS_PASSWORD
|
||||
@@ -118,6 +137,13 @@ spec:
|
||||
name: ljwx-health-secret
|
||||
key: OSS_SECRET_KEY
|
||||
|
||||
# Sa-Token JWT 签名密钥(仅从运行时 Secret 注入)
|
||||
- name: SA_TOKEN_JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-secret
|
||||
key: SA_TOKEN_JWT_SECRET
|
||||
|
||||
volumeMounts:
|
||||
- name: app-logs
|
||||
mountPath: /app/logs
|
||||
@@ -159,10 +185,10 @@ spec:
|
||||
path: /actuator/health
|
||||
port: 9998
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 30
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 30
|
||||
timeoutSeconds: 10
|
||||
failureThreshold: 60
|
||||
successThreshold: 1
|
||||
|
||||
# 生命周期
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: health-recon
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
app: health-recon
|
||||
component: health-shadow
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: health-recon
|
||||
component: health-shadow
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: health-recon
|
||||
component: health-shadow
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8080"
|
||||
prometheus.io/path: /actuator/prometheus
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
imagePullSecrets:
|
||||
- name: ghcr-pull
|
||||
- name: regcred
|
||||
containers:
|
||||
- name: health-recon
|
||||
image: harbor.omniverseai.net/ljwx-health/health-recon:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
env:
|
||||
- name: CLICKHOUSE_HOST
|
||||
value: clickhouse.infra.svc.cluster.local
|
||||
- name: CLICKHOUSE_PORT
|
||||
value: "8123"
|
||||
- name: CLICKHOUSE_DB
|
||||
value: ljwx_health
|
||||
- name: CLICKHOUSE_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-clickhouse-secret
|
||||
key: CLICKHOUSE_USER
|
||||
- name: CLICKHOUSE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-clickhouse-secret
|
||||
key: CLICKHOUSE_PASSWORD
|
||||
- name: PG_JDBC_URL
|
||||
value: jdbc:postgresql://postgres-lb.infra.svc.cluster.local:5432/ljwx_health?connectTimeout=5&socketTimeout=30
|
||||
- name: PG_USER
|
||||
value: ljwx_app
|
||||
- name: PG_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-secret
|
||||
key: DB_APP_PASSWORD
|
||||
- name: RECON_CRON
|
||||
value: "0 30 1 * * ?"
|
||||
- name: RECON_RUN_ON_START
|
||||
value: "true"
|
||||
- name: RECON_ALLOWED_CUSTOMER_IDS
|
||||
value: "1"
|
||||
- name: RECON_MAX_DIFF
|
||||
value: "0"
|
||||
- name: TZ
|
||||
value: Asia/Shanghai
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 640Mi
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/liveness
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
failureThreshold: 24
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/liveness
|
||||
port: http
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/readiness
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
terminationGracePeriodSeconds: 30
|
||||
@@ -0,0 +1,93 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: health-writer
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
app: health-writer
|
||||
component: health-shadow
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: health-writer
|
||||
component: health-shadow
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: health-writer
|
||||
component: health-shadow
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8080"
|
||||
prometheus.io/path: /actuator/prometheus
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
imagePullSecrets:
|
||||
- name: ghcr-pull
|
||||
- name: regcred
|
||||
containers:
|
||||
- name: health-writer
|
||||
image: harbor.omniverseai.net/ljwx-health/health-writer:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
env:
|
||||
- name: KAFKA_BOOTSTRAP_SERVERS
|
||||
value: kafka-cluster-kafka-bootstrap.shared-base.svc.cluster.local:9092
|
||||
- name: KAFKA_CONCURRENCY
|
||||
value: "1"
|
||||
- name: CLICKHOUSE_HOST
|
||||
value: clickhouse.infra.svc.cluster.local
|
||||
- name: CLICKHOUSE_PORT
|
||||
value: "8123"
|
||||
- name: CLICKHOUSE_DB
|
||||
value: ljwx_health
|
||||
- name: CLICKHOUSE_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-clickhouse-secret
|
||||
key: CLICKHOUSE_USER
|
||||
- name: CLICKHOUSE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-clickhouse-secret
|
||||
key: CLICKHOUSE_PASSWORD
|
||||
- name: LJWX_CONSUMER_HEALTH_RAW_VENDOR_COLUMNS_ENABLED
|
||||
value: "true"
|
||||
- name: TZ
|
||||
value: Asia/Shanghai
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 768Mi
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/liveness
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
failureThreshold: 24
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/liveness
|
||||
port: http
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/readiness
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
terminationGracePeriodSeconds: 30
|
||||
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"annotations": {"list": []},
|
||||
"editable": true,
|
||||
"graphTooltip": 1,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"mappings": [], "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}, "unit": "short"}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 0, "y": 0},
|
||||
"id": 1,
|
||||
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
|
||||
"targets": [{"expr": "max(up{namespace=\"ljwx-health\",service=\"ljwx-boot\"})", "refId": "A"}],
|
||||
"title": "Boot target up",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"mappings": [], "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "red", "value": 0.001}]}, "unit": "percentunit"}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 6, "y": 0},
|
||||
"id": 2,
|
||||
"options": {"colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
|
||||
"targets": [{"expr": "ljwx_health:http_5xx:ratio5m", "refId": "A"}],
|
||||
"title": "5xx ratio / 5m",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"mappings": [], "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "red", "value": 1}]}, "unit": "s"}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 12, "y": 0},
|
||||
"id": 3,
|
||||
"options": {"colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
|
||||
"targets": [{"expr": "ljwx_health:http_latency_p95:5m", "refId": "A"}],
|
||||
"title": "P95 latency / 5m",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"mappings": [], "thresholds": {"mode": "absolute", "steps": [{"color": "green"}]}, "unit": "reqps"}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 18, "y": 0},
|
||||
"id": 4,
|
||||
"options": {"colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
|
||||
"targets": [{"expr": "ljwx_health:http_requests:rate5m", "refId": "A"}],
|
||||
"title": "Request rate / 5m",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"color": {"mode": "palette-classic"}, "custom": {"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "drawStyle": "line", "fillOpacity": 15, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "showPoints": "never", "spanNulls": false, "stacking": {"group": "A", "mode": "none"}}, "mappings": [], "thresholds": {"mode": "absolute", "steps": [{"color": "green"}]}, "unit": "percentunit"}, "overrides": []},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 5},
|
||||
"id": 5,
|
||||
"options": {"legend": {"calcs": ["lastNotNull", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true}, "tooltip": {"mode": "single", "sort": "none"}},
|
||||
"targets": [{"expr": "sum(jvm_memory_used_bytes{namespace=\"ljwx-health\",service=\"ljwx-boot\",area=\"heap\"}) / clamp_min(sum(jvm_memory_max_bytes{namespace=\"ljwx-health\",service=\"ljwx-boot\",area=\"heap\"}), 1)", "legendFormat": "heap", "refId": "A"}],
|
||||
"title": "JVM heap saturation",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"color": {"mode": "palette-classic"}, "custom": {"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "drawStyle": "line", "fillOpacity": 15, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "showPoints": "never", "spanNulls": false, "stacking": {"group": "A", "mode": "none"}}, "mappings": [], "thresholds": {"mode": "absolute", "steps": [{"color": "green"}]}, "unit": "percentunit"}, "overrides": []},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 5},
|
||||
"id": 6,
|
||||
"options": {"legend": {"calcs": ["lastNotNull", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true}, "tooltip": {"mode": "single", "sort": "none"}},
|
||||
"targets": [{"expr": "sum(hikaricp_connections_active{namespace=\"ljwx-health\",service=\"ljwx-boot\"}) / clamp_min(sum(hikaricp_connections_max{namespace=\"ljwx-health\",service=\"ljwx-boot\"}), 1)", "legendFormat": "pool", "refId": "A"}],
|
||||
"title": "Database pool saturation",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "reqps", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}]}}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 0, "y": 13},
|
||||
"id": 7,
|
||||
"targets": [{"expr": "ljwx_health:bigscreen_bff_qps:5m{tenant=~\"$tenant\"}", "legendFormat": "tenant {{tenant}}", "refId": "A"}],
|
||||
"title": "Bigscreen BFF QPS",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1, "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 0.995}]}}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 6, "y": 13},
|
||||
"id": 8,
|
||||
"targets": [{"expr": "ljwx_health:bigscreen_bff_success:ratio5m{tenant=~\"$tenant\"}", "legendFormat": "tenant {{tenant}}", "refId": "A"}],
|
||||
"title": "BFF success ratio (target 99.5%)",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "s", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "red", "value": 0.5}]}}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 12, "y": 13},
|
||||
"id": 9,
|
||||
"targets": [
|
||||
{"expr": "ljwx_health:bigscreen_bff_latency_p95:5m{tenant=~\"$tenant\"}", "legendFormat": "P95 {{tenant}}", "refId": "A"},
|
||||
{"expr": "ljwx_health:bigscreen_bff_latency_p99:5m{tenant=~\"$tenant\"}", "legendFormat": "P99 {{tenant}}", "refId": "B"}
|
||||
],
|
||||
"title": "BFF P95 / P99 latency",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1, "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 0.7}]}}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 18, "y": 13},
|
||||
"id": 10,
|
||||
"targets": [{"expr": "ljwx_health:bigscreen_cache_hit:ratio5m{tenant=~\"$tenant\"}", "legendFormat": "tenant {{tenant}}", "refId": "A"}],
|
||||
"title": "Redis cache hit ratio (target 70%)",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1, "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 0.3}]}}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 0, "y": 18},
|
||||
"id": 11,
|
||||
"targets": [{"expr": "ljwx_health:bigscreen_snapshot_304:ratio5m{tenant=~\"$tenant\"}", "legendFormat": "tenant {{tenant}}", "refId": "A"}],
|
||||
"title": "Snapshot 304 ratio (target 30%)",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "s", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "red", "value": 0.5}]}}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 6, "y": 18},
|
||||
"id": 12,
|
||||
"targets": [{"expr": "ljwx_health:bigscreen_snapshot_build_p95:5m{tenant=~\"$tenant\"}", "legendFormat": "tenant {{tenant}}", "refId": "A"}],
|
||||
"title": "Snapshot DB aggregation P95",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1, "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 0.995}]}}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 12, "y": 18},
|
||||
"id": 13,
|
||||
"targets": [{"expr": "ljwx_health:bigscreen_kiosk_refresh_success:ratio5m{tenant=~\"$tenant\"}", "legendFormat": "tenant {{tenant}}", "refId": "A"}],
|
||||
"title": "Kiosk refresh success ratio",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "s", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "red", "value": 2}]}}, "overrides": []},
|
||||
"gridPos": {"h": 5, "w": 6, "x": 18, "y": 18},
|
||||
"id": 14,
|
||||
"targets": [{"expr": "ljwx_health:bigscreen_kiosk_update_delay_p95:5m{tenant=~\"$tenant\"}", "legendFormat": "tenant {{tenant}}", "refId": "A"}],
|
||||
"title": "generatedAt to kiosk update P95",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "s", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "red", "value": 300}]}}, "overrides": []},
|
||||
"gridPos": {"h": 7, "w": 8, "x": 0, "y": 23},
|
||||
"id": 15,
|
||||
"targets": [
|
||||
{"expr": "ljwx_health:bigscreen_kiosk_heartbeat_age:seconds{tenant=~\"$tenant\"}", "legendFormat": "heartbeat age {{tenant}}", "refId": "A"},
|
||||
{"expr": "ljwx_health:bigscreen_kiosk_recovery_p95:1h{tenant=~\"$tenant\"}", "legendFormat": "recovery P95 {{tenant}}", "refId": "B"}
|
||||
],
|
||||
"title": "Kiosk heartbeat and recovery",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "short", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "red", "value": 1}]}}, "overrides": []},
|
||||
"gridPos": {"h": 7, "w": 8, "x": 8, "y": 23},
|
||||
"id": 16,
|
||||
"targets": [
|
||||
{"expr": "sum by (tenant, type) (increase(bigscreen_kiosk_js_errors_total{tenant=~\"$tenant\"}[$__range]))", "legendFormat": "{{type}} {{tenant}}", "refId": "A"},
|
||||
{"expr": "sum by (tenant, event) (increase(bigscreen_kiosk_connectivity_total{tenant=~\"$tenant\"}[$__range]))", "legendFormat": "{{event}} {{tenant}}", "refId": "B"}
|
||||
],
|
||||
"title": "JS errors and offline/recovery events",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "bytes", "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "red", "value": 157286400}]}}, "overrides": []},
|
||||
"gridPos": {"h": 7, "w": 8, "x": 16, "y": 23},
|
||||
"id": 17,
|
||||
"targets": [{"expr": "max by (tenant) (max_over_time(bigscreen_kiosk_js_heap_bytes{tenant=~\"$tenant\"}[24h])) - min by (tenant) (min_over_time(bigscreen_kiosk_js_heap_bytes{tenant=~\"$tenant\"}[24h]))", "legendFormat": "24h growth {{tenant}}", "refId": "A"}],
|
||||
"title": "Kiosk 24h JS heap growth",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"fieldConfig": {"defaults": {"unit": "short"}, "overrides": []},
|
||||
"gridPos": {"h": 7, "w": 24, "x": 0, "y": 30},
|
||||
"id": 18,
|
||||
"targets": [{"expr": "ALERTS{alertname=~\"LjwxHealthBigscreen.*\",tenant=~\"$tenant\",alertstate=\"firing\"}", "format": "table", "instant": true, "refId": "A"}],
|
||||
"title": "Active bigscreen alerts",
|
||||
"type": "table"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 39,
|
||||
"tags": ["ljwx-health", "slo", "operations", "bigscreen", "trial"],
|
||||
"templating": {"list": [{"name": "tenant", "label": "Tenant", "type": "query", "datasource": {"type": "prometheus", "uid": "prometheus"}, "query": {"query": "label_values(bigscreen_bff_requests_total, tenant)", "refId": "StandardVariableQuery"}, "definition": "label_values(bigscreen_bff_requests_total, tenant)", "includeAll": true, "allValue": ".*", "multi": true, "refresh": 1, "current": {"text": "All", "value": "$__all", "selected": true}}]},
|
||||
"time": {"from": "now-24h", "to": "now"},
|
||||
"timezone": "browser",
|
||||
"title": "LJWX Health / SLO and Bigscreen Trial",
|
||||
"uid": "ljwx-health-slo",
|
||||
"version": 2
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: ljwx-boot
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: ljwx-boot
|
||||
minReplicas: 2
|
||||
maxReplicas: 4
|
||||
behavior:
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 60
|
||||
policies:
|
||||
- type: Pods
|
||||
value: 1
|
||||
periodSeconds: 60
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 25
|
||||
periodSeconds: 60
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: ljwx-admin
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: ljwx-admin
|
||||
minReplicas: 2
|
||||
maxReplicas: 4
|
||||
behavior:
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 60
|
||||
policies:
|
||||
- type: Pods
|
||||
value: 1
|
||||
periodSeconds: 60
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 25
|
||||
periodSeconds: 60
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
# Public clients use /api as the gateway prefix. Boot controllers are rooted at
|
||||
# /auth, /sys_*, /api/v2, etc.; strip exactly the outer gateway prefix.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ljwx-health-api-ingress
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
app: ljwx-health
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/use-regex: "true"
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /$2
|
||||
nginx.ingress.kubernetes.io/enable-cors: "true"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
|
||||
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||
nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
|
||||
nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: health-admin.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
- path: /api(/|$)(.*)
|
||||
pathType: ImplementationSpecific
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
- host: health-dashboard.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
- path: /api(/|$)(.*)
|
||||
pathType: ImplementationSpecific
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
- host: health-admin.omniverseai.net
|
||||
http:
|
||||
paths:
|
||||
- path: /api(/|$)(.*)
|
||||
pathType: ImplementationSpecific
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
- host: health-dashboard.omniverseai.net
|
||||
http:
|
||||
paths:
|
||||
- path: /api(/|$)(.*)
|
||||
pathType: ImplementationSpecific
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
@@ -3,8 +3,8 @@
|
||||
# ljwx-health Ingress Configuration
|
||||
# 路由配置:
|
||||
# - health-admin.lingjingwanxiang.cn -> ljwx-admin (Vue3 前端)
|
||||
# - health-dashboard.lingjingwanxiang.cn -> ljwx-bigscreen (Flask 大屏)
|
||||
# - health-admin.lingjingwanxiang.cn/api -> ljwx-boot (后端 API)
|
||||
# - health-dashboard.lingjingwanxiang.cn -> ljwx-admin (Vue3 大屏)
|
||||
# - API 路由由 ingress-api.yaml 独立处理并去掉外层 /api 前缀
|
||||
# - legacy omniverseai.net hosts remain during cutover
|
||||
# =============================================================================
|
||||
apiVersion: networking.k8s.io/v1
|
||||
@@ -15,9 +15,13 @@ metadata:
|
||||
labels:
|
||||
app: ljwx-health
|
||||
annotations:
|
||||
# Public TLS is terminated by Cloudflare for the active omniverseai.net
|
||||
# hosts. Keep the origin on HTTP until the lingjingwanxiang.cn cutover DNS
|
||||
# records exist; enabling cert-manager/ssl-redirect before that point makes
|
||||
# ACME issuance fail and breaks the current tunnel origin path.
|
||||
# CORS 配置
|
||||
nginx.ingress.kubernetes.io/enable-cors: "true"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "https://health-admin.lingjingwanxiang.cn, https://health-dashboard.lingjingwanxiang.cn, https://health-admin.omniverseai.net, https://health-dashboard.omniverseai.net"
|
||||
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS"
|
||||
nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
|
||||
# 请求大小限制
|
||||
@@ -27,7 +31,7 @@ metadata:
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
# =========================================================================
|
||||
# Rule 1: health-admin.lingjingwanxiang.cn
|
||||
@@ -35,14 +39,6 @@ spec:
|
||||
- host: health-admin.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
# 后端 API 路由
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
# 前端静态资源
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
@@ -58,24 +54,17 @@ spec:
|
||||
- host: health-dashboard.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
# 大屏应用
|
||||
# Vue3 大屏应用
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-bigscreen
|
||||
name: ljwx-admin
|
||||
port:
|
||||
number: 5001
|
||||
number: 80
|
||||
- host: health-admin.omniverseai.net
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
@@ -90,6 +79,6 @@ spec:
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-bigscreen
|
||||
name: ljwx-admin
|
||||
port:
|
||||
number: 5001
|
||||
number: 80
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# Flyway Repair Job
|
||||
# 修复失败的迁移记录,清理 flyway_schema_history 表
|
||||
# =============================================================================
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: ljwx-health-flyway-repair
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
app: ljwx-health
|
||||
component: flyway-repair
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
|
||||
argocd.argoproj.io/sync-wave: "1" # 在 init-db (wave 0) 之后运行
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 300
|
||||
backoffLimit: 3
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ljwx-health
|
||||
component: flyway-repair
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: flyway-repair
|
||||
image: mysql:8.0
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
echo "检查并修复 Flyway schema history..."
|
||||
mysql -h mysql-lb.infra.svc.cluster.local -u root -p${MYSQL_ROOT_PASSWORD} ljwx_health <<EOF
|
||||
DROP TABLE IF EXISTS flyway_schema_history;
|
||||
SELECT 'Flyway schema history table dropped. Flyway will recreate it on next run.' AS result;
|
||||
EOF
|
||||
|
||||
echo "✅ Flyway schema history 修复完成"
|
||||
env:
|
||||
- name: MYSQL_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-secret
|
||||
key: DB_PASSWORD
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
@@ -0,0 +1,56 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: health-shadow-clickhouse-init
|
||||
namespace: ljwx-health
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded
|
||||
argocd.argoproj.io/sync-wave: "-1"
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
activeDeadlineSeconds: 300
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
automountServiceAccountToken: false
|
||||
containers:
|
||||
- name: clickhouse-schema-init
|
||||
image: clickhouse/clickhouse-server:24.3.12.75
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
clickhouse-client \
|
||||
--host clickhouse.infra.svc.cluster.local \
|
||||
--port 9000 \
|
||||
--user "$CLICKHOUSE_USER" \
|
||||
--password "$CLICKHOUSE_PASSWORD" \
|
||||
--multiquery < /schema/clickhouse-health-raw.sql
|
||||
env:
|
||||
- name: CLICKHOUSE_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-clickhouse-secret
|
||||
key: CLICKHOUSE_USER
|
||||
- name: CLICKHOUSE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-clickhouse-secret
|
||||
key: CLICKHOUSE_PASSWORD
|
||||
volumeMounts:
|
||||
- name: schema
|
||||
mountPath: /schema
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
volumes:
|
||||
- name: schema
|
||||
configMap:
|
||||
name: ljwx-health-shadow-clickhouse-schema
|
||||
@@ -0,0 +1,73 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: health-shadow-kafka-init
|
||||
namespace: ljwx-health
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded
|
||||
argocd.argoproj.io/sync-wave: "-1"
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
activeDeadlineSeconds: 300
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
automountServiceAccountToken: false
|
||||
containers:
|
||||
- name: kafka-topic-init
|
||||
image: apache/kafka:4.1.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
BOOTSTRAP=kafka-cluster-kafka-bootstrap.shared-base.svc.cluster.local:9092
|
||||
/opt/kafka/bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" \
|
||||
--create --if-not-exists --topic health_raw \
|
||||
--partitions 3 --replication-factor 1 \
|
||||
--config retention.ms=604800000
|
||||
/opt/kafka/bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" \
|
||||
--create --if-not-exists --topic health_raw.DLT \
|
||||
--partitions 1 --replication-factor 1 \
|
||||
--config retention.ms=1209600000
|
||||
current_partitions="$(/opt/kafka/bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" \
|
||||
--describe --topic health_raw \
|
||||
| awk 'NR == 1 { for (i = 1; i <= NF; i++) if ($i == "PartitionCount:") { print $(i + 1); exit } }')"
|
||||
case "$current_partitions" in
|
||||
''|*[!0-9]*)
|
||||
echo "unable to determine health_raw partition count: $current_partitions" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if [ "$current_partitions" -lt 3 ]; then
|
||||
/opt/kafka/bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" \
|
||||
--alter --topic health_raw --partitions 3
|
||||
fi
|
||||
/opt/kafka/bin/kafka-configs.sh --bootstrap-server "$BOOTSTRAP" \
|
||||
--alter --entity-type topics --entity-name health_raw \
|
||||
--add-config retention.ms=604800000
|
||||
/opt/kafka/bin/kafka-configs.sh --bootstrap-server "$BOOTSTRAP" \
|
||||
--alter --entity-type topics --entity-name health_raw.DLT \
|
||||
--add-config retention.ms=1209600000
|
||||
main_desc="$(/opt/kafka/bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" \
|
||||
--describe --topic health_raw)"
|
||||
dlt_desc="$(/opt/kafka/bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" \
|
||||
--describe --topic health_raw.DLT)"
|
||||
main_config="$(/opt/kafka/bin/kafka-configs.sh --bootstrap-server "$BOOTSTRAP" \
|
||||
--describe --entity-type topics --entity-name health_raw)"
|
||||
dlt_config="$(/opt/kafka/bin/kafka-configs.sh --bootstrap-server "$BOOTSTRAP" \
|
||||
--describe --entity-type topics --entity-name health_raw.DLT)"
|
||||
printf '%s\n' "$main_desc"
|
||||
printf '%s\n' "$dlt_desc"
|
||||
printf '%s\n' "$main_desc" | grep -q 'PartitionCount: 3'
|
||||
printf '%s\n' "$dlt_desc" | grep -q 'PartitionCount: 1'
|
||||
printf '%s\n' "$main_config" | grep -q 'retention.ms=604800000'
|
||||
printf '%s\n' "$dlt_config" | grep -q 'retention.ms=1209600000'
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# Database Initialization Job
|
||||
# 在 ljwx-boot 启动前创建数据库,让 Flyway 可以运行迁移
|
||||
# =============================================================================
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: ljwx-health-init-db
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
app: ljwx-health
|
||||
component: init-db
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
|
||||
argocd.argoproj.io/sync-wave: "0" # 首先运行
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 300 # 5分钟后自动清理
|
||||
backoffLimit: 3
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ljwx-health
|
||||
component: init-db
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: init-database
|
||||
image: mysql:8.0
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
echo "检查并创建 ljwx_health 数据库..."
|
||||
mysql -h mysql-lb.infra.svc.cluster.local -u root -p${MYSQL_ROOT_PASSWORD} <<EOF
|
||||
CREATE DATABASE IF NOT EXISTS ljwx_health
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
SHOW DATABASES LIKE 'ljwx_health';
|
||||
|
||||
SELECT 'Database ljwx_health is ready' AS status;
|
||||
EOF
|
||||
|
||||
echo "✅ 数据库初始化完成"
|
||||
env:
|
||||
- name: MYSQL_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ljwx-health-secret
|
||||
key: DB_PASSWORD
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
@@ -7,17 +7,53 @@ namespace: ljwx-health
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- configmap.yaml
|
||||
- job-init-db.yaml
|
||||
- job-flyway-repair.yaml
|
||||
- deployment-boot.yaml
|
||||
- deployment-admin.yaml
|
||||
- deployment-bigscreen.yaml
|
||||
- deployment-health-writer.yaml
|
||||
- deployment-health-recon.yaml
|
||||
- service-boot.yaml
|
||||
- service-boot-compat.yaml
|
||||
- service-admin.yaml
|
||||
- service-bigscreen.yaml
|
||||
- service-health-shadow.yaml
|
||||
- job-health-shadow-kafka-init.yaml
|
||||
- job-health-shadow-clickhouse-init.yaml
|
||||
- ingress.yaml
|
||||
- ingress-api.yaml
|
||||
- servicemonitor.yaml
|
||||
- prometheusrule.yaml
|
||||
- prometheusrule-health-shadow.yaml
|
||||
- servicemonitor-health-shadow.yaml
|
||||
- pdb.yaml
|
||||
- hpa.yaml
|
||||
- networkpolicy.yaml
|
||||
- cloudflared.yaml
|
||||
- cloudflared-networkpolicy.yaml
|
||||
|
||||
commonLabels:
|
||||
app.kubernetes.io/name: ljwx-health
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
generatorOptions:
|
||||
disableNameSuffixHash: true
|
||||
|
||||
configMapGenerator:
|
||||
- name: ljwx-health-shadow-clickhouse-schema
|
||||
files:
|
||||
- clickhouse-health-raw.sql=sql/health-shadow/clickhouse-health-raw.sql
|
||||
options:
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
- name: grafana-dashboard-ljwx-health-slo
|
||||
files:
|
||||
- ljwx-health-slo-dashboard.json=grafana/ljwx-health-slo-dashboard.json
|
||||
options:
|
||||
labels:
|
||||
grafana_dashboard: "1"
|
||||
app.kubernetes.io/component: observability
|
||||
|
||||
labels:
|
||||
- includeSelectors: false
|
||||
includeTemplates: true
|
||||
pairs:
|
||||
app.kubernetes.io/name: ljwx-health
|
||||
app.kubernetes.io/managed-by: kustomize
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: ljwx-health-ingress
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
podSelector:
|
||||
matchExpressions:
|
||||
- key: app
|
||||
operator: In
|
||||
values:
|
||||
- ljwx-boot
|
||||
- ljwx-admin
|
||||
- health-writer
|
||||
- health-recon
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector: {}
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: infra
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: ingress-nginx
|
||||
app.kubernetes.io/component: controller
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: observability
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: prometheus
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: ljwx-boot
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ljwx-boot
|
||||
component: backend
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: ljwx-admin
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ljwx-admin
|
||||
component: frontend
|
||||
@@ -0,0 +1,66 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: ljwx-health-shadow
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
release: monitoring
|
||||
spec:
|
||||
groups:
|
||||
- name: ljwx_health_shadow
|
||||
interval: 30s
|
||||
rules:
|
||||
- alert: LjwxHealthShadowWriterUnavailable
|
||||
expr: absent(up{namespace="ljwx-health",service="health-writer"}) or up{namespace="ljwx-health",service="health-writer"} == 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: data-integrity
|
||||
annotations:
|
||||
summary: Health shadow writer is unavailable
|
||||
description: PostgreSQL remains authoritative; keep publisher canary fail closed and investigate Kafka/ClickHouse.
|
||||
- alert: LjwxHealthShadowReconUnavailable
|
||||
expr: absent(up{namespace="ljwx-health",service="health-recon"}) or up{namespace="ljwx-health",service="health-recon"} == 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: data-integrity
|
||||
annotations:
|
||||
summary: Health shadow reconciliation is unavailable
|
||||
description: No promotion decision is allowed while the recon runner is unavailable.
|
||||
- alert: LjwxHealthShadowWriterFailures
|
||||
expr: increase(health_shadow_writer_batches_total{result="failed"}[10m]) > 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
category: data-integrity
|
||||
annotations:
|
||||
summary: Health shadow writer batch failed
|
||||
description: The Kafka batch remains unacknowledged; PostgreSQL upload is unaffected.
|
||||
- alert: LjwxHealthShadowReconMismatch
|
||||
expr: health_olap_recon_missing_count{customer_id="0"} > 0 or health_olap_recon_duplicate_count{customer_id="0"} > 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
category: data-integrity
|
||||
annotations:
|
||||
summary: Health shadow reconciliation mismatch
|
||||
description: Missing or duplicate business rows fail the seven-day promotion gate.
|
||||
- alert: LjwxHealthShadowReconLagHigh
|
||||
expr: health_olap_recon_lag_seconds{customer_id="0"} > 3600
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
category: data-integrity
|
||||
annotations:
|
||||
summary: Health shadow lag exceeds one hour
|
||||
description: Keep ClickHouse out of customer-visible reads until lag returns within threshold.
|
||||
- alert: LjwxHealthShadowDisallowedTenant
|
||||
expr: health_olap_recon_disallowed_tenant_count{customer_id="0"} > 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
category: data-integrity
|
||||
annotations:
|
||||
summary: Health shadow contains a non-canary tenant
|
||||
description: Disable the publisher canary and investigate tenant-gate enforcement before any promotion.
|
||||
@@ -0,0 +1,671 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: ljwx-health-slo
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
release: monitoring
|
||||
app.kubernetes.io/name: ljwx-health
|
||||
app.kubernetes.io/component: observability
|
||||
spec:
|
||||
groups:
|
||||
- name: ljwx_health_availability
|
||||
interval: 30s
|
||||
rules:
|
||||
- alert: LjwxHealthDeploymentUnavailable
|
||||
expr: kube_deployment_status_replicas_available{namespace="ljwx-health",deployment=~"ljwx-(boot|admin)"} < 1
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
category: availability
|
||||
annotations:
|
||||
summary: "LJWX Health deployment is unavailable"
|
||||
description: "{{ $labels.deployment }} has had no available replica for more than two minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- alert: LjwxHealthBootMetricsTargetMissing
|
||||
expr: absent(up{namespace="ljwx-health",service="ljwx-boot"})
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
category: observability
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "Prometheus has no ljwx-boot target"
|
||||
description: "The application may be healthy, but its metrics target has been absent for five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- alert: LjwxHealthBootMetricsTargetDown
|
||||
expr: up{namespace="ljwx-health",service="ljwx-boot"} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
category: availability
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "Prometheus cannot scrape ljwx-boot"
|
||||
description: "The ljwx-boot metrics target has been down for more than two minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- alert: LjwxHealthPodRestarting
|
||||
expr: increase(kube_pod_container_status_restarts_total{namespace="ljwx-health",container=~"ljwx-(boot|admin)"}[30m]) > 2
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: availability
|
||||
annotations:
|
||||
summary: "LJWX Health pod is restarting repeatedly"
|
||||
description: "{{ $labels.pod }} restarted more than twice in 30 minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- name: ljwx_health_golden_signals
|
||||
interval: 30s
|
||||
rules:
|
||||
- record: ljwx_health:http_requests:rate5m
|
||||
expr: sum(rate(http_server_requests_seconds_count{namespace="ljwx-health",service="ljwx-boot",uri!~"/actuator.*"}[5m]))
|
||||
|
||||
- record: ljwx_health:http_5xx:ratio5m
|
||||
expr: |
|
||||
sum(rate(http_server_requests_seconds_count{namespace="ljwx-health",service="ljwx-boot",status=~"5..",uri!~"/actuator.*"}[5m]))
|
||||
/
|
||||
clamp_min(sum(rate(http_server_requests_seconds_count{namespace="ljwx-health",service="ljwx-boot",uri!~"/actuator.*"}[5m])), 0.001)
|
||||
|
||||
- record: ljwx_health:http_5xx:ratio1h
|
||||
expr: |
|
||||
sum(rate(http_server_requests_seconds_count{namespace="ljwx-health",service="ljwx-boot",status=~"5..",uri!~"/actuator.*"}[1h]))
|
||||
/
|
||||
clamp_min(sum(rate(http_server_requests_seconds_count{namespace="ljwx-health",service="ljwx-boot",uri!~"/actuator.*"}[1h])), 0.001)
|
||||
|
||||
- record: ljwx_health:http_5xx:ratio30m
|
||||
expr: |
|
||||
sum(rate(http_server_requests_seconds_count{namespace="ljwx-health",service="ljwx-boot",status=~"5..",uri!~"/actuator.*"}[30m]))
|
||||
/
|
||||
clamp_min(sum(rate(http_server_requests_seconds_count{namespace="ljwx-health",service="ljwx-boot",uri!~"/actuator.*"}[30m])), 0.001)
|
||||
|
||||
- record: ljwx_health:http_5xx:ratio6h
|
||||
expr: |
|
||||
sum(rate(http_server_requests_seconds_count{namespace="ljwx-health",service="ljwx-boot",status=~"5..",uri!~"/actuator.*"}[6h]))
|
||||
/
|
||||
clamp_min(sum(rate(http_server_requests_seconds_count{namespace="ljwx-health",service="ljwx-boot",uri!~"/actuator.*"}[6h])), 0.001)
|
||||
|
||||
- record: ljwx_health:http_latency_p95:5m
|
||||
expr: |
|
||||
histogram_quantile(
|
||||
0.95,
|
||||
sum(rate(http_server_requests_seconds_bucket{namespace="ljwx-health",service="ljwx-boot",uri!~"/actuator.*"}[5m])) by (le)
|
||||
)
|
||||
|
||||
- alert: LjwxHealthBootHighErrorRate
|
||||
expr: ljwx_health:http_5xx:ratio5m > 0.05
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: errors
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "ljwx-boot 5xx ratio is above 5%"
|
||||
description: "The five-minute server error ratio has remained above 5%."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- alert: LjwxHealthBootHighP95Latency
|
||||
expr: ljwx_health:http_latency_p95:5m > 1
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
category: latency
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "ljwx-boot P95 latency is above one second"
|
||||
description: "The five-minute P95 latency has remained above one second."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- alert: LjwxHealthBootJvmHeapHigh
|
||||
expr: sum(jvm_memory_used_bytes{namespace="ljwx-health",service="ljwx-boot",area="heap"}) / clamp_min(sum(jvm_memory_max_bytes{namespace="ljwx-health",service="ljwx-boot",area="heap"}), 1) > 0.85
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
category: saturation
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "ljwx-boot JVM heap is above 85%"
|
||||
description: "Heap saturation has remained above 85% for ten minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- alert: LjwxHealthBootDbPoolSaturated
|
||||
expr: sum(hikaricp_connections_active{namespace="ljwx-health",service="ljwx-boot"}) / clamp_min(sum(hikaricp_connections_max{namespace="ljwx-health",service="ljwx-boot"}), 1) > 0.9
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
category: saturation
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "ljwx-boot database pool is above 90%"
|
||||
description: "The database connection pool has remained saturated for five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- name: ljwx_health_slo_burn_rate
|
||||
interval: 30s
|
||||
rules:
|
||||
- alert: LjwxHealthSloFastBurn
|
||||
expr: ljwx_health:http_5xx:ratio5m > 0.0144 and ljwx_health:http_5xx:ratio1h > 0.0144
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
category: slo
|
||||
service: ljwx-boot
|
||||
slo_target: "99.9"
|
||||
annotations:
|
||||
summary: "LJWX Health is consuming error budget rapidly"
|
||||
description: "The 5m and 1h windows exceed the 14.4x burn threshold for the 99.9% SLO."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- alert: LjwxHealthSloSlowBurn
|
||||
expr: ljwx_health:http_5xx:ratio30m > 0.006 and ljwx_health:http_5xx:ratio6h > 0.006
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
category: slo
|
||||
service: ljwx-boot
|
||||
slo_target: "99.9"
|
||||
annotations:
|
||||
summary: "LJWX Health is steadily consuming error budget"
|
||||
description: "The 30m and 6h windows exceed the 6x burn threshold for the 99.9% SLO."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- name: ljwx_health_realtime_score_canary
|
||||
interval: 30s
|
||||
rules:
|
||||
- record: ljwx_health:realtime_score_freshness_p95:5m
|
||||
expr: |
|
||||
histogram_quantile(
|
||||
0.95,
|
||||
sum(rate(health_realtime_score_freshness_seconds_bucket{namespace="ljwx-health",service="ljwx-boot"}[5m])) by (le)
|
||||
)
|
||||
|
||||
- record: ljwx_health:realtime_score_write_amplification:5m
|
||||
expr: |
|
||||
sum(rate(health_realtime_score_flush_total{namespace="ljwx-health",service="ljwx-boot",result="success"}[5m]))
|
||||
/
|
||||
clamp_min(sum(rate(health_realtime_score_events_total{namespace="ljwx-health",service="ljwx-boot",result="accepted"}[5m])), 0.001)
|
||||
|
||||
- alert: LjwxHealthRealtimeScoreFreshnessSloBreached
|
||||
expr: |
|
||||
ljwx_health:realtime_score_freshness_p95:5m > 5
|
||||
and
|
||||
sum(rate(health_realtime_score_events_total{namespace="ljwx-health",service="ljwx-boot",result="accepted"}[5m])) > 0
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
category: health-score
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "Realtime health-score freshness P95 is above five seconds"
|
||||
description: "Persisted events have taken more than five seconds to become visible in Redis for ten minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/02-architecture/business-data-chains/epic-360-operations.md"
|
||||
|
||||
- alert: LjwxHealthRealtimeScoreListenerFailures
|
||||
expr: |
|
||||
sum(rate(health_realtime_score_events_total{namespace="ljwx-health",service="ljwx-boot",result="failed"}[5m])) > 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: health-score
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "Realtime health-score listener is failing"
|
||||
description: "Persisted health events are failing in the realtime-score listener window."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/02-architecture/business-data-chains/epic-360-operations.md"
|
||||
|
||||
- alert: LjwxHealthRealtimeScoreExecutorSaturated
|
||||
expr: |
|
||||
sum(rate(health_realtime_score_events_total{namespace="ljwx-health",service="ljwx-boot",result="rejected"}[5m])) > 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: warning
|
||||
category: health-score
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "Realtime health-score executor is rejecting work"
|
||||
description: "The isolated realtime-score executor is saturated; upload ACK remains isolated and overnight ground truth is the recovery path."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/02-architecture/business-data-chains/epic-360-operations.md"
|
||||
|
||||
- alert: LjwxHealthRealtimeScoreExecutorBacklog
|
||||
expr: |
|
||||
executor_queued_tasks{namespace="ljwx-health",service="ljwx-boot",name="healthRealtimeScoreTaskExecutor"} > 80
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: health-score
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "Realtime health-score executor queue is near capacity"
|
||||
description: "The isolated executor has held more than 80 queued event batches for five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/02-architecture/business-data-chains/epic-360-operations.md"
|
||||
|
||||
- alert: LjwxHealthRealtimeScoreFlushFailures
|
||||
expr: |
|
||||
sum(rate(health_realtime_score_flush_total{namespace="ljwx-health",service="ljwx-boot",result=~"failed|ack_failed"}[5m])) > 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: health-score
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "Realtime health-score merge writes are failing"
|
||||
description: "A realtime-score database flush or post-commit acknowledgement has failed during the last five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/02-architecture/business-data-chains/epic-360-operations.md"
|
||||
|
||||
- alert: LjwxHealthRealtimeScoreWriteAmplificationHigh
|
||||
expr: |
|
||||
ljwx_health:realtime_score_write_amplification:5m >= 0.2
|
||||
and
|
||||
sum(rate(health_realtime_score_events_total{namespace="ljwx-health",service="ljwx-boot",result="accepted"}[5m])) > 0
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
category: health-score
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "Realtime health-score write amplification is too high"
|
||||
description: "Merged PostgreSQL writes have remained at or above 20 percent of accepted events."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/02-architecture/business-data-chains/epic-360-operations.md"
|
||||
|
||||
- alert: LjwxHealthRealtimeScoreCalibrationIncomplete
|
||||
expr: |
|
||||
health_realtime_score_reconciliation_states{namespace="ljwx-health",service="ljwx-boot"} > 0
|
||||
and
|
||||
health_realtime_score_reconciliation_coverage_ratio{namespace="ljwx-health",service="ljwx-boot"} < 1
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
category: health-score
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "Realtime health-score calibration coverage is incomplete"
|
||||
description: "Tenant {{ $labels.customer }} has realtime states without matching overnight ground truth."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/02-architecture/business-data-chains/epic-360-operations.md"
|
||||
|
||||
- alert: LjwxHealthRealtimeScoreCalibrationDeviation
|
||||
expr: |
|
||||
health_realtime_score_reconciliation_exceeds{namespace="ljwx-health",service="ljwx-boot"} > 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: health-score
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "Realtime health-score calibration deviation exceeded the threshold"
|
||||
description: "Tenant {{ $labels.customer }} has users whose realtime score differs materially from overnight ground truth."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/02-architecture/business-data-chains/epic-360-operations.md"
|
||||
|
||||
- name: ljwx_health_edge_connectivity
|
||||
interval: 30s
|
||||
rules:
|
||||
- alert: LjwxHealthCloudflaredUnavailable
|
||||
expr: kube_deployment_status_replicas_available{namespace="ljwx-health",deployment="ljwx-health-cloudflared"} < 1
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
category: edge
|
||||
annotations:
|
||||
summary: "LJWX Health Cloudflare Tunnel is unavailable"
|
||||
description: "No in-cluster cloudflared connector has been available for two minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/edge-tunnel.md"
|
||||
|
||||
- alert: LjwxHealthCloudflaredRedundancyLost
|
||||
expr: sum(cloudflared_tunnel_ha_connections{namespace="ljwx-health"}) < 2
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: edge
|
||||
annotations:
|
||||
summary: "LJWX Health Cloudflare Tunnel lost connector redundancy"
|
||||
description: "Fewer than two Cloudflare edge connections have been active for five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/edge-tunnel.md"
|
||||
|
||||
- alert: LjwxHealthCloudflaredMetricsMissing
|
||||
expr: absent(up{namespace="ljwx-health",service="ljwx-health-cloudflared"})
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: observability
|
||||
annotations:
|
||||
summary: "LJWX Health Cloudflare Tunnel metrics are missing"
|
||||
description: "Prometheus has not discovered cloudflared metrics for five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/edge-tunnel.md"
|
||||
|
||||
- name: ljwx_health_message_chain
|
||||
interval: 30s
|
||||
rules:
|
||||
- alert: LjwxHealthMessageOutboxBacklog
|
||||
expr: max(ljwx_message_outbox_queue_size{namespace="ljwx-health",service="ljwx-boot"}) > 1000
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
category: message
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "LJWX Health message outbox backlog is high"
|
||||
description: "The message outbox has contained more than 1000 pending events for ten minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- alert: LjwxHealthMessageOutboxDeliveryFailureRate
|
||||
expr: |
|
||||
sum(rate(ljwx_message_outbox_delivery_total{namespace="ljwx-health",service="ljwx-boot",status="failure"}[5m]))
|
||||
/
|
||||
clamp_min(sum(rate(ljwx_message_outbox_delivery_total{namespace="ljwx-health",service="ljwx-boot"}[5m])), 0.001) > 0.05
|
||||
and
|
||||
sum(rate(ljwx_message_outbox_delivery_total{namespace="ljwx-health",service="ljwx-boot"}[5m])) > 0
|
||||
for: 10m
|
||||
labels:
|
||||
severity: critical
|
||||
category: message
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "LJWX Health message outbox delivery is failing"
|
||||
description: "More than 5% of message outbox deliveries have failed for ten minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- alert: LjwxHealthMessageResponseLatencyHigh
|
||||
expr: |
|
||||
histogram_quantile(
|
||||
0.95,
|
||||
sum(rate(ljwx_message_response_latency_seconds_bucket{namespace="ljwx-health",service="ljwx-boot"}[10m])) by (le)
|
||||
) > 300
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
category: message
|
||||
service: ljwx-boot
|
||||
annotations:
|
||||
summary: "LJWX Health message response P95 is above five minutes"
|
||||
description: "The send-to-response P95 has remained above 300 seconds for fifteen minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/service-degradation.md"
|
||||
|
||||
- name: ljwx_health_storage_capacity
|
||||
interval: 30s
|
||||
rules:
|
||||
- alert: LjwxHealthArchiveVerificationFailed
|
||||
expr: sum(increase(health_archive_failed_total{namespace="ljwx-health"}[15m])) > 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
category: storage
|
||||
pilot_action: stop
|
||||
annotations:
|
||||
summary: "Health archive verification or retention inspection failed"
|
||||
description: "At least one archive or retention stage failed in the last fifteen minutes. Stop the full-collection pilot until the object and manifest are verified."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/storage-capacity-gate.md"
|
||||
|
||||
- alert: LjwxHealthArchiveDeleteBlocked
|
||||
expr: sum(increase(health_archive_delete_blocked_total{namespace="ljwx-health"}[15m])) > 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: warning
|
||||
category: storage
|
||||
pilot_action: hold
|
||||
annotations:
|
||||
summary: "Health retention deletion was blocked by the verified-manifest gate"
|
||||
description: "Retention found rows without a matching VERIFIED archive manifest. Keep deletion disabled and investigate the archive range."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/storage-capacity-gate.md"
|
||||
|
||||
- alert: LjwxHealthArchiveBacklogHigh
|
||||
expr: max(health_archive_pending_rows{namespace="ljwx-health"}) > 100000
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
category: storage
|
||||
pilot_action: hold
|
||||
annotations:
|
||||
summary: "Health archive lifecycle backlog exceeds 100k rows"
|
||||
description: "Eligible health rows have remained above the pilot capacity threshold for fifteen minutes. Do not expand the pilot."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/storage-capacity-gate.md"
|
||||
|
||||
- alert: LjwxHealthOlapLagStopThreshold
|
||||
expr: max(health_olap_recon_lag_seconds{namespace="ljwx-health"}) > 300
|
||||
for: 15m
|
||||
labels:
|
||||
severity: critical
|
||||
category: storage
|
||||
pilot_action: stop
|
||||
annotations:
|
||||
summary: "Health OLAP reconciliation lag exceeds five minutes"
|
||||
description: "The RFC stop threshold has been exceeded for fifteen minutes. Disable the tenant pilot flag and return devices to the 60-second legacy policy."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/storage-capacity-gate.md"
|
||||
|
||||
- alert: LjwxHealthPostgresDeadlockDetected
|
||||
expr: sum(increase(pg_stat_database_deadlocks{datname="ljwx_health"}[15m])) > 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
category: storage
|
||||
pilot_action: stop
|
||||
annotations:
|
||||
summary: "PostgreSQL recorded a deadlock during the pilot window"
|
||||
description: "Any database deadlock fails the #375 capacity gate. Stop burst collection and capture blocking-session evidence."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/storage-capacity-gate.md"
|
||||
|
||||
- alert: LjwxHealthStorageVolumeHigh
|
||||
expr: |
|
||||
1 - (
|
||||
kubelet_volume_stats_available_bytes{namespace=~"infra|shared-base|ljwx-health",persistentvolumeclaim=~".*(postgres|timescaledb|kafka|redpanda|redis|minio|clickhouse).*"}
|
||||
/
|
||||
clamp_min(kubelet_volume_stats_capacity_bytes{namespace=~"infra|shared-base|ljwx-health",persistentvolumeclaim=~".*(postgres|timescaledb|kafka|redpanda|redis|minio|clickhouse).*"}, 1)
|
||||
) > 0.80
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
category: storage
|
||||
pilot_action: hold
|
||||
annotations:
|
||||
summary: "A health data platform volume is above 80%"
|
||||
description: "{{ $labels.persistentvolumeclaim }} has crossed the expansion hold threshold."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/storage-capacity-gate.md"
|
||||
|
||||
- alert: LjwxHealthStorageVolumeCritical
|
||||
expr: |
|
||||
1 - (
|
||||
kubelet_volume_stats_available_bytes{namespace=~"infra|shared-base|ljwx-health",persistentvolumeclaim=~".*(postgres|timescaledb|kafka|redpanda|redis|minio|clickhouse).*"}
|
||||
/
|
||||
clamp_min(kubelet_volume_stats_capacity_bytes{namespace=~"infra|shared-base|ljwx-health",persistentvolumeclaim=~".*(postgres|timescaledb|kafka|redpanda|redis|minio|clickhouse).*"}, 1)
|
||||
) > 0.90
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
category: storage
|
||||
pilot_action: stop
|
||||
annotations:
|
||||
summary: "A health data platform volume is above 90%"
|
||||
description: "{{ $labels.persistentvolumeclaim }} has crossed the pilot stop threshold."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/storage-capacity-gate.md"
|
||||
|
||||
- name: ljwx_health_bigscreen_trial
|
||||
interval: 30s
|
||||
rules:
|
||||
- record: ljwx_health:bigscreen_bff_qps:5m
|
||||
expr: sum by (tenant) (rate(bigscreen_bff_requests_total{namespace="ljwx-health",service="ljwx-boot",endpoint=~"snapshot|realtime"}[5m]))
|
||||
|
||||
- record: ljwx_health:bigscreen_bff_success:ratio5m
|
||||
expr: |
|
||||
sum by (tenant) (rate(bigscreen_bff_requests_total{namespace="ljwx-health",service="ljwx-boot",endpoint=~"snapshot|realtime",result=~"success|not_modified"}[5m]))
|
||||
/
|
||||
clamp_min(sum by (tenant) (rate(bigscreen_bff_requests_total{namespace="ljwx-health",service="ljwx-boot",endpoint=~"snapshot|realtime"}[5m])), 0.001)
|
||||
|
||||
- record: ljwx_health:bigscreen_bff_latency_p95:5m
|
||||
expr: |
|
||||
histogram_quantile(
|
||||
0.95,
|
||||
sum by (tenant, le) (rate(bigscreen_bff_request_latency_seconds_bucket{namespace="ljwx-health",service="ljwx-boot",endpoint=~"snapshot|realtime"}[5m]))
|
||||
)
|
||||
|
||||
- record: ljwx_health:bigscreen_bff_latency_p99:5m
|
||||
expr: |
|
||||
histogram_quantile(
|
||||
0.99,
|
||||
sum by (tenant, le) (rate(bigscreen_bff_request_latency_seconds_bucket{namespace="ljwx-health",service="ljwx-boot",endpoint=~"snapshot|realtime"}[5m]))
|
||||
)
|
||||
|
||||
- record: ljwx_health:bigscreen_cache_hit:ratio5m
|
||||
expr: |
|
||||
sum by (tenant) (rate(bigscreen_snapshot_cache_access_total{namespace="ljwx-health",service="ljwx-boot",result="hit"}[5m]))
|
||||
/
|
||||
clamp_min(sum by (tenant) (rate(bigscreen_snapshot_cache_access_total{namespace="ljwx-health",service="ljwx-boot",result=~"hit|miss"}[5m])), 0.001)
|
||||
|
||||
- record: ljwx_health:bigscreen_snapshot_304:ratio5m
|
||||
expr: |
|
||||
sum by (tenant) (rate(bigscreen_bff_requests_total{namespace="ljwx-health",service="ljwx-boot",endpoint="snapshot",result="not_modified"}[5m]))
|
||||
/
|
||||
clamp_min(sum by (tenant) (rate(bigscreen_bff_requests_total{namespace="ljwx-health",service="ljwx-boot",endpoint="snapshot"}[5m])), 0.001)
|
||||
|
||||
- record: ljwx_health:bigscreen_snapshot_build_p95:5m
|
||||
expr: |
|
||||
histogram_quantile(
|
||||
0.95,
|
||||
sum by (tenant, le) (rate(bigscreen_snapshot_build_latency_seconds_bucket{namespace="ljwx-health",service="ljwx-boot"}[5m]))
|
||||
)
|
||||
|
||||
- record: ljwx_health:bigscreen_kiosk_refresh_success:ratio5m
|
||||
expr: |
|
||||
sum by (tenant) (rate(bigscreen_kiosk_refresh_total{namespace="ljwx-health",service="ljwx-boot",result=~"success|not_modified"}[5m]))
|
||||
/
|
||||
clamp_min(sum by (tenant) (rate(bigscreen_kiosk_refresh_total{namespace="ljwx-health",service="ljwx-boot"}[5m])), 0.001)
|
||||
|
||||
- record: ljwx_health:bigscreen_kiosk_update_delay_p95:5m
|
||||
expr: |
|
||||
histogram_quantile(
|
||||
0.95,
|
||||
sum by (tenant, le) (rate(bigscreen_kiosk_last_update_delay_seconds_bucket{namespace="ljwx-health",service="ljwx-boot",result="success"}[5m]))
|
||||
)
|
||||
|
||||
- record: ljwx_health:bigscreen_kiosk_recovery_p95:1h
|
||||
expr: |
|
||||
histogram_quantile(
|
||||
0.95,
|
||||
sum by (tenant, le) (rate(bigscreen_kiosk_recovery_latency_seconds_bucket{namespace="ljwx-health",service="ljwx-boot"}[1h]))
|
||||
)
|
||||
|
||||
- record: ljwx_health:bigscreen_kiosk_heartbeat_age:seconds
|
||||
expr: time() - max by (tenant) (bigscreen_kiosk_last_heartbeat_timestamp{namespace="ljwx-health",service="ljwx-boot"})
|
||||
|
||||
- alert: LjwxHealthBigscreenBffSuccessLow
|
||||
expr: ljwx_health:bigscreen_bff_success:ratio5m < 0.995 and ljwx_health:bigscreen_bff_qps:5m > 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
category: bigscreen
|
||||
annotations:
|
||||
summary: "Bigscreen BFF success ratio is below 99.5%"
|
||||
description: "Tenant {{ $labels.tenant }} has missed the trial success target for five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/bigscreen-observability.md"
|
||||
|
||||
- alert: LjwxHealthBigscreenBffP95High
|
||||
expr: ljwx_health:bigscreen_bff_latency_p95:5m > 0.5
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: bigscreen
|
||||
annotations:
|
||||
summary: "Bigscreen BFF P95 latency is above 500ms"
|
||||
description: "Tenant {{ $labels.tenant }} has exceeded the P95 trial target for five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/bigscreen-observability.md"
|
||||
|
||||
- alert: LjwxHealthBigscreenBffP99High
|
||||
expr: ljwx_health:bigscreen_bff_latency_p99:5m > 1
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: bigscreen
|
||||
annotations:
|
||||
summary: "Bigscreen BFF P99 latency is above one second"
|
||||
description: "Tenant {{ $labels.tenant }} has exceeded the P99 trial target for five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/bigscreen-observability.md"
|
||||
|
||||
- alert: LjwxHealthBigscreenCacheHitLow
|
||||
expr: |
|
||||
ljwx_health:bigscreen_cache_hit:ratio5m < 0.70
|
||||
and
|
||||
sum by (tenant) (rate(bigscreen_snapshot_cache_access_total{namespace="ljwx-health",service="ljwx-boot",result=~"hit|miss"}[5m])) > 0
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
category: bigscreen
|
||||
annotations:
|
||||
summary: "Bigscreen snapshot cache hit ratio is below 70%"
|
||||
description: "Tenant {{ $labels.tenant }} has missed the Redis cache hit target for ten minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/bigscreen-observability.md"
|
||||
|
||||
- alert: LjwxHealthBigscreenSnapshot304Low
|
||||
expr: |
|
||||
ljwx_health:bigscreen_snapshot_304:ratio5m < 0.30
|
||||
and
|
||||
sum by (tenant) (rate(bigscreen_bff_requests_total{namespace="ljwx-health",service="ljwx-boot",endpoint="snapshot"}[5m])) > 0
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
category: bigscreen
|
||||
annotations:
|
||||
summary: "Bigscreen snapshot 304 ratio is below 30%"
|
||||
description: "Tenant {{ $labels.tenant }} has missed the HTTP cache reuse target for fifteen minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/bigscreen-observability.md"
|
||||
|
||||
- alert: LjwxHealthBigscreenKioskRefreshLow
|
||||
expr: |
|
||||
ljwx_health:bigscreen_kiosk_refresh_success:ratio5m < 0.995
|
||||
and
|
||||
sum by (tenant) (rate(bigscreen_kiosk_refresh_total{namespace="ljwx-health",service="ljwx-boot"}[5m])) > 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
category: bigscreen
|
||||
annotations:
|
||||
summary: "Bigscreen kiosk refresh success ratio is below 99.5%"
|
||||
description: "Tenant {{ $labels.tenant }} has missed the kiosk refresh target for five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/bigscreen-observability.md"
|
||||
|
||||
- alert: LjwxHealthBigscreenKioskUpdateDelayed
|
||||
expr: ljwx_health:bigscreen_kiosk_update_delay_p95:5m > 2
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: bigscreen
|
||||
annotations:
|
||||
summary: "Bigscreen kiosk last-update delay is above two seconds"
|
||||
description: "Tenant {{ $labels.tenant }} has exceeded the generatedAt-to-render target for five minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/bigscreen-observability.md"
|
||||
|
||||
- alert: LjwxHealthBigscreenKioskJsError
|
||||
expr: sum by (tenant) (increase(bigscreen_kiosk_js_errors_total{namespace="ljwx-health",service="ljwx-boot"}[15m])) > 0
|
||||
labels:
|
||||
severity: critical
|
||||
category: bigscreen
|
||||
annotations:
|
||||
summary: "Bigscreen kiosk reported a JavaScript error"
|
||||
description: "Tenant {{ $labels.tenant }} reported at least one JavaScript error in fifteen minutes."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/bigscreen-observability.md"
|
||||
|
||||
- alert: LjwxHealthBigscreenKioskContinuousOffline
|
||||
expr: |
|
||||
ljwx_health:bigscreen_kiosk_heartbeat_age:seconds > 300
|
||||
or
|
||||
max by (tenant) (bigscreen_kiosk_browser_online{namespace="ljwx-health",service="ljwx-boot"}) == 0
|
||||
or
|
||||
absent(bigscreen_kiosk_last_heartbeat_timestamp{namespace="ljwx-health",service="ljwx-boot",tenant="1"})
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
category: bigscreen
|
||||
annotations:
|
||||
summary: "Bigscreen kiosk has been continuously offline"
|
||||
description: "Tenant {{ $labels.tenant }} has no fresh online heartbeat."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/bigscreen-observability.md"
|
||||
|
||||
- alert: LjwxHealthBigscreenKioskRecoverySlow
|
||||
expr: ljwx_health:bigscreen_kiosk_recovery_p95:1h > 30
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
category: bigscreen
|
||||
annotations:
|
||||
summary: "Bigscreen kiosk recovery P95 is above 30 seconds"
|
||||
description: "Tenant {{ $labels.tenant }} has missed the offline recovery target."
|
||||
runbook_url: "https://github.com/lingjingwanxiang/ljwx-health/blob/main/docs/04-operations/runbooks/bigscreen-observability.md"
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# Provision the restricted PostgreSQL runtime role without printing its secret.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="${NAMESPACE:-ljwx-health}"
|
||||
SECRET_NAME="${SECRET_NAME:-ljwx-health-secret}"
|
||||
POSTGRES_NAMESPACE="${POSTGRES_NAMESPACE:-infra}"
|
||||
POSTGRES_POD="${POSTGRES_POD:-postgres-infra-postgresql-0}"
|
||||
POSTGRES_DATABASE="${POSTGRES_DATABASE:-ljwx_health}"
|
||||
APP_ROLE="${APP_ROLE:-ljwx_app}"
|
||||
DB_HOST="${DB_HOST:-postgres-lb.infra.svc.cluster.local}"
|
||||
DB_PORT="${DB_PORT:-5432}"
|
||||
|
||||
if [[ ! "${APP_ROLE}" =~ ^[a-z_][a-z0-9_]*$ ]]; then
|
||||
echo "APP_ROLE must be a lowercase PostgreSQL identifier" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! kubectl -n "${NAMESPACE}" get secret "${SECRET_NAME}" >/dev/null 2>&1; then
|
||||
echo "missing secret ${NAMESPACE}/${SECRET_NAME}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
app_password="${DB_APP_PASSWORD:-}"
|
||||
if [[ -z "${app_password}" ]]; then
|
||||
app_password="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')"
|
||||
fi
|
||||
if [[ ! "${app_password}" =~ ^[0-9A-Za-z._~-]{32,128}$ ]]; then
|
||||
echo "DB_APP_PASSWORD must contain 32-128 URL-safe characters" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The generated alphabet needs no SQL escaping. Feed the DDL through stdin so
|
||||
# the password does not appear in argv, terminal output, or shell history.
|
||||
printf "ALTER ROLE %s LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS PASSWORD '%s';\n" \
|
||||
"${APP_ROLE}" "${app_password}" \
|
||||
| kubectl -n "${POSTGRES_NAMESPACE}" exec -i "${POSTGRES_POD}" -- \
|
||||
psql -U postgres -d "${POSTGRES_DATABASE}" -v ON_ERROR_STOP=1 -f - >/dev/null
|
||||
|
||||
# Prove TCP password authentication and role flags before publishing the key.
|
||||
actual_flags="$({ printf '%s\n' "${app_password}"; } \
|
||||
| kubectl -n "${POSTGRES_NAMESPACE}" exec -i "${POSTGRES_POD}" -- sh -ec '
|
||||
IFS= read -r PGPASSWORD
|
||||
export PGPASSWORD
|
||||
psql --host="$1" --port="$2" --username="$3" --dbname="$4" \
|
||||
--no-password --tuples-only --no-align \
|
||||
--command="SELECT current_user || '\''|'\'' || rolsuper || '\''|'\'' || rolbypassrls FROM pg_roles WHERE rolname = current_user"
|
||||
' sh "${DB_HOST}" "${DB_PORT}" "${APP_ROLE}" "${POSTGRES_DATABASE}")"
|
||||
if [[ "${actual_flags}" != "${APP_ROLE}|false|false" ]]; then
|
||||
echo "restricted-role verification failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Preserve every existing Secret key and merge only DB_APP_PASSWORD. The patch
|
||||
# travels over stdin, so neither the plaintext nor base64 value appears in argv.
|
||||
DB_APP_PASSWORD_B64="$(printf '%s' "${app_password}" | base64 | tr -d '\n')"
|
||||
printf '{"data":{"DB_APP_PASSWORD":"%s"}}\n' "${DB_APP_PASSWORD_B64}" \
|
||||
| kubectl -n "${NAMESPACE}" patch secret "${SECRET_NAME}" \
|
||||
--type=merge --patch-file=/dev/stdin >/dev/null
|
||||
unset DB_APP_PASSWORD_B64
|
||||
unset app_password
|
||||
|
||||
echo "provisioned ${APP_ROLE}: LOGIN/NOSUPERUSER/NOBYPASSRLS; secret key updated without disclosure"
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rotate only the Sa-Token JWT signing key without reading or rewriting other Secret data.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="${NAMESPACE:-ljwx-health}"
|
||||
SECRET_NAME="${SECRET_NAME:-ljwx-health-secret}"
|
||||
|
||||
if ! kubectl get namespace "${NAMESPACE}" >/dev/null 2>&1; then
|
||||
echo "namespace ${NAMESPACE} does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! kubectl get secret "${SECRET_NAME}" -n "${NAMESPACE}" >/dev/null 2>&1; then
|
||||
echo "secret ${NAMESPACE}/${SECRET_NAME} does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${SA_TOKEN_JWT_SECRET:-}" ]]; then
|
||||
SA_TOKEN_JWT_SECRET="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||||
fi
|
||||
if [[ "${#SA_TOKEN_JWT_SECRET}" -lt 32 ]]; then
|
||||
echo "SA_TOKEN_JWT_SECRET must contain at least 32 characters" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
encoded="$(printf '%s' "${SA_TOKEN_JWT_SECRET}" | base64 | tr -d '\n')"
|
||||
printf '{"data":{"SA_TOKEN_JWT_SECRET":"%s"}}' "${encoded}" \
|
||||
| kubectl patch secret "${SECRET_NAME}" -n "${NAMESPACE}" --type=merge --patch-file=/dev/stdin >/dev/null
|
||||
|
||||
unset encoded SA_TOKEN_JWT_SECRET
|
||||
echo "rotated ${NAMESPACE}/${SECRET_NAME} key SA_TOKEN_JWT_SECRET (value withheld)"
|
||||
echo "existing Sa-Token sessions become invalid after ljwx-boot rolls out"
|
||||
@@ -11,7 +11,7 @@ spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: 5001
|
||||
port: 5225
|
||||
targetPort: 5225
|
||||
protocol: TCP
|
||||
selector:
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: health-writer
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
app: health-writer
|
||||
component: health-shadow
|
||||
spec:
|
||||
selector:
|
||||
app: health-writer
|
||||
component: health-shadow
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: health-recon
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
app: health-recon
|
||||
component: health-shadow
|
||||
spec:
|
||||
selector:
|
||||
app: health-recon
|
||||
component: health-shadow
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: ljwx-health-shadow
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
release: monitoring
|
||||
app.kubernetes.io/name: ljwx-health
|
||||
app.kubernetes.io/component: health-shadow
|
||||
spec:
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- ljwx-health
|
||||
selector:
|
||||
matchLabels:
|
||||
component: health-shadow
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /actuator/prometheus
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
relabelings:
|
||||
- sourceLabels: [__meta_kubernetes_namespace]
|
||||
targetLabel: namespace
|
||||
- sourceLabels: [__meta_kubernetes_service_name]
|
||||
targetLabel: service
|
||||
- sourceLabels: [__meta_kubernetes_pod_name]
|
||||
targetLabel: pod
|
||||
@@ -0,0 +1,30 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: ljwx-health-boot
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
release: monitoring
|
||||
app.kubernetes.io/name: ljwx-health
|
||||
app.kubernetes.io/component: backend
|
||||
spec:
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- ljwx-health
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ljwx-boot
|
||||
component: backend
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /actuator/prometheus
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
honorLabels: false
|
||||
relabelings:
|
||||
- sourceLabels: [__meta_kubernetes_namespace]
|
||||
targetLabel: namespace
|
||||
- sourceLabels: [__meta_kubernetes_service_name]
|
||||
targetLabel: service
|
||||
- sourceLabels: [__meta_kubernetes_pod_name]
|
||||
targetLabel: pod
|
||||
@@ -0,0 +1,61 @@
|
||||
CREATE DATABASE IF NOT EXISTS ljwx_health;
|
||||
USE ljwx_health;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS t_health_raw (
|
||||
event_id String,
|
||||
customer_id UInt64,
|
||||
org_id UInt64,
|
||||
user_id UInt64,
|
||||
device_sn String,
|
||||
ts DateTime64(3, 'Asia/Shanghai'),
|
||||
heart_rate Nullable(Int16),
|
||||
blood_oxygen Nullable(Int16),
|
||||
temperature Nullable(Float64),
|
||||
pressure_high Nullable(Int16),
|
||||
pressure_low Nullable(Int16),
|
||||
stress Nullable(Int16),
|
||||
step Nullable(Int32),
|
||||
distance Nullable(Float64),
|
||||
calorie Nullable(Float64),
|
||||
version UInt64 DEFAULT 1,
|
||||
vendor LowCardinality(String) DEFAULT '',
|
||||
source_adapter LowCardinality(String) DEFAULT '',
|
||||
precision_tier LowCardinality(String) DEFAULT '',
|
||||
ingested_at DateTime64(3, 'UTC') DEFAULT now64(3)
|
||||
)
|
||||
ENGINE = ReplacingMergeTree(version)
|
||||
PARTITION BY toYYYYMM(ts)
|
||||
ORDER BY (customer_id, event_id)
|
||||
TTL toDateTime(ts) + INTERVAL 365 DAY DELETE;
|
||||
|
||||
ALTER TABLE t_health_raw ADD COLUMN IF NOT EXISTS customer_id UInt64 AFTER event_id;
|
||||
ALTER TABLE t_health_raw ADD COLUMN IF NOT EXISTS ingested_at DateTime64(3, 'UTC') DEFAULT now64(3);
|
||||
ALTER TABLE t_health_raw ADD COLUMN IF NOT EXISTS vendor LowCardinality(String) DEFAULT '';
|
||||
ALTER TABLE t_health_raw ADD COLUMN IF NOT EXISTS source_adapter LowCardinality(String) DEFAULT '';
|
||||
ALTER TABLE t_health_raw ADD COLUMN IF NOT EXISTS precision_tier LowCardinality(String) DEFAULT '';
|
||||
ALTER TABLE t_health_raw MODIFY COLUMN ts DateTime64(3, 'Asia/Shanghai');
|
||||
|
||||
-- ClickHouse 24.3 does not support CREATE OR REPLACE VIEW. Rebuild this
|
||||
-- derived, stateless view idempotently while preserving the raw fact table.
|
||||
DROP VIEW IF EXISTS t_health_raw_business;
|
||||
CREATE VIEW t_health_raw_business AS
|
||||
SELECT
|
||||
customer_id,
|
||||
event_id,
|
||||
argMax(org_id, version) AS org_id,
|
||||
argMax(user_id, version) AS user_id,
|
||||
argMax(device_sn, version) AS device_sn,
|
||||
argMax(ts, version) AS ts,
|
||||
argMax(heart_rate, version) AS heart_rate,
|
||||
argMax(blood_oxygen, version) AS blood_oxygen,
|
||||
argMax(temperature, version) AS temperature,
|
||||
argMax(pressure_high, version) AS pressure_high,
|
||||
argMax(pressure_low, version) AS pressure_low,
|
||||
argMax(stress, version) AS stress,
|
||||
argMax(step, version) AS step,
|
||||
argMax(distance, version) AS distance,
|
||||
argMax(calorie, version) AS calorie,
|
||||
max(version) AS latest_version,
|
||||
max(ingested_at) AS ingested_at
|
||||
FROM t_health_raw
|
||||
GROUP BY customer_id, event_id;
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
# Copy protected ClickHouse credentials without decoding or printing them.
|
||||
|
||||
set -euo pipefail
|
||||
set +x
|
||||
|
||||
SOURCE_NAMESPACE="${SOURCE_NAMESPACE:-infra}"
|
||||
SOURCE_SECRET="${SOURCE_SECRET:-clickhouse-secret}"
|
||||
TARGET_NAMESPACE="${TARGET_NAMESPACE:-ljwx-health}"
|
||||
TARGET_SECRET="${TARGET_SECRET:-ljwx-health-clickhouse-secret}"
|
||||
KUBECTL="${KUBECTL:-kubectl}"
|
||||
|
||||
command -v "$KUBECTL" >/dev/null 2>&1 || {
|
||||
echo "kubectl executable not found: $KUBECTL" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v jq >/dev/null 2>&1 || {
|
||||
echo "jq is required" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
source_json="$($KUBECTL -n "$SOURCE_NAMESPACE" get secret "$SOURCE_SECRET" -o json)"
|
||||
source_user="$(printf '%s' "$source_json" | jq -r '.data.username // empty')"
|
||||
source_password="$(printf '%s' "$source_json" | jq -r '.data.password // empty')"
|
||||
if [ -z "$source_user" ] || [ -z "$source_password" ]; then
|
||||
echo "ClickHouse source Secret is missing username/password" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
target_patch="$(jq -cn \
|
||||
--arg user "$source_user" \
|
||||
--arg password "$source_password" \
|
||||
'{data:{CLICKHOUSE_USER:$user,CLICKHOUSE_PASSWORD:$password}}')"
|
||||
|
||||
if $KUBECTL -n "$TARGET_NAMESPACE" get secret "$TARGET_SECRET" >/dev/null 2>&1; then
|
||||
printf '%s' "$target_patch" | $KUBECTL -n "$TARGET_NAMESPACE" patch secret "$TARGET_SECRET" \
|
||||
--type=merge --patch-file=/dev/stdin >/dev/null
|
||||
else
|
||||
printf '%s' "$target_patch" | jq \
|
||||
--arg name "$TARGET_SECRET" \
|
||||
--arg namespace "$TARGET_NAMESPACE" \
|
||||
'{apiVersion:"v1",kind:"Secret",metadata:{name:$name,namespace:$namespace},type:"Opaque",data:.data}' \
|
||||
| $KUBECTL create -f - >/dev/null
|
||||
fi
|
||||
|
||||
$KUBECTL -n "$TARGET_NAMESPACE" annotate secret "$TARGET_SECRET" \
|
||||
kubectl.kubernetes.io/last-applied-configuration- >/dev/null 2>&1 || true
|
||||
|
||||
target_json="$($KUBECTL -n "$TARGET_NAMESPACE" get secret "$TARGET_SECRET" -o json)"
|
||||
target_user="$(printf '%s' "$target_json" | jq -r '.data.CLICKHOUSE_USER // empty')"
|
||||
target_password="$(printf '%s' "$target_json" | jq -r '.data.CLICKHOUSE_PASSWORD // empty')"
|
||||
if [ "$target_user" != "$source_user" ] || [ "$target_password" != "$source_password" ]; then
|
||||
echo "ClickHouse credential verification failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
unset source_json source_user source_password target_patch target_json target_user target_password
|
||||
echo "ClickHouse credentials synchronized and verified without disclosure"
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Align ljwx-health runtime OSS credentials with the in-cluster MinIO source Secret.
|
||||
|
||||
set -euo pipefail
|
||||
set +x
|
||||
|
||||
SOURCE_NAMESPACE="${SOURCE_NAMESPACE:-infra}"
|
||||
SOURCE_SECRET="${SOURCE_SECRET:-minio-credentials}"
|
||||
SOURCE_ACCESS_KEY="${SOURCE_ACCESS_KEY:-root-user}"
|
||||
SOURCE_SECRET_KEY="${SOURCE_SECRET_KEY:-root-password}"
|
||||
TARGET_NAMESPACE="${TARGET_NAMESPACE:-ljwx-health}"
|
||||
TARGET_SECRET="${TARGET_SECRET:-ljwx-health-secret}"
|
||||
CLEAN_SOURCE_LAST_APPLIED="${CLEAN_SOURCE_LAST_APPLIED:-true}"
|
||||
KUBECTL="${KUBECTL:-kubectl}"
|
||||
|
||||
command -v "$KUBECTL" >/dev/null 2>&1 || {
|
||||
echo "kubectl executable not found: $KUBECTL" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v jq >/dev/null 2>&1 || {
|
||||
echo "jq is required" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
source_json="$($KUBECTL -n "$SOURCE_NAMESPACE" get secret "$SOURCE_SECRET" -o json)"
|
||||
source_access="$(printf '%s' "$source_json" | jq -r --arg key "$SOURCE_ACCESS_KEY" '.data[$key] // empty')"
|
||||
source_secret="$(printf '%s' "$source_json" | jq -r --arg key "$SOURCE_SECRET_KEY" '.data[$key] // empty')"
|
||||
|
||||
if [ -z "$source_access" ] || [ -z "$source_secret" ]; then
|
||||
echo "MinIO source Secret is missing the configured credential keys" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
$KUBECTL -n "$TARGET_NAMESPACE" get secret "$TARGET_SECRET" >/dev/null
|
||||
|
||||
jq -cn \
|
||||
--arg access "$source_access" \
|
||||
--arg secret "$source_secret" \
|
||||
'{data:{OSS_ACCESS_KEY:$access,OSS_SECRET_KEY:$secret}}' \
|
||||
| $KUBECTL -n "$TARGET_NAMESPACE" patch secret "$TARGET_SECRET" \
|
||||
--type=merge --patch-file=/dev/stdin >/dev/null
|
||||
|
||||
# `kubectl apply` stores base64 Secret values in this annotation. Runtime Secrets
|
||||
# are intentionally managed by merge patch, so the annotation is both unnecessary
|
||||
# and an avoidable disclosure surface.
|
||||
$KUBECTL -n "$TARGET_NAMESPACE" annotate secret "$TARGET_SECRET" \
|
||||
kubectl.kubernetes.io/last-applied-configuration- >/dev/null 2>&1 || true
|
||||
if [ "$CLEAN_SOURCE_LAST_APPLIED" = "true" ]; then
|
||||
$KUBECTL -n "$SOURCE_NAMESPACE" annotate secret "$SOURCE_SECRET" \
|
||||
kubectl.kubernetes.io/last-applied-configuration- >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
target_json="$($KUBECTL -n "$TARGET_NAMESPACE" get secret "$TARGET_SECRET" -o json)"
|
||||
target_access="$(printf '%s' "$target_json" | jq -r '.data.OSS_ACCESS_KEY // empty')"
|
||||
target_secret="$(printf '%s' "$target_json" | jq -r '.data.OSS_SECRET_KEY // empty')"
|
||||
if [ "$target_access" != "$source_access" ] || [ "$target_secret" != "$source_secret" ]; then
|
||||
echo "MinIO credential verification failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
unset source_json source_access source_secret target_json target_access target_secret
|
||||
echo "MinIO credentials synchronized and verified without disclosure"
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ljwx-admin
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
ops.ljwx.io/migration-stage: contabo-canary
|
||||
spec:
|
||||
containers:
|
||||
- name: ljwx-admin
|
||||
env:
|
||||
- name: VITE_API_URL
|
||||
value: https://health-admin-contabo-canary.lingjingwanxiang.cn/api
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ljwx-bigscreen
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
ops.ljwx.io/migration-stage: contabo-canary
|
||||
spec:
|
||||
containers:
|
||||
- name: ljwx-bigscreen
|
||||
env:
|
||||
- name: BIGSCREEN_CUTOVER_ADMIN_BASE_URL
|
||||
value: https://health-dashboard-contabo-canary.lingjingwanxiang.cn
|
||||
@@ -0,0 +1,22 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ljwx-boot
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
ops.ljwx.io/migration-stage: contabo-canary
|
||||
spec:
|
||||
containers:
|
||||
- name: ljwx-boot
|
||||
env:
|
||||
- name: LJWX_HEALTH_OUTBOX_KAFKA_ENABLED
|
||||
value: "false"
|
||||
- name: LJWX_HEALTH_OUTBOX_KAFKA_ALLOWED_CUSTOMER_IDS
|
||||
value: ""
|
||||
- name: LJWX_HEALTH_REALTIME_SCORE_ENABLED
|
||||
value: "false"
|
||||
- name: LJWX_HEALTH_REALTIME_SCORE_CUSTOMERS
|
||||
value: ""
|
||||
@@ -0,0 +1,69 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ljwx-health-traefik
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
$patch: replace
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- health-admin-contabo-canary.lingjingwanxiang.cn
|
||||
- health-dashboard-contabo-canary.lingjingwanxiang.cn
|
||||
secretName: ljwx-health-contabo-canary-tls
|
||||
rules:
|
||||
- host: health-admin-contabo-canary.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-admin
|
||||
port:
|
||||
number: 80
|
||||
- host: health-dashboard-contabo-canary.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-admin
|
||||
port:
|
||||
number: 80
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ljwx-health-api-traefik
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
$patch: replace
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- health-admin-contabo-canary.lingjingwanxiang.cn
|
||||
- health-dashboard-contabo-canary.lingjingwanxiang.cn
|
||||
secretName: ljwx-health-contabo-canary-tls
|
||||
rules:
|
||||
- host: health-admin-contabo-canary.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
- host: health-dashboard-contabo-canary.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
@@ -0,0 +1,213 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: ljwx-health
|
||||
resources:
|
||||
- ../contabo-prod
|
||||
patches:
|
||||
- target:
|
||||
version: v1
|
||||
kind: ConfigMap
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-cloudflared-config
|
||||
patch: |-
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared-config
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: apps
|
||||
version: v1
|
||||
kind: Deployment
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-cloudflared
|
||||
patch: |-
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
version: v1
|
||||
kind: Service
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-cloudflared
|
||||
patch: |-
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: policy
|
||||
version: v1
|
||||
kind: PodDisruptionBudget
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-cloudflared
|
||||
patch: |-
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: monitoring.coreos.com
|
||||
version: v1
|
||||
kind: ServiceMonitor
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-cloudflared
|
||||
patch: |-
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: networking.k8s.io
|
||||
version: v1
|
||||
kind: NetworkPolicy
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-cloudflared
|
||||
patch: |-
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: batch
|
||||
version: v1
|
||||
kind: Job
|
||||
namespace: ljwx-health
|
||||
name: health-shadow-kafka-init
|
||||
patch: |-
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: health-shadow-kafka-init
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: batch
|
||||
version: v1
|
||||
kind: Job
|
||||
namespace: ljwx-health
|
||||
name: health-shadow-clickhouse-init
|
||||
patch: |-
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: health-shadow-clickhouse-init
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: apps
|
||||
version: v1
|
||||
kind: Deployment
|
||||
namespace: ljwx-health
|
||||
name: health-writer
|
||||
patch: |-
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: health-writer
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: apps
|
||||
version: v1
|
||||
kind: Deployment
|
||||
namespace: ljwx-health
|
||||
name: health-recon
|
||||
patch: |-
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: health-recon
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
version: v1
|
||||
kind: Service
|
||||
namespace: ljwx-health
|
||||
name: health-writer
|
||||
patch: |-
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: health-writer
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
version: v1
|
||||
kind: Service
|
||||
namespace: ljwx-health
|
||||
name: health-recon
|
||||
patch: |-
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: health-recon
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: monitoring.coreos.com
|
||||
version: v1
|
||||
kind: ServiceMonitor
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-shadow
|
||||
patch: |-
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: ljwx-health-shadow
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: monitoring.coreos.com
|
||||
version: v1
|
||||
kind: PrometheusRule
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-shadow
|
||||
patch: |-
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: ljwx-health-shadow
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: monitoring.coreos.com
|
||||
version: v1
|
||||
kind: PrometheusRule
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-slo
|
||||
patch: |-
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: ljwx-health-slo
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
version: v1
|
||||
kind: ConfigMap
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-shadow-clickhouse-schema
|
||||
patch: |-
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ljwx-health-shadow-clickhouse-schema
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- path: ingress-canary-patch.yaml
|
||||
- path: deployment-boot-staging.yaml
|
||||
- path: deployment-admin-staging.yaml
|
||||
- path: deployment-bigscreen-staging.yaml
|
||||
@@ -0,0 +1,22 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared-config
|
||||
data:
|
||||
config.yaml: |
|
||||
tunnel: 70479f3e-caaf-4528-b713-413cc27e8ad0
|
||||
credentials-file: /etc/cloudflared/credentials/credentials.json
|
||||
metrics: 0.0.0.0:2000
|
||||
loglevel: info
|
||||
ingress:
|
||||
- hostname: health-admin.omniverseai.net
|
||||
service: http://traefik.kube-system.svc.cluster.local:80
|
||||
originRequest:
|
||||
httpHostHeader: health-admin.omniverseai.net
|
||||
connectTimeout: 10s
|
||||
- hostname: health-dashboard.omniverseai.net
|
||||
service: http://traefik.kube-system.svc.cluster.local:80
|
||||
originRequest:
|
||||
httpHostHeader: health-dashboard.omniverseai.net
|
||||
connectTimeout: 10s
|
||||
- service: http_status:404
|
||||
@@ -0,0 +1,37 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: ljwx-health-cloudflared
|
||||
spec:
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 7844
|
||||
- protocol: UDP
|
||||
port: 7844
|
||||
@@ -0,0 +1,55 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ljwx-health-api-traefik
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: dnspod-letsencrypt
|
||||
traefik.ingress.kubernetes.io/router.middlewares: ljwx-health-health-api-chain@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- health-admin.lingjingwanxiang.cn
|
||||
- health-dashboard.lingjingwanxiang.cn
|
||||
secretName: ljwx-health-contabo-prod-tls
|
||||
rules:
|
||||
- host: health-admin.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
- host: health-dashboard.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
- host: health-admin.omniverseai.net
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
- host: health-dashboard.omniverseai.net
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-boot
|
||||
port:
|
||||
number: 9998
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: ljwx-health-ingress
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector: {}
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: infra
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: ingress-nginx
|
||||
app.kubernetes.io/component: controller
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: observability
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: prometheus
|
||||
@@ -0,0 +1,55 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ljwx-health-traefik
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: dnspod-letsencrypt
|
||||
traefik.ingress.kubernetes.io/router.middlewares: ljwx-health-health-cors@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- health-admin.lingjingwanxiang.cn
|
||||
- health-dashboard.lingjingwanxiang.cn
|
||||
secretName: ljwx-health-contabo-prod-tls
|
||||
rules:
|
||||
- host: health-admin.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-admin
|
||||
port:
|
||||
number: 80
|
||||
- host: health-dashboard.lingjingwanxiang.cn
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-admin
|
||||
port:
|
||||
number: 80
|
||||
- host: health-admin.omniverseai.net
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-admin
|
||||
port:
|
||||
number: 80
|
||||
- host: health-dashboard.omniverseai.net
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ljwx-admin
|
||||
port:
|
||||
number: 80
|
||||
@@ -0,0 +1,38 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: ljwx-health
|
||||
resources:
|
||||
- ../k3s-eu
|
||||
- middleware-traefik.yaml
|
||||
- ingress-traefik.yaml
|
||||
- ingress-api-traefik.yaml
|
||||
patches:
|
||||
- target:
|
||||
group: networking.k8s.io
|
||||
version: v1
|
||||
kind: Ingress
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-ingress
|
||||
patch: |-
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ljwx-health-ingress
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- target:
|
||||
group: networking.k8s.io
|
||||
version: v1
|
||||
kind: Ingress
|
||||
namespace: ljwx-health
|
||||
name: ljwx-health-api-ingress
|
||||
patch: |-
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ljwx-health-api-ingress
|
||||
namespace: ljwx-health
|
||||
$patch: delete
|
||||
- path: cloudflared-config-patch.yaml
|
||||
- path: cloudflared-networkpolicy-patch.yaml
|
||||
- path: ingress-networkpolicy-patch.yaml
|
||||
@@ -0,0 +1,52 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: health-cors
|
||||
spec:
|
||||
headers:
|
||||
accessControlAllowCredentials: true
|
||||
accessControlAllowHeaders:
|
||||
- DNT
|
||||
- X-CustomHeader
|
||||
- Keep-Alive
|
||||
- User-Agent
|
||||
- X-Requested-With
|
||||
- If-Modified-Since
|
||||
- Cache-Control
|
||||
- Content-Type
|
||||
- Authorization
|
||||
accessControlAllowMethods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
accessControlAllowOriginList:
|
||||
- https://health-admin.lingjingwanxiang.cn
|
||||
- https://health-dashboard.lingjingwanxiang.cn
|
||||
- https://health-admin.omniverseai.net
|
||||
- https://health-dashboard.omniverseai.net
|
||||
- https://health-admin-contabo-canary.lingjingwanxiang.cn
|
||||
- https://health-dashboard-contabo-canary.lingjingwanxiang.cn
|
||||
accessControlMaxAge: 600
|
||||
addVaryHeader: true
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: health-strip-api
|
||||
spec:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- /api
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: health-api-chain
|
||||
spec:
|
||||
chain:
|
||||
middlewares:
|
||||
- name: health-strip-api
|
||||
- name: health-cors
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ljwx-bigscreen
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: ljwx-bigscreen
|
||||
env:
|
||||
- name: BIGSCREEN_CUTOVER_MODE
|
||||
value: allowlist
|
||||
- name: BIGSCREEN_CUTOVER_TENANTS
|
||||
value: "1"
|
||||
@@ -0,0 +1,30 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ljwx-boot
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
ops.ljwx.io/config-revision: health-shadow-realtime-score-tenant-1-v2
|
||||
spec:
|
||||
containers:
|
||||
- name: ljwx-boot
|
||||
env:
|
||||
- name: LJWX_HEALTH_OUTBOX_KAFKA_ENABLED
|
||||
value: "true"
|
||||
- name: LJWX_HEALTH_OUTBOX_KAFKA_ALLOWED_CUSTOMER_IDS
|
||||
value: "1"
|
||||
- name: LJWX_HEALTH_OUTBOX_KAFKA_ALLOW_ALL_TENANTS
|
||||
value: "false"
|
||||
- name: LJWX_HEALTH_OUTBOX_KAFKA_BOOTSTRAP_SERVERS
|
||||
value: kafka-cluster-kafka-bootstrap.shared-base.svc.cluster.local:9092
|
||||
- name: LJWX_HEALTH_REALTIME_SCORE_ENABLED
|
||||
value: "true"
|
||||
- name: LJWX_HEALTH_REALTIME_SCORE_CUSTOMERS
|
||||
value: "1"
|
||||
- name: LJWX_HEALTH_REALTIME_SCORE_ROLLOUT_PERCENTAGE
|
||||
value: "0"
|
||||
- name: LJWX_HEALTH_REALTIME_SCORE_FLUSH_INTERVAL_MS
|
||||
value: "60000"
|
||||
@@ -3,13 +3,28 @@ kind: Kustomization
|
||||
namespace: ljwx-health
|
||||
resources:
|
||||
- ../../base
|
||||
- ../../schema-bundle
|
||||
- schema-release.yaml
|
||||
patches:
|
||||
- path: deployment-bigscreen-cutover.yaml
|
||||
- path: deployment-boot-shadow-canary.yaml
|
||||
- path: schema-job-release-patch.yaml
|
||||
images:
|
||||
- name: harbor.omniverseai.net/ljwx-health/ljwx-boot
|
||||
newName: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-boot
|
||||
digest: sha256:898aa553fa95e59bc94c8918f8975c994cf657164c4b534563b89a5f365cd1bc
|
||||
newName: ghcr.io/lingjingwanxiang/ljwx-health/ljwx-boot
|
||||
digest: sha256:c9bcea82ef9da03b3315a5550cb4cbfa456f71d6267e492f304aa085cb7f50ba
|
||||
- name: harbor.omniverseai.net/ljwx-health/ljwx-admin
|
||||
newName: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-admin
|
||||
digest: sha256:901465c8d0fd55c802670d8e0a348f1eeca2d69b908dc21aaf124749ebc63540
|
||||
newName: ghcr.io/lingjingwanxiang/ljwx-health/ljwx-admin
|
||||
digest: sha256:f212cab3e2c4cfc9161443d90bfc637137686d62b8417074365b3f8dc623c8e3
|
||||
- name: harbor.omniverseai.net/ljwx-health/ljwx-bigscreen
|
||||
newName: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-bigscreen
|
||||
digest: sha256:1ff48cfec957bec053cf25a22f28edc58d0a229e4a4a86211ba3bc6f13eae753
|
||||
newName: ghcr.io/lingjingwanxiang/ljwx-health/ljwx-bigscreen
|
||||
digest: sha256:a3a5e2141d74380b0b85408b855692f0d61e56d108e18023e9551fed5aaf3657
|
||||
- name: harbor.omniverseai.net/ljwx-health/health-writer
|
||||
newName: ghcr.io/lingjingwanxiang/ljwx-health/health-writer
|
||||
digest: sha256:8bc97dd8af74fb768cf47b818c000f9d4b554f83871a0fb4bbf271c548ead6dd
|
||||
- name: harbor.omniverseai.net/ljwx-health/health-recon
|
||||
newName: ghcr.io/lingjingwanxiang/ljwx-health/health-recon
|
||||
digest: sha256:35cd383ff02f258fe6f81e924371fffc0be19b49bd711c5ecf703ce6e76071f2
|
||||
- name: ghcr.io/lingjingwanxiang/ljwx-health/schema-runner
|
||||
newName: ghcr.io/lingjingwanxiang/ljwx-health/schema-runner
|
||||
digest: sha256:0d077562a841fa881fa5f4454cea64defa3f345582f96b00c94b7adffdd6c11c
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
apiVersion: delivery.lingjingwanxiang.cn/v1alpha1
|
||||
kind: LJWXHealthPromotion
|
||||
receiptDigest: sha256:06ee4823163efd0681d042a94eea9958f0b50ea17f0d6517b25b20d65620e2b3
|
||||
source:
|
||||
repository: lingjingwanxiang/ljwx-health
|
||||
sha: ee2e655bd137cf83e5f43c638009ed1a3a0fc839
|
||||
tag: ljwx-health-release-ee2e655bd137cf83e5f43c638009ed1a3a0fc839-run-30660382221-attempt-1
|
||||
manifestDigest: sha256:4cb7ffe007a4f8944c910b415e667aa474e792a56e1eb3438bb7db94cf2abe68
|
||||
releaseManifest:
|
||||
contentDigest: sha256:4cb7ffe007a4f8944c910b415e667aa474e792a56e1eb3438bb7db94cf2abe68
|
||||
ociRef: ghcr.io/lingjingwanxiang/ljwx-health/release-manifest@sha256:6b47536d82a9b4a821aa34968ad6f98dd709f50baeb7649465923d563d1ecf55
|
||||
ociDigest: sha256:6b47536d82a9b4a821aa34968ad6f98dd709f50baeb7649465923d563d1ecf55
|
||||
schema:
|
||||
version: 3.1.72
|
||||
manifestDigest: sha256:e4d7195bd821fe6765234499fd313064ee2c591f7169d21a380c234b7c3f76d0
|
||||
bundleDigest: sha256:783efd481f9af5d6f9459f53ed20a5d8758ab96cfd25ce25b63814b6282cbfcd
|
||||
bundleMediaType: application/vnd.ljwx.health.schema.bundle.v2+tar+gzip
|
||||
expectedProductionFingerprint: sha256:565a8f1edf4b997ecfd66e6c3f06b33ebead4842a51d8fbb92d5b1f5f75d9ee8
|
||||
canonicalProductionFingerprint: sha256:3440ae5ba527de93c5f1dae245c87552250c891c36476ff43dfa267173e95c12
|
||||
adoption:
|
||||
catalogComparison:
|
||||
canonicalFingerprint: sha256:3440ae5ba527de93c5f1dae245c87552250c891c36476ff43dfa267173e95c12
|
||||
evidenceDigest: sha256:290e7fac704fc4b706ed1449b1e979d0a0395383e91817328a9f4b54dad4f079
|
||||
evidencePath: ljwx-boot/database/schema/adoption/production-k3s-eu-pg18.allowed-catalog-differences.json
|
||||
environment: ljwx-health-k3s-eu
|
||||
expectedPostFingerprint: sha256:565a8f1edf4b997ecfd66e6c3f06b33ebead4842a51d8fbb92d5b1f5f75d9ee8
|
||||
expectedPreFingerprint: sha256:565a8f1edf4b997ecfd66e6c3f06b33ebead4842a51d8fbb92d5b1f5f75d9ee8
|
||||
planDigest: sha256:f39b85d57258492ff0af6693268a9239af9cc6449ced1104673a006fa6c6e29e
|
||||
planPath: ljwx-boot/database/schema/adoption/production-k3s-eu-pg18.json
|
||||
profile: production-pg18
|
||||
ociRef: ghcr.io/lingjingwanxiang/ljwx-health/schema-bundle@sha256:26f4ef828dde53804b50cc1bf6d5f01491ae6d149ef0606e53358d99bc4ef06b
|
||||
ociDigest: sha256:26f4ef828dde53804b50cc1bf6d5f01491ae6d149ef0606e53358d99bc4ef06b
|
||||
runnerImageRef: ghcr.io/lingjingwanxiang/ljwx-health/schema-runner@sha256:0d077562a841fa881fa5f4454cea64defa3f345582f96b00c94b7adffdd6c11c
|
||||
signingIdentity: https://github.com/lingjingwanxiang/ljwx-workflow-templates/.github/workflows/release-ljwx-health.yml@fd260a24f92edef33f8b99c1047d5c8b80586223
|
||||
signingIssuer: https://token.actions.githubusercontent.com
|
||||
binding:
|
||||
predicateDigest: sha256:73fe489662e0c8c741ba6703d06d957c3908312805df1f596a1136792e68723a
|
||||
attestationDigest: sha256:1e4e5bfb9d168db75f5aa710dd9b999a9093dd0b0f102cbcbd190f4f7dd320f5
|
||||
build:
|
||||
runId: '30660382221'
|
||||
runAttempt: '1'
|
||||
components:
|
||||
- name: ljwx-boot
|
||||
image: ghcr.io/lingjingwanxiang/ljwx-health/ljwx-boot
|
||||
digest: sha256:c9bcea82ef9da03b3315a5550cb4cbfa456f71d6267e492f304aa085cb7f50ba
|
||||
imageRef: ghcr.io/lingjingwanxiang/ljwx-health/ljwx-boot@sha256:c9bcea82ef9da03b3315a5550cb4cbfa456f71d6267e492f304aa085cb7f50ba
|
||||
- name: health-writer
|
||||
image: ghcr.io/lingjingwanxiang/ljwx-health/health-writer
|
||||
digest: sha256:8bc97dd8af74fb768cf47b818c000f9d4b554f83871a0fb4bbf271c548ead6dd
|
||||
imageRef: ghcr.io/lingjingwanxiang/ljwx-health/health-writer@sha256:8bc97dd8af74fb768cf47b818c000f9d4b554f83871a0fb4bbf271c548ead6dd
|
||||
- name: health-recon
|
||||
image: ghcr.io/lingjingwanxiang/ljwx-health/health-recon
|
||||
digest: sha256:35cd383ff02f258fe6f81e924371fffc0be19b49bd711c5ecf703ce6e76071f2
|
||||
imageRef: ghcr.io/lingjingwanxiang/ljwx-health/health-recon@sha256:35cd383ff02f258fe6f81e924371fffc0be19b49bd711c5ecf703ce6e76071f2
|
||||
- name: ljwx-admin
|
||||
image: ghcr.io/lingjingwanxiang/ljwx-health/ljwx-admin
|
||||
digest: sha256:f212cab3e2c4cfc9161443d90bfc637137686d62b8417074365b3f8dc623c8e3
|
||||
imageRef: ghcr.io/lingjingwanxiang/ljwx-health/ljwx-admin@sha256:f212cab3e2c4cfc9161443d90bfc637137686d62b8417074365b3f8dc623c8e3
|
||||
- name: ljwx-bigscreen
|
||||
image: ghcr.io/lingjingwanxiang/ljwx-health/ljwx-bigscreen
|
||||
digest: sha256:a3a5e2141d74380b0b85408b855692f0d61e56d108e18023e9551fed5aaf3657
|
||||
imageRef: ghcr.io/lingjingwanxiang/ljwx-health/ljwx-bigscreen@sha256:a3a5e2141d74380b0b85408b855692f0d61e56d108e18023e9551fed5aaf3657
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: ljwx-health-schema-bundle
|
||||
namespace: ljwx-health
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: schema-bundle
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ljwx-health-config
|
||||
- configMapRef:
|
||||
name: ljwx-health-schema-release-783efd481f9af5d6
|
||||
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ljwx-health-schema-release-783efd481f9af5d6
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
|
||||
argocd.argoproj.io/sync-wave: '-3'
|
||||
labels:
|
||||
app.kubernetes.io/name: ljwx-health
|
||||
app.kubernetes.io/component: schema-release
|
||||
immutable: true
|
||||
data:
|
||||
sourceSha: ee2e655bd137cf83e5f43c638009ed1a3a0fc839
|
||||
releaseManifestDigest: sha256:4cb7ffe007a4f8944c910b415e667aa474e792a56e1eb3438bb7db94cf2abe68
|
||||
releaseManifestOciRef: ghcr.io/lingjingwanxiang/ljwx-health/release-manifest@sha256:6b47536d82a9b4a821aa34968ad6f98dd709f50baeb7649465923d563d1ecf55
|
||||
schemaVersion: 3.1.72
|
||||
schemaManifestDigest: sha256:e4d7195bd821fe6765234499fd313064ee2c591f7169d21a380c234b7c3f76d0
|
||||
schemaBundleDigest: sha256:783efd481f9af5d6f9459f53ed20a5d8758ab96cfd25ce25b63814b6282cbfcd
|
||||
schemaBundleMediaType: application/vnd.ljwx.health.schema.bundle.v2+tar+gzip
|
||||
schemaOciRef: ghcr.io/lingjingwanxiang/ljwx-health/schema-bundle@sha256:26f4ef828dde53804b50cc1bf6d5f01491ae6d149ef0606e53358d99bc4ef06b
|
||||
schemaRunnerImageRef: ghcr.io/lingjingwanxiang/ljwx-health/schema-runner@sha256:0d077562a841fa881fa5f4454cea64defa3f345582f96b00c94b7adffdd6c11c
|
||||
expectedProductionFingerprint: sha256:565a8f1edf4b997ecfd66e6c3f06b33ebead4842a51d8fbb92d5b1f5f75d9ee8
|
||||
canonicalProductionFingerprint: sha256:3440ae5ba527de93c5f1dae245c87552250c891c36476ff43dfa267173e95c12
|
||||
adoptionPlanPath: ljwx-boot/database/schema/adoption/production-k3s-eu-pg18.json
|
||||
adoptionPlanDigest: sha256:f39b85d57258492ff0af6693268a9239af9cc6449ced1104673a006fa6c6e29e
|
||||
adoptionEvidencePath: ljwx-boot/database/schema/adoption/production-k3s-eu-pg18.allowed-catalog-differences.json
|
||||
adoptionEvidenceDigest: sha256:290e7fac704fc4b706ed1449b1e979d0a0395383e91817328a9f4b54dad4f079
|
||||
adoptionExpectedPreFingerprint: sha256:565a8f1edf4b997ecfd66e6c3f06b33ebead4842a51d8fbb92d5b1f5f75d9ee8
|
||||
adoptionExpectedPostFingerprint: sha256:565a8f1edf4b997ecfd66e6c3f06b33ebead4842a51d8fbb92d5b1f5f75d9ee8
|
||||
signingIdentity: https://github.com/lingjingwanxiang/ljwx-workflow-templates/.github/workflows/release-ljwx-health.yml@fd260a24f92edef33f8b99c1047d5c8b80586223
|
||||
signingIssuer: https://token.actions.githubusercontent.com
|
||||
bindingPredicateDigest: sha256:73fe489662e0c8c741ba6703d06d957c3908312805df1f596a1136792e68723a
|
||||
bindingAttestationDigest: sha256:1e4e5bfb9d168db75f5aa710dd9b999a9093dd0b0f102cbcbd190f4f7dd320f5
|
||||
@@ -0,0 +1,374 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: ljwx-health-schema-bundle
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
app: ljwx-health
|
||||
component: schema-bundle
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
spec:
|
||||
backoffLimit: 0
|
||||
ttlSecondsAfterFinished: 604800
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ljwx-health
|
||||
component: schema-bundle
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
automountServiceAccountToken: false
|
||||
imagePullSecrets:
|
||||
- name: ghcr-pull
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
fsGroup: 10001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: schema-bundle
|
||||
image: ghcr.io/lingjingwanxiang/ljwx-health/schema-runner:unpromoted
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/bash
|
||||
- -ceu
|
||||
- |
|
||||
export HOME=/work/home
|
||||
export DOCKER_CONFIG=/registry
|
||||
mkdir -p "${HOME}" /work/release /work/schema /work/bundle
|
||||
|
||||
for name in \
|
||||
sourceSha releaseManifestDigest releaseManifestOciRef \
|
||||
schemaVersion schemaManifestDigest schemaBundleDigest \
|
||||
schemaBundleMediaType schemaOciRef schemaRunnerImageRef \
|
||||
expectedProductionFingerprint canonicalProductionFingerprint \
|
||||
adoptionPlanPath adoptionPlanDigest adoptionEvidencePath \
|
||||
adoptionEvidenceDigest adoptionExpectedPreFingerprint \
|
||||
adoptionExpectedPostFingerprint signingIdentity signingIssuer \
|
||||
bindingPredicateDigest bindingAttestationDigest; do
|
||||
value="$(printenv "${name}")"
|
||||
test -n "${value}"
|
||||
done
|
||||
case "${schemaRunnerImageRef}" in
|
||||
ghcr.io/lingjingwanxiang/ljwx-health/schema-runner@sha256:*) ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
|
||||
cosign verify \
|
||||
--certificate-identity "${signingIdentity}" \
|
||||
--certificate-oidc-issuer "${signingIssuer}" \
|
||||
"${releaseManifestOciRef}" \
|
||||
>/work/release-signature-verification.json
|
||||
oras pull "${releaseManifestOciRef}" --output /work/release
|
||||
python3 - "${releaseManifestDigest}" \
|
||||
/work/release/release-manifest-core.json <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
expected = sys.argv[1]
|
||||
payload = json.loads(Path(sys.argv[2]).read_text())
|
||||
actual = payload.pop("manifestDigest", None)
|
||||
canonical = json.dumps(
|
||||
payload,
|
||||
ensure_ascii=True,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
computed = "sha256:" + hashlib.sha256(canonical).hexdigest()
|
||||
if actual != expected or computed != expected:
|
||||
raise SystemExit(
|
||||
"signed Release core content digest is invalid"
|
||||
)
|
||||
PY
|
||||
|
||||
cosign verify \
|
||||
--certificate-identity "${signingIdentity}" \
|
||||
--certificate-oidc-issuer "${signingIssuer}" \
|
||||
"${schemaOciRef}" \
|
||||
>/work/schema-signature-verification.json
|
||||
cosign verify-attestation \
|
||||
--type custom \
|
||||
--certificate-identity "${signingIdentity}" \
|
||||
--certificate-oidc-issuer "${signingIssuer}" \
|
||||
"${schemaOciRef}" \
|
||||
>/work/schema-binding-verification.json
|
||||
cosign verify-attestation \
|
||||
--type spdxjson \
|
||||
--certificate-identity "${signingIdentity}" \
|
||||
--certificate-oidc-issuer "${signingIssuer}" \
|
||||
"${schemaOciRef}" \
|
||||
>/work/schema-sbom-verification.json
|
||||
cosign verify-attestation \
|
||||
--type slsaprovenance \
|
||||
--certificate-identity "${signingIdentity}" \
|
||||
--certificate-oidc-issuer "${signingIssuer}" \
|
||||
"${schemaOciRef}" \
|
||||
>/work/schema-provenance-verification.json
|
||||
|
||||
cat >/work/schema-release-binding.predicate.json <<JSON
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"kind": "LJWXHealthSchemaReleaseBinding",
|
||||
"schemaSubject": "${schemaOciRef}",
|
||||
"releaseManifest": {
|
||||
"contentDigest": "${releaseManifestDigest}",
|
||||
"ociRef": "${releaseManifestOciRef}",
|
||||
"ociDigest": "${releaseManifestOciRef##*@}"
|
||||
},
|
||||
"source": {
|
||||
"repository": "lingjingwanxiang/ljwx-health",
|
||||
"sha": "${sourceSha}"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
actual_predicate_digest="$(
|
||||
sha256sum /work/schema-release-binding.predicate.json \
|
||||
| awk '{print "sha256:" $1}'
|
||||
)"
|
||||
test "${actual_predicate_digest}" = "${bindingPredicateDigest}"
|
||||
python3 - /work/schema-release-binding.predicate.json \
|
||||
/work/schema-binding-verification.json \
|
||||
"${schemaOciRef##*@}" <<'PY'
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
expected = json.loads(Path(sys.argv[1]).read_text())
|
||||
raw = Path(sys.argv[2]).read_text().strip()
|
||||
digest = sys.argv[3].removeprefix("sha256:")
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
documents = parsed if isinstance(parsed, list) else [parsed]
|
||||
except json.JSONDecodeError:
|
||||
documents = [
|
||||
json.loads(line)
|
||||
for line in raw.splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
for document in documents:
|
||||
encoded = document.get("payload")
|
||||
if not isinstance(encoded, str):
|
||||
continue
|
||||
statement = json.loads(base64.b64decode(encoded))
|
||||
subjects = statement.get("subject", [])
|
||||
predicate = statement.get("predicate")
|
||||
if (
|
||||
isinstance(predicate, dict)
|
||||
and isinstance(predicate.get("Data"), str)
|
||||
):
|
||||
predicate = json.loads(predicate["Data"])
|
||||
if (
|
||||
any(
|
||||
item.get("digest", {}).get("sha256") == digest
|
||||
for item in subjects
|
||||
if isinstance(item, dict)
|
||||
)
|
||||
and predicate == expected
|
||||
):
|
||||
break
|
||||
else:
|
||||
raise SystemExit(
|
||||
"verified schema attestation does not bind this Release"
|
||||
)
|
||||
PY
|
||||
|
||||
oras pull "${schemaOciRef}" --output /work/schema
|
||||
actual_bundle_digest="$(
|
||||
sha256sum /work/schema/ljwx-health-schema-bundle.tar.gz \
|
||||
| awk '{print "sha256:" $1}'
|
||||
)"
|
||||
test "${actual_bundle_digest}" = "${schemaBundleDigest}"
|
||||
tar -xzf /work/schema/ljwx-health-schema-bundle.tar.gz \
|
||||
-C /work/bundle
|
||||
python3 - /work/bundle/bundle-metadata.json <<'PY'
|
||||
import json
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
metadata = json.loads(Path(sys.argv[1]).read_text())
|
||||
sbom_path = Path("/work/bundle/bundle-sbom.spdx.json")
|
||||
provenance_path = Path(
|
||||
"/work/bundle/bundle-provenance.intoto.json"
|
||||
)
|
||||
digest = lambda path: (
|
||||
"sha256:" + hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
)
|
||||
if (
|
||||
digest(sbom_path) != metadata["sbomDigest"]
|
||||
or digest(provenance_path)
|
||||
!= metadata["provenanceDigest"]
|
||||
):
|
||||
raise SystemExit(
|
||||
"schema bundle SBOM/provenance digest mismatch"
|
||||
)
|
||||
provenance = json.loads(provenance_path.read_text())
|
||||
parameters = provenance["predicate"]["buildDefinition"][
|
||||
"externalParameters"
|
||||
]
|
||||
production = next(
|
||||
item for item in metadata["fingerprints"]
|
||||
if item["profile"] == "production-pg18"
|
||||
)
|
||||
expected = {
|
||||
"source": metadata["source"]["sha"],
|
||||
"runner": metadata["runner"]["imageRef"],
|
||||
"manifest": metadata["schemaManifestDigest"],
|
||||
"media": metadata["mediaType"],
|
||||
"canonicalProduction": production["digest"],
|
||||
"adoptedProduction": metadata["adoption"][
|
||||
"expectedPostFingerprint"
|
||||
],
|
||||
"adoptionPlan": metadata["adoption"]["planDigest"],
|
||||
"adoptionEvidence": metadata["adoption"][
|
||||
"catalogComparison"
|
||||
]["evidenceDigest"],
|
||||
"provenanceSource": parameters["sourceSha"],
|
||||
"provenanceRunner": parameters["runnerImageRef"],
|
||||
}
|
||||
actual = {
|
||||
"source": os.environ["sourceSha"],
|
||||
"runner": os.environ["schemaRunnerImageRef"],
|
||||
"manifest": os.environ["schemaManifestDigest"],
|
||||
"media": os.environ["schemaBundleMediaType"],
|
||||
"canonicalProduction": os.environ[
|
||||
"canonicalProductionFingerprint"
|
||||
],
|
||||
"adoptedProduction": os.environ[
|
||||
"expectedProductionFingerprint"
|
||||
],
|
||||
"adoptionPlan": os.environ["adoptionPlanDigest"],
|
||||
"adoptionEvidence": os.environ[
|
||||
"adoptionEvidenceDigest"
|
||||
],
|
||||
"provenanceSource": os.environ["sourceSha"],
|
||||
"provenanceRunner": os.environ["schemaRunnerImageRef"],
|
||||
}
|
||||
if actual != expected:
|
||||
raise SystemExit(
|
||||
f"schema bundle metadata mismatch: {actual} != {expected}"
|
||||
)
|
||||
adoption = metadata["adoption"]
|
||||
comparison = adoption["catalogComparison"]
|
||||
plan_path = Path("/work/bundle") / os.environ[
|
||||
"adoptionPlanPath"
|
||||
]
|
||||
evidence_path = Path("/work/bundle") / os.environ[
|
||||
"adoptionEvidencePath"
|
||||
]
|
||||
plan = json.loads(plan_path.read_text())
|
||||
plan_digest = "sha256:" + hashlib.sha256(
|
||||
json.dumps(
|
||||
plan,
|
||||
ensure_ascii=True,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
).hexdigest()
|
||||
if (
|
||||
adoption["planPath"] != os.environ["adoptionPlanPath"]
|
||||
or plan_digest != os.environ["adoptionPlanDigest"]
|
||||
or comparison["evidencePath"]
|
||||
!= os.environ["adoptionEvidencePath"]
|
||||
or digest(evidence_path)
|
||||
!= os.environ["adoptionEvidenceDigest"]
|
||||
or adoption["expectedPreFingerprint"]
|
||||
!= os.environ["adoptionExpectedPreFingerprint"]
|
||||
or adoption["expectedPostFingerprint"]
|
||||
!= os.environ["adoptionExpectedPostFingerprint"]
|
||||
or plan["catalogComparison"] != comparison
|
||||
):
|
||||
raise SystemExit(
|
||||
"schema bundle adoption contract mismatch"
|
||||
)
|
||||
PY
|
||||
|
||||
cd /work/bundle
|
||||
export PGHOST="${DB_HOST}"
|
||||
export PGPORT="${DB_PORT}"
|
||||
export PGDATABASE="${DB_NAME}"
|
||||
export LJWX_SCHEMA_SOURCE_SHA="${sourceSha}"
|
||||
export LJWX_SCHEMA_ALLOW_PRODUCTION=true
|
||||
DB_MIGRATOR_PASSWORD="$(cat /credentials/DB_PASSWORD)"
|
||||
export DB_MIGRATOR_PASSWORD
|
||||
adoption_plan=ljwx-boot/database/schema/adoption/production-k3s-eu-pg18.json
|
||||
export PGUSER=ljwx_migrator
|
||||
export PGPASSWORD="${DB_MIGRATOR_PASSWORD}"
|
||||
verify_schema() {
|
||||
ljwx-boot/scripts/schema-verify.sh \
|
||||
--profile production-pg18 \
|
||||
--target production \
|
||||
--expected-database "${DB_NAME}" \
|
||||
--database "${DB_NAME}" \
|
||||
--output /work/verify-evidence.json
|
||||
}
|
||||
if verify_schema; then
|
||||
echo "Production schema already satisfies the complete signed manifest; skipping retry upgrade"
|
||||
else
|
||||
ljwx-boot/scripts/schema-upgrade.sh \
|
||||
--profile production-pg18 \
|
||||
--target production \
|
||||
--expected-database "${DB_NAME}" \
|
||||
--database "${DB_NAME}" \
|
||||
--adoption-plan "${adoption_plan}" \
|
||||
--output /work/upgrade-evidence.json
|
||||
verify_schema
|
||||
fi
|
||||
actual_fingerprint="$(
|
||||
jq -r .logicalFingerprint /work/verify-evidence.json
|
||||
)"
|
||||
if [ "${actual_fingerprint}" != "${expectedProductionFingerprint}" ]; then
|
||||
echo "Production schema fingerprint mismatch: expected=${expectedProductionFingerprint}, actual=${actual_fingerprint}" >&2
|
||||
exit 1
|
||||
fi
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ljwx-health-config
|
||||
- configMapRef:
|
||||
name: ljwx-health-schema-release-unpromoted
|
||||
volumeMounts:
|
||||
- name: work
|
||||
mountPath: /work
|
||||
- name: registry
|
||||
mountPath: /registry
|
||||
readOnly: true
|
||||
- name: migrator-credentials
|
||||
mountPath: /credentials
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
volumes:
|
||||
- name: work
|
||||
emptyDir: {}
|
||||
- name: registry
|
||||
secret:
|
||||
secretName: ghcr-pull
|
||||
items:
|
||||
- key: .dockerconfigjson
|
||||
path: config.json
|
||||
- name: migrator-credentials
|
||||
secret:
|
||||
secretName: ljwx-health-schema-migrator
|
||||
items:
|
||||
- key: DB_PASSWORD
|
||||
path: DB_PASSWORD
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: ljwx-health
|
||||
resources:
|
||||
- job-schema-bundle.yaml
|
||||
@@ -0,0 +1,36 @@
|
||||
# LJWX Health isolated schema restore drill
|
||||
|
||||
This package is deliberately dormant. No Argo CD application references it
|
||||
until a reviewed activation commit pins all of the following:
|
||||
|
||||
- an exact signed LJWX Health Release receipt and schema ConfigMap;
|
||||
- the schema-runner digest from that same Release;
|
||||
- an exact pgBackRest backup label;
|
||||
- a unique immutable Job name and either `pre-adoption` or `post-adoption`.
|
||||
|
||||
The cross-namespace read-only Role and RoleBinding live in
|
||||
`apps/postgres-infra-operations/phase5-schema-restore-drill-rbac.yaml`. That
|
||||
resource is synced by the PostgreSQL operations Argo application before the
|
||||
restore Job is activated. Keeping it outside this
|
||||
package prevents the parent LJWX Health namespace transformer from moving the
|
||||
Role out of `infra`.
|
||||
|
||||
The Job restores only to a 12 GiB `emptyDir`. It never mounts a production PVC
|
||||
or a production application Secret. Egress is limited to cluster DNS, the
|
||||
Kubernetes API, the MinIO backup endpoint, and public HTTPS needed for
|
||||
GHCR/Sigstore verification; production PostgreSQL port 5432 is not reachable.
|
||||
|
||||
The `pre-adoption` mode applies the signed adoption plan, upgrade, and complete
|
||||
verify to a backup taken before production adoption. The `post-adoption` mode
|
||||
re-runs the signed upgrade idempotently and verifies the already adopted
|
||||
history. Both modes then start the exact `ljwx-boot@sha256` from the same
|
||||
Release against the restored database and an ephemeral Redis process. The
|
||||
application smoke uses generated credentials only and never reads the
|
||||
production application Secret. Both modes report the exact backup and source
|
||||
database identity, pgBackRest repository verification, RPO, end-to-end RTO,
|
||||
critical row counts, catalog/RLS/extension inventory, TimescaleDB absence or
|
||||
object counts, normalized fingerprint, and application readiness result.
|
||||
|
||||
Activation and cleanup are GitOps changes. Preserve Job logs and the
|
||||
`RESTORE_DRILL_PASS` JSON before a later cleanup commit removes the Job and its
|
||||
ephemeral volume.
|
||||
@@ -0,0 +1,393 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: ljwx-health-schema-restore-drill-unconfigured
|
||||
namespace: ljwx-health
|
||||
labels:
|
||||
app.kubernetes.io/name: ljwx-health
|
||||
app.kubernetes.io/component: schema-restore-drill
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "4"
|
||||
operations.ljwx.io/activation: dormant-requires-exact-release-and-backup
|
||||
spec:
|
||||
backoffLimit: 0
|
||||
activeDeadlineSeconds: 3600
|
||||
ttlSecondsAfterFinished: 604800
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: ljwx-health
|
||||
app.kubernetes.io/component: schema-restore-drill
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
serviceAccountName: ljwx-health-schema-restore-drill
|
||||
automountServiceAccountToken: false
|
||||
imagePullSecrets:
|
||||
- name: ghcr-pull
|
||||
securityContext:
|
||||
fsGroup: 1001
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
initContainers:
|
||||
- name: prepare-volumes
|
||||
image: docker.io/bitnami/postgresql@sha256:9b62e6057d781853916a712ad2591a607de4529160e07c2eaf58d75ef6c45b9d
|
||||
command:
|
||||
- /bin/bash
|
||||
- -ceu
|
||||
- |
|
||||
chmod 0700 /restore
|
||||
chmod 0770 /socket /evidence
|
||||
chown 1001:1001 /restore /socket /evidence
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
securityContext:
|
||||
runAsNonRoot: false
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
add: ["CHOWN"]
|
||||
volumeMounts:
|
||||
- name: restore
|
||||
mountPath: /restore
|
||||
- name: socket
|
||||
mountPath: /socket
|
||||
- name: evidence
|
||||
mountPath: /evidence
|
||||
- name: verify-release-and-fetch-backup-config
|
||||
image: ghcr.io/lingjingwanxiang/ljwx-health/schema-runner:unpromoted
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/bash
|
||||
- /scripts/prepare-release-and-backup.sh
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ljwx-health-schema-release-unpromoted
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: scripts
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
- name: work
|
||||
mountPath: /work
|
||||
- name: evidence
|
||||
mountPath: /evidence
|
||||
- name: backup-credentials
|
||||
mountPath: /credentials
|
||||
- name: pgbackrest-config
|
||||
mountPath: /pgbackrest
|
||||
- name: registry
|
||||
mountPath: /registry
|
||||
readOnly: true
|
||||
- name: kube-api-access
|
||||
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
|
||||
readOnly: true
|
||||
containers:
|
||||
- name: restore-postgres
|
||||
image: docker.io/bitnami/postgresql@sha256:9b62e6057d781853916a712ad2591a607de4529160e07c2eaf58d75ef6c45b9d
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/bash
|
||||
- /scripts/restore-postgres.sh
|
||||
env:
|
||||
- name: BACKUP_LABEL
|
||||
value: BACKUP_LABEL_REQUIRED
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
ephemeral-storage: 4Gi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 2Gi
|
||||
ephemeral-storage: 12Gi
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: scripts
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
- name: restore
|
||||
mountPath: /restore
|
||||
- name: socket
|
||||
mountPath: /socket
|
||||
- name: evidence
|
||||
mountPath: /evidence
|
||||
- name: backup-credentials
|
||||
mountPath: /credentials
|
||||
readOnly: true
|
||||
- name: pgbackrest-config
|
||||
mountPath: /etc/pgbackrest
|
||||
readOnly: true
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: verify-restored-schema
|
||||
image: ghcr.io/lingjingwanxiang/ljwx-health/schema-runner:unpromoted
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/bash
|
||||
- /scripts/verify-restored-schema.sh
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ljwx-health-schema-release-unpromoted
|
||||
env:
|
||||
- name: BACKUP_LABEL
|
||||
value: BACKUP_LABEL_REQUIRED
|
||||
- name: RESTORE_MODE
|
||||
value: pre-adoption
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: scripts
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
- name: work
|
||||
mountPath: /work
|
||||
readOnly: true
|
||||
- name: socket
|
||||
mountPath: /socket
|
||||
- name: evidence
|
||||
mountPath: /evidence
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: ephemeral-redis
|
||||
image: harbor.omniverseai.net/library/redis-stack-server@sha256:768c7c0782c0ffc931caf934c5491de6fe2a195b1f7be748ddd34746f50b3c22
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
- /scripts/run-ephemeral-redis.sh
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: scripts
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
- name: evidence
|
||||
mountPath: /evidence
|
||||
- name: redis-tmp
|
||||
mountPath: /tmp
|
||||
- name: smoke-restored-application
|
||||
image: ghcr.io/lingjingwanxiang/ljwx-health/ljwx-boot:unpromoted
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
- /scripts/smoke-restored-application.sh
|
||||
env:
|
||||
- name: LJWX_BOOT_IMAGE_REF
|
||||
value: ghcr.io/lingjingwanxiang/ljwx-health/ljwx-boot:unpromoted
|
||||
- name: SPRING_PROFILES_ACTIVE
|
||||
value: k8s
|
||||
- name: SERVER_PORT
|
||||
value: "19998"
|
||||
- name: DB_HOST
|
||||
value: 127.0.0.1
|
||||
- name: DB_PORT
|
||||
value: "55432"
|
||||
- name: DB_NAME
|
||||
value: ljwx_health
|
||||
- name: DB_USER
|
||||
value: ljwx_app
|
||||
- name: DB_PASSWORD
|
||||
value: restore-drill-trust-only
|
||||
- name: SPRING_DATASOURCE_URL
|
||||
value: jdbc:postgresql://127.0.0.1:55432/ljwx_health?currentSchema=public&stringtype=unspecified
|
||||
- name: SPRING_DATASOURCE_USERNAME
|
||||
value: ljwx_app
|
||||
- name: SPRING_DATASOURCE_PASSWORD
|
||||
value: restore-drill-trust-only
|
||||
- name: SPRING_DATASOURCE_DRIVER_CLASS_NAME
|
||||
value: org.postgresql.Driver
|
||||
- name: SPRING_FLYWAY_ENABLED
|
||||
value: "false"
|
||||
- name: REDIS_HOST
|
||||
value: 127.0.0.1
|
||||
- name: REDIS_PORT
|
||||
value: "6379"
|
||||
- name: REDIS_DB
|
||||
value: "15"
|
||||
- name: REDIS_PASSWORD
|
||||
value: ""
|
||||
- name: SPRING_REDIS_HOST
|
||||
value: 127.0.0.1
|
||||
- name: SPRING_REDIS_PORT
|
||||
value: "6379"
|
||||
- name: SPRING_REDIS_DATABASE
|
||||
value: "15"
|
||||
- name: SPRING_REDIS_PASSWORD
|
||||
value: ""
|
||||
- name: JAVA_TOOL_OPTIONS
|
||||
value: ""
|
||||
- name: JAVA_OPTS
|
||||
value: -Xms128m -Xmx640m -Duser.timezone=Asia/Shanghai
|
||||
- name: OTEL_TRACES_EXPORTER
|
||||
value: none
|
||||
- name: OTEL_METRICS_EXPORTER
|
||||
value: none
|
||||
- name: OTEL_LOGS_EXPORTER
|
||||
value: none
|
||||
- name: MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS
|
||||
value: never
|
||||
- name: MANAGEMENT_ENDPOINT_HEALTH_SHOW_COMPONENTS
|
||||
value: never
|
||||
- name: LOGGING_LEVEL_COM_LJWX_STARTER_SATOKEN_LOG
|
||||
value: "OFF"
|
||||
- name: OSS_ACCESS_KEY
|
||||
value: restore-drill
|
||||
- name: OSS_SECRET_KEY
|
||||
value: restore-drill
|
||||
- name: OSS_BUCKET_NAME
|
||||
value: restore-drill
|
||||
- name: OSS_ENDPOINT
|
||||
value: http://127.0.0.1:1
|
||||
- name: LJWX_LICENSE_ENABLED
|
||||
value: "false"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 768Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 1536Mi
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: scripts
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
- name: evidence
|
||||
mountPath: /evidence
|
||||
- name: app-logs
|
||||
mountPath: /app/logs
|
||||
- name: app-uploads
|
||||
mountPath: /app/uploads
|
||||
- name: app-tmp
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: scripts
|
||||
configMap:
|
||||
name: ljwx-health-schema-restore-drill-scripts
|
||||
defaultMode: 0550
|
||||
- name: restore
|
||||
emptyDir:
|
||||
sizeLimit: 12Gi
|
||||
- name: socket
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 64Mi
|
||||
- name: evidence
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 128Mi
|
||||
- name: backup-credentials
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 1Mi
|
||||
- name: pgbackrest-config
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 1Mi
|
||||
- name: work
|
||||
emptyDir:
|
||||
sizeLimit: 512Mi
|
||||
- name: registry
|
||||
secret:
|
||||
secretName: ghcr-pull
|
||||
items:
|
||||
- key: .dockerconfigjson
|
||||
path: config.json
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 512Mi
|
||||
- name: redis-tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 128Mi
|
||||
- name: app-logs
|
||||
emptyDir:
|
||||
sizeLimit: 256Mi
|
||||
- name: app-uploads
|
||||
emptyDir:
|
||||
sizeLimit: 64Mi
|
||||
- name: app-tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 256Mi
|
||||
- name: kube-api-access
|
||||
projected:
|
||||
defaultMode: 0440
|
||||
sources:
|
||||
- serviceAccountToken:
|
||||
path: token
|
||||
expirationSeconds: 3600
|
||||
- configMap:
|
||||
name: kube-root-ca.crt
|
||||
items:
|
||||
- key: ca.crt
|
||||
path: ca.crt
|
||||
- downwardAPI:
|
||||
items:
|
||||
- path: namespace
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.namespace
|
||||
@@ -0,0 +1,21 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- rbac.yaml
|
||||
- networkpolicy.yaml
|
||||
- job-schema-restore-drill.yaml
|
||||
configMapGenerator:
|
||||
- name: ljwx-health-schema-restore-drill-scripts
|
||||
namespace: ljwx-health
|
||||
files:
|
||||
- scripts/prepare-release-and-backup.sh
|
||||
- scripts/restore-postgres.sh
|
||||
- scripts/verify-restored-schema.sh
|
||||
- scripts/run-ephemeral-redis.sh
|
||||
- scripts/smoke-restored-application.sh
|
||||
generatorOptions:
|
||||
labels:
|
||||
app.kubernetes.io/name: ljwx-health
|
||||
app.kubernetes.io/component: schema-restore-drill
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "-2"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user