From 2d523e16d0486755ed8e1cacbec663082a762194 Mon Sep 17 00:00:00 2001 From: bjwswang Date: Mon, 30 Oct 2023 09:20:25 +0000 Subject: [PATCH 01/12] feat: update arcadia helm charts and add minio as system datasource for arcadia Signed-off-by: bjwswang --- .github/workflows/example_test.yaml | 2 +- Dockerfile | 1 + api/v1alpha1/datasource.go | 8 + api/v1alpha1/datasource_types.go | 4 +- api/v1alpha1/zz_generated.deepcopy.go | 6 +- assets/system_datasource.drawio | 1 + assets/system_datasource.drawio.png | Bin 0 -> 63013 bytes charts/arcadia/Chart.lock | 6 + charts/arcadia/Chart.yaml | 12 +- charts/arcadia/charts/minio/Chart.yaml | 18 + charts/arcadia/charts/minio/README.md | 260 ++++++++ .../arcadia/charts/minio/templates/NOTES.txt | 43 ++ .../minio/templates/_helper_create_bucket.txt | 127 ++++ .../minio/templates/_helper_create_policy.txt | 75 +++ .../templates/_helper_create_svcacct.txt | 106 ++++ .../minio/templates/_helper_create_user.txt | 105 ++++ .../templates/_helper_custom_command.txt | 58 ++ .../charts/minio/templates/_helper_policy.tpl | 28 + .../charts/minio/templates/_helpers.tpl | 218 +++++++ .../charts/minio/templates/configmap.yaml | 38 ++ .../minio/templates/console-ingress.yaml | 58 ++ .../minio/templates/console-service.yaml | 48 ++ .../charts/minio/templates/deployment.yaml | 206 +++++++ .../charts/minio/templates/ingress.yaml | 58 ++ .../charts/minio/templates/networkpolicy.yaml | 27 + .../minio/templates/poddisruptionbudget.yaml | 18 + .../charts/minio/templates/post-job.yaml | 272 +++++++++ .../arcadia/charts/minio/templates/pvc.yaml | 35 ++ .../charts/minio/templates/secrets.yaml | 22 + .../templates/securitycontextconstraints.yaml | 45 ++ .../charts/minio/templates/service.yaml | 49 ++ .../minio/templates/serviceaccount.yaml | 7 + .../minio/templates/servicemonitor.yaml | 117 ++++ .../charts/minio/templates/statefulset.yaml | 259 ++++++++ charts/arcadia/charts/minio/values.yaml | 557 ++++++++++++++++++ ...rcadia.kubeagi.k8s.com.cn_datasources.yaml | 63 +- .../arcadia.kubeagi.k8s.com.cn_embedders.yaml | 169 +++--- .../crds/arcadia.kubeagi.k8s.com.cn_llms.yaml | 42 +- charts/arcadia/templates/post-oss.yaml | 19 + charts/arcadia/templates/rbac.yaml | 1 + charts/arcadia/values.yaml | 39 ++ ...rcadia.kubeagi.k8s.com.cn_datasources.yaml | 11 +- config/rbac/role.yaml | 1 + .../samples/arcadia_v1alpha1_datasource.yaml | 17 - .../arcadia_v1alpha1_local_datasource.yaml | 8 + controllers/datasource_controller.go | 24 +- .../go-server/pkg/datasource/datasource.go | 4 +- pkg/datasource/datasource.go | 41 +- {config/samples => tests}/example-test.sh | 36 +- 49 files changed, 3230 insertions(+), 139 deletions(-) create mode 100644 assets/system_datasource.drawio create mode 100644 assets/system_datasource.drawio.png create mode 100644 charts/arcadia/Chart.lock create mode 100644 charts/arcadia/charts/minio/Chart.yaml create mode 100644 charts/arcadia/charts/minio/README.md create mode 100644 charts/arcadia/charts/minio/templates/NOTES.txt create mode 100644 charts/arcadia/charts/minio/templates/_helper_create_bucket.txt create mode 100644 charts/arcadia/charts/minio/templates/_helper_create_policy.txt create mode 100644 charts/arcadia/charts/minio/templates/_helper_create_svcacct.txt create mode 100644 charts/arcadia/charts/minio/templates/_helper_create_user.txt create mode 100644 charts/arcadia/charts/minio/templates/_helper_custom_command.txt create mode 100644 charts/arcadia/charts/minio/templates/_helper_policy.tpl create mode 100644 charts/arcadia/charts/minio/templates/_helpers.tpl create mode 100644 charts/arcadia/charts/minio/templates/configmap.yaml create mode 100644 charts/arcadia/charts/minio/templates/console-ingress.yaml create mode 100644 charts/arcadia/charts/minio/templates/console-service.yaml create mode 100644 charts/arcadia/charts/minio/templates/deployment.yaml create mode 100644 charts/arcadia/charts/minio/templates/ingress.yaml create mode 100644 charts/arcadia/charts/minio/templates/networkpolicy.yaml create mode 100644 charts/arcadia/charts/minio/templates/poddisruptionbudget.yaml create mode 100644 charts/arcadia/charts/minio/templates/post-job.yaml create mode 100644 charts/arcadia/charts/minio/templates/pvc.yaml create mode 100644 charts/arcadia/charts/minio/templates/secrets.yaml create mode 100644 charts/arcadia/charts/minio/templates/securitycontextconstraints.yaml create mode 100644 charts/arcadia/charts/minio/templates/service.yaml create mode 100644 charts/arcadia/charts/minio/templates/serviceaccount.yaml create mode 100644 charts/arcadia/charts/minio/templates/servicemonitor.yaml create mode 100644 charts/arcadia/charts/minio/templates/statefulset.yaml create mode 100644 charts/arcadia/charts/minio/values.yaml create mode 100644 charts/arcadia/templates/post-oss.yaml delete mode 100644 config/samples/arcadia_v1alpha1_datasource.yaml create mode 100644 config/samples/arcadia_v1alpha1_local_datasource.yaml rename {config/samples => tests}/example-test.sh (86%) diff --git a/.github/workflows/example_test.yaml b/.github/workflows/example_test.yaml index 16d0ed0a1..20ce35a60 100644 --- a/.github/workflows/example_test.yaml +++ b/.github/workflows/example_test.yaml @@ -78,7 +78,7 @@ jobs: mkdir -p ${GITHUB_WORKSPACE}/bin cp /usr/local/bin/kustomize ${GITHUB_WORKSPACE}/bin/kustomize - name: Example test - run: config/samples/example-test.sh + run: tests/example-test.sh - name: Upload logs if test fail if: failure() uses: actions/upload-artifact@v3 diff --git a/Dockerfile b/Dockerfile index 8b60d8629..d39c25a75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ COPY go.mod go.mod COPY go.sum go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer +RUN go env -w GOPROXY=https://goproxy.cn,direct RUN go mod download # Copy the go source diff --git a/api/v1alpha1/datasource.go b/api/v1alpha1/datasource.go index 76b0f40fc..8ae60710d 100644 --- a/api/v1alpha1/datasource.go +++ b/api/v1alpha1/datasource.go @@ -23,11 +23,19 @@ const ( type DatasourceType string const ( + DatasourceTypeLocal DatasourceType = "local" DatasourceTypeOSS DatasourceType = "oss" DatasourceTypeUnknown DatasourceType = "unknown" ) func (ds DatasourceSpec) Type() DatasourceType { + // if endpoint is nil,this must be a `Local` datasource which utilize `SystemDatasource` by default to host its data + if ds.Enpoint == nil { + return DatasourceTypeLocal + } + + // For third party datasources + // Object storage service if ds.OSS != nil { return DatasourceTypeOSS diff --git a/api/v1alpha1/datasource_types.go b/api/v1alpha1/datasource_types.go index c9f07ade5..de186f6b9 100644 --- a/api/v1alpha1/datasource_types.go +++ b/api/v1alpha1/datasource_types.go @@ -35,7 +35,7 @@ type DatasourceSpec struct { Description string `json:"description,omitempty"` // Enpoint defines connection info - Enpoint Endpoint `json:"endpoint"` + Enpoint *Endpoint `json:"endpoint,omitempty"` // OSS defines info for object storage service OSS *OSS `json:"oss,omitempty"` @@ -56,6 +56,8 @@ type DatasourceStatus struct { //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:scope=Namespaced +//+kubebuilder:printcolumn:name="display-name",type=string,JSONPath=`.spec.displayName` +//+kubebuilder:printcolumn:name="type",type=string,JSONPath=`.metadata.labels.arcadia\.kubeagi\.k8s\.com\.cn/datasource-type` // Datasource is the Schema for the datasources API type Datasource struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 4ce689017..07b3efe93 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -127,7 +127,11 @@ func (in *DatasourceList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DatasourceSpec) DeepCopyInto(out *DatasourceSpec) { *out = *in - in.Enpoint.DeepCopyInto(&out.Enpoint) + if in.Enpoint != nil { + in, out := &in.Enpoint, &out.Enpoint + *out = new(Endpoint) + (*in).DeepCopyInto(*out) + } if in.OSS != nil { in, out := &in.OSS, &out.OSS *out = new(OSS) diff --git a/assets/system_datasource.drawio b/assets/system_datasource.drawio new file mode 100644 index 000000000..b84cfffb6 --- /dev/null +++ b/assets/system_datasource.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/assets/system_datasource.drawio.png b/assets/system_datasource.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..5782f41e838689baa4b6bfb716bdca294720fb72 GIT binary patch literal 63013 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%0Am-SR8$68cWi$%TVCk!BNGoD88IhgDY3X%(Vc! z8b;Pbg~Yq0dLv5H9? z3_$E!F*rAVeooe_+p69%5~~NsZi$9=`fzN3`u76>79|kt^~+8{ATAq>uNaZ1pGujZ zC@8&mX>0340bx3(OJUDemvPDCfa$Fo(v2K~M488tDfNxLqpxFtJSygdHC{UlP+X$5 zi;p1?kT(VJX_L_>_qy6j@-%(}<=206Sp3yws>f)^7gwFMw;guLEC`tXA`I|H z4fj=BpnnF*Y8`F_#ZX^S%UJQM-?&_JuGV}P0fOqgx48WhV`XI(3nDH`&=+2XBgUs{ zqI&b?A^5Dc^UoLB%1^beP9~>b-{2g~X4>1RjD;<2dNbUWUJ>7U&^B(r-EOK=5}$Z> zxE}0(RwPT}r{vYANt_84v#ncHzE{udo@gCb=biziJ5JSkH_ym#_F3*$&t%YcvjekP z3%X)~aIM^XW^Yr-v64Dq>g=mdwpTzg$s+Ort4SV1-v?Un8-CQWaNHh{=^XSL^t;lu z;C{TUGJPw>F#BYT=vcl>ef`wSJ0KzS*%=GVv4^OOa*;j<+&;(g&9op);5WFrdC~cn zsTL&>^Pr-@JWt=OfD9O;C~(^S`+wo=Ab&fPum!fkK$55|DodHx?0 zK<51;fjt_0`{uo~{HmvJti)C;%1pc+n;kWL!dK%PMfg6yt%4r7y7g5;G$MfrjXvmz z(A71Moq0IweCp?sX_3S|Vkq3vC5u~>g35ir&=5-uNl`!K5f>>wqJ4ztu9mE}__s5H z^WV)8hevt%RLjC743j)2j(_~}tH%2#(Xi!y-O_v0>d|@uF8jg8AAkCyH=cj`c4XgS4L#R^?kAXsm)rC8XM>s5 zo)KZ*xIou5R=4N|so-uX7d?2N#9SPA*AREB+*SYG!swKEu{*E0^Vakp))(l%Qw@i$ zH?enO$SUZ00zCE&|oly2ECh@O2rHb3}Xwf0?8 zstr7u5HfC*Agra-eVdZJbXi`;I(GNm+@kqv0jrG8LL;>f0?Hqn ziQkZF;h4@ZohLlS+orB|V3y&YAkw3-EfB_o!dSv!k42Rj;o;{;1Asd2>X_ zz2yBnTTi#>PqX+Z{1*xSr@kM3P+Q-3+0r7hbdDdr01fCU`t|*Syo=*vjwa`jZB~1N zTK>gya`4;Ntp_VE%W-z1J`&{S=zT%=L$~*4>oLI<4zyB-91g?B4km5GHM}E=Y5eH; zY!%q&w0zz-1AH^`_NUj6^8kk;!bhiv;LirHfr6(Xa;1%ZSJDnw;fQOML!;V6v7bKA z#ZM1C(C|amM{{A6Q5=CYFT3WNLcK+v9^w*y{`eEA=fQG(*!1a^;||_WdR^$o(fq## zN-DH#$M418XHF-Ql1x(_dt*;49y%{Sb=Biod&2CmUXJDiG8=o{*fxgG&5nzpzxe?Q za0gypPvS$%yJ3l)8TDpLuIrgD$rUZx(fxPMw{*X}v-}gfSS57S{@~e01iU`E)NPI< zyZqvr;JnSTjXgt&X~j#M6K?M!SEPyY={AT=@lR*WMm8pDHnbm`=Fb;mVacuYg&e8!MCAzAA%dR)>}y3hTnR41{iIMh__=Tv|Dn!@gs} z8{bAQit18_A5KJ{pl@{@%+DK_9r@aQv*VJCH36GW=>EQf>R`~@-(5aFP&ext5&BJ` zIqh)rEvFma59*4a@del-H6Wcf-iGfm=p|Lllp*fA2bdy0_wXL(itjuZ{nE;2?pxbWyF^95$5sgv z2cNuNMPCStIg5+@u(&PYIH~^hwa1}ut=ZYn9z}H_A&coXB~;06J21AiF#e|FRC0wM zZF6oaS<>MCMnI*s=~>o6yLF!S7n>)BsMl|}!RNoIzGu!GxOui%Ty5LN zP1s#lIoz!2pYV%0GYK$8j?jJX)X&uE?qjgWyE*hO0Vm_bZ>E)B-DTyveAj+%_$EzG zBN4Tp&7FIBtWwkjK~gue9~i%nud?%z=?b1qw6cilV% z7-Hv}%p=-%`PC|DLGvlc()Q1;cTh#Ze?n3w9QZD_=T1;Xgi_W5bSn<1aU@7Q^C0!> zw}ol#*Y-~V%rybA93-ko)&7;QcTEMWGJf*p+-%D_{b?4qrB<%!{me#s9N5*e&(mZ^ z+Khf`S#2q|mDLecVdw()``GK=UpFx+@%yI>6jj;bUSRkG#zKyIEg~&W;o|1({gODP z@MVXHx#_lh)e2=SvFJ~G1WwJl-(|C!8D~fd>Iz(71gqXM8 zmV5~b4dC&mQHs5}2iTSZLhckYAu$%{4IHLepbPI%C{J_HT84bF?EuccL5xsmJNre} zgnLQbAJWn*qSlx!)8%vm$hS|~3YSrS_g~D@TX>=IsiB$^hDaJyt9SdwC0(?~ie{Dh z#y)%tMYa<61jtHuP8~1G@6N9S-T2`~U1?}<7Var!UQP~aVz)#gl8KRw`$jPcV7ZeX zc0`*>IYqTvJYjLL40r#V=yEDolh?paDYh2flu`b9NMYGr!lSzLv5L7Ly^hsJxZF0+ zu;}7=BAUwi4UeRgsCH2954ab0oo>eG!t=*F!<|y|S!V~Vzvmt z3Uqcl==@@oA?$XLMmG(j3dW}-6ms`&Po(7hw>}{_8Uqoq#_kR#LbVkZRx59NI>kCf z9TJhG1d+D3VSSdPb{Y6do_Ce?@Y}I3y?{rnUQV)_4*kc!BMSDOk*GP-`6AcJ zzG?itaZagoy2u}QFx)>1bt2V$V`EKyZk(4Gy5*w|JGLUz?)Q%jbj17*mGAqFOnY2} z$qrftQNKQtly6U;*PVn0=;Zd*KOG^^kQN6mOU#gaHm|Jh@AyT`^sZ*`|G{OPrYz^< zeUmNT2pv#cy4bMh6xM>iyNG31;BsWCN4$(C0dfVKO@}KX1#u0i+v$fsjI5W1m1rzA zNVbAHm51Bi8A9a`>_8RnazuJot-=|En>gvmTN%xeMRKWq8<=b)uy!$wfqofn=V`&T zb(&BOle%~dn;iSFaqnq2SO2>fcZFxt@VM{%T*i`GIjOEp}L_-F~pDEf{pgvV3icRe%O9FMwW&E3*PNm9JKI-b}7^U+4)rwlUYU?c;Cat=h_ChoLi`|S6 zdJ}Y}nOYJz@q;S_QRqF>yKSXbl8OETtu!HN8t}x(S5L5YR82emj#*3C1=YOvGbg* zA7Etnx11rJ^G}m;u#UX@R{%1!FzX=SO(|#1Rt&*h|Ni=_eTRiK(ZLwqjr7EYLza}0 zm$c5yU;Ga?(tFXK;KFOrx{+p9RzX@N*Scg>nCcm1V=muRnCxeKXkjaii$3~$@8P>; zHz61q{gZ!21}nOd=Ffu=@W)m$6yvC-@Fgc8PQjhKAI%zr<+HKZfe%R20up8>NR8HwP>n zdSi}i<%~!&EuPwgq`=OTcPkC4xKcTuT{x!ThOoAsh z*^U%7w4DC%soYXr>)Fq-@OTKdh5U@oE;oa}3C8JH2%#5y@VL2@`Q8^>-HhdQ`B0M35I3L$8x-+}zx9GBAur zA4{bif?B^Tz*^*C5|Mk&jpVvgjo8Ug9xQ{npvVLnWMmTC{_$ni ziVTU~Ft1&$&XVVR^CRERUdiVuA^taA3TR#NG#YUdrt^8`KjybXZj-VD=lC>Mv?8{j zsy}xzp@C|538*qs5R?zI4K;?-=#rZYiX66v)#B!q-W<0q7dy8(UWiY4Cuf1@2c!x4 zabtSHfpL{8>uEFmIA6aK*DWt0F!NeF)C9z~V3RfotCTmn)f?(;)$}D$k;TB|0v%XK zklsUhu{YacJ@}XoT1;{BYItD-%>G8IQmL-4coH$Vl1n5Ple0niM1cyfeB>sRi9b&F zkl|M4egw*f`rd~R76&7O>6EbMIFae5e{c#qVs7=NVB(WM8!5-!;lkXceu}qELc@$4 z8kNCT_=Nd-sUi(%TNUM(LO5Wp_d=~gyWSozXi_sWH7iM7BN+Mc4jt+=R^WOrO}_Us zr}TqySzh+EeF4lX;#mw%Pb8(c<78-|CTdLp-h(^thoO0r3kphZiSKlZN@169sj?VG z+$)$iN`47AxI#&_J0w~f#&yA3`D#bU#c(U{G59!<7?j=96QLpH=-vGj3qrheB4ufJ zc7c4(RQ2ZJ5=R-}Lxx(Pji?*KPwp(i+Sc_#C^Ck5Qcd>`YIol8Ol(-PC<^yu2-3qkaHq`BO+M-L{AQE z(KC|P$f1TkS5MA5vE;txty}vUOF|zKE=K&BtQw2o2Ln(n2vXQUS04_%gNq8c)8A(< ztdPyF7X@2sJXrW8;|zu*XsZ)P;OQQr z3k4n4+Iy$uX^FOUxorVRCf0KQc_Ya8c}|7L^J!{Tk6+^_U|U2GXK{x zc%NtdmT+DPZ3s}7VP#blR9b5dOX89GZj|+cE*E(&hLC5lEo@wtS2|jLyHq}%cyYFF zTtqQ;xL(2ttB1x^kd;D#fM%!5G&Qc#1ugnuz0AHvObL3t?OiF2Ju+}V{tE-5pc9fc z3Q5o9?pJ6JS!rpB3=^tl14Fc^Z!UGFzQ_3AuM?FwJ(_AkKeH76ihw^36}ZVZB#ptv zLAbPT6vi@!d%8bftMC{@%4~tdld_Eu6IolM!Q-65_0g)YPdkFlpQrPF7XQ? zfFdxU%)MImdn(SiB@||<#viVj$V2r9ua1v(g@IZUK2cG^Ja2;iQcg`+=jM2rV4`PZ z#5z9g?^Dr-((4Y4QZ5>dkI;c!+X(oaTcJ>|IuBa8Z3gIvH~q!ijSj8Nv=U81xRfP7 z9VmB%+u`TWT^&ckK(dnH%bT6mjhVWaOs*7A1TX!s-FYoN>7cU3e_TZt$K=Nna)c5C z;_=})5HCeCBF>L(HDS+C4pk$Sy#xS5ack^fJLMA*Os~beK4hnEc~{z0hb5dhogn%! zbWxeTZjU>2-kfIn0yMQ@rzqFLgDXQEyZU7zzo5PltEzXT3>!km-?VX>!Uz_qWMXN1 zi^k5agb*6!t(9|H)`uF}pp9(LK;9{;96bp_h98$i#a<{ffTnNs@pGNQ0kdPNahc;_ zCBx7SZ4XZnx)85_i}#VpZ`hZ3yR4LTIJh`2vGE#Cp|xWb&=z$ewq2i82|XpsjZ6cB zMqSQi_v5AkUNL;UN5(IZ|ARrVABkD)AGGNe-6+f9vd)L%CU}CeLBnf{We}9_^U81( zUL{7_3gynY_U_uaPNj1y9^`q=mxk9oulbR$<#k16!Sr;X4IDw=zcK5q(CvbH(!)#= z;5d3eu2c8taUaJ&F4Lwp- zr&nR*COZ>5gM8Iky53IG5#NuJE_v>(QX;d03w_82)v2N3g5L=|$>J-~z%6=|`nieh zwb(|-IqbE9g;$mU9PEKeIK_QlvRkz(Q6}2JK(eOUu`R{Jfc=)_xSYRW0^U7K>N%)d zc##*+8M5V$K}M~j0#nGthJ-5;d76BBlTMHeKeyZSJ%RPm`+hDS9ffqoO@$aTgcunD z%Pz}O|1TnM{Qkl;{Hqdgskg=MOnUhkpwmeF!~Sy}+N-caarBV5-HkuhZony~Fk*M=q4;vz*gIX)%@!j@{MQr7l`qj7{C&Un6SFYoP;z(IxLLazvg%d4@$B zWT?NG4M1|dV{fB`wR?f1mEHa@vd6ibpH)a$&hZg~1tCHhue@Ao3m;DD*>HP3Wlh`y z`M$E95}i49MIw>f5zkSJV7^M8rHvzS#`W4`b!4uWqjt(nG#p2w$KQhH8AL$F z1!VH?U*n0%pN1p1CzO1u6X|I%|IHWi><1Y`fO^+au3;cH0G|pB!*UkG0^vF{byel{ zMN?3Q>fm^A)(z5Gi_1~lOBejfReC4a$R-23Fhjb?>{hL;f!Qry_=|upYI9PmC_wI{ zgGFFQWj-}?9KEjy8Y#LWse6C_U2kV~$9X*g6m3BovrOEOq^Cd};*#~!aTaM)B{>j! zXj!1lHkAZ-f=!~P6#_AougDX|eEcfu6Eegyo@?FxmzNAw{RT{E}1cD_p1NA|Xx64&C{;HYgh|peoUDGFHTO@R?WO#!hT%O;EMEr{c%W4?17Y zJIAlhH}`K}fe(JBrFly(1s$`HTQ9EI!TTi-TKF;iz(?v25dy0H`hq<=Q-hp{ICo6t zf}{-ly4peC2(&E7h_iY3wguNTN#etQ{sw9FC7%E67yM+#)wt6j zNDsOW6Qn1GT^T4h(jbt4mNat`;TP_#;mllk(X@zika9&H-S8iF`giv{-e+IOVQ7~dXl~kvM+`^@r)X$Q$XWnb#*t?f6G-PkWJu_sM8@U?J?*Yz@yBvq6ATJN=5ECAnAqO?JrV?K zP8b=<=S{uYZ>aSQbSFE?>Fhy~K1d{R`MNCfkcOtJ61IZ9C2Zv{c0)dTaIC9()Mn8 zKFK8Ei1=EkJM@w7whsImbCzAr-JK-fRiVk0!TsKfXJYl;^FrLQ%}~~#B4aq|o?o#1 z-#WDK9?4?gt5h^O$QjLI)J+i60X@Mzmga>m9^RLcjwqWsk#F@}<)zS`vWyfP)@c5~ zz-5+XYK~$ssZK2T)51dPQ6DvdrpRbXhl(wMBOOD=Xj`Vr5^4G&UG3qa5{}__kvJA5 z!&z;6aopNgT;e_?{iE{aHILL)l*mSN66}}fD7z%;+~wVZnmxww>*%(ORN3*wN70k5 zodj^EvfLO6TY=Z7w^~FOac3%pazVn(>QxyIml25g_}-|&IOOk%0c1f3CQ288bPphx zleGZ@AoFtF3ZptI(9|b)NH{Ye9{0+U_}aIClkLF|;e3K9IicYyd%w&L82*%n543WX zUGo@EeNi?K2>qJ%5lpxsJnR^)CZU?VlYf}OFgSUWuoJ(!msVaEdidkw2DiwnC|lQ* zP{WFt$k@`qE$XopzU1z$Xe?!PS^F1>&uFYh0zTT@aBH$D{YwlM7S7L)?iYz6dgR|w zS-Q=o)41Jk2)B$XH>)Z}=2~3#d2qVQ(I5XgFtxvw;9?iPU9hbrEbA#pCnQ2b<~V{?NEbP)rfxdBSXRTSV!^}Z&$<$&7}|a7y$g<_vi8ol zOZB5Nwa2ak?9+tc>7|lk_GRt-z;M+1piq^d^W?nmU^%N1JfIN%Ww{*NREyYY)?;-0 z<)z>}lGnuFN7u|$M;oj+xWu`w0WQgK|3l6?zq0Q-=#C<1#KUS#@APKIIWnt!xH*6jCshGi{u&VL19S)I7RR^$I| zwov-vr6Yx6{((>|cMGdg=Dz%ds45(ZC}KgM#B86;3IpD`f4TM~_FuNMu_2D;IUm$Y z3Lo*gA$>r4T3_Pj;t%&%WKCY;mlAWzAZrO~DAowLq(tKZ<-9AHf>NJ*8GR;M4^l~k z=KM*CMYdTp9z>jsY5*Kb)*0d<3|mmC4oTD(y|B~fE(f4cK(7_qlTKLdPx4y3Y4I=) zC;%n2retKf`JtjqUNtX`Z75+3wF-P4_M5KB3JA=$Zycz|_*On|GV~6n6A9yu`V30aitzLI=&S%D-lx2sWzSI44of9kQC0UK0yCF1wotVRNtri+qOg!e&F~bU zzjCanCkFz(`~w~1lepqH=`60U#Bvs#&AUZof+j8f7ubSKz_6ujrGP@^Js~l7>t3I8L7hKg%&SsCB zaGbo3HUGCFmo%JLmRR!sbIJgT9r63RRQ&tdry(8&rmBM+US4Ypy*EgY2@@`Fw)hQ;C!qipO1uB)%}1FE)5%9^!cIF|==5S#FXF1eNSL2J|R2?(>mu=LiF1Ftm zagO7&7Yx$W3oO3zG+Ix`)U22;C^S-F3n8yN&ekT2Y5rY9R+zd zH?gYIDUN;*v*N4i^%2=iTo9tie)YRrBZv2p4)jxe#?h^a&ak;h1WluGtAxE|R4Qhl zoRP@&h#hb9tOyTn&E{2@y_FvBbCFcH9<*cmQ=P?0B!!) zUR~;$KE+%pC;e8F2H42_Zm=;BvH&oXk*Y-1HbAQqcTZ#I?PrqJO?h;<(%V@?pcPl; zP=}&Qx9Y#}+`#vv0MQIw>-Rob6KGEHyX~9>(ZBr(S-c=IMvaXTVcL!rLs%Qo0>pZI z%%2HJ#|U8~U;^Ys)$Y@TWq_v|tPi3_)ptjf`+ogNf~>aA{x`P~d_C=~DgPY^nB;@6NN6Lc17dR$7J zHSu7r;_FMuL4U=E!}2;RL--6K7!}|=`M(4@Gd-`k6Qqmy-*or6Oc zFr;B~A_E~j;DuQ3Q4XyIWr`Se$K~hKa#_#&18TMgfC%;VzqMC@aJJ|u>gxJBf>yiR z)e)Lk1DosVx90yL`O%d5w{_=m+{aT10D4&d)UV37FF!nU@&fT!x*^;!q!#*HHc;)0 zrIUYiHI^F2#_M(aU4dv_TZ0Ml#uf*&m73LNm_M$q7HXMqua2VtJbk7{xh_CoX|>n^ z=ov*vfGl>jkmqf1D!BgAIuu}BdHU~*B98Ge%T{AaL=qEH%l=lq`~5u<;L!8}qQ({f zBl3^|>RBT33~K)X;cmy{Wu2BM4gmZ88l_YwmZnhG7Es&;2t`I)1Mz){AW8sZJ%x|D z(&ioX=MM)!iGnieOsmRO(hi3JaBc@JG{Ug=6$XI+Tm3kZ7jS}jjHYB`U>l*1>HriO zpvyPaR&!X8d<8%|j5aRu^z>A8sz|NUXx=&WBe-!EstpP^?yHZPX1o201WxOC!b5H)`<50 zeXg{!YObtWbWyOr7@*quHv1zb2$GowuH?-|BR#omU$4f5hy*qc0 zO%A4i@Bhjqa+_{1hT_VbK)s$GT&gW+(4XbcdtU$}EbpEHI6#Gu!AgHj|FriuV!&6VFbB%NqCyF;+yx^Zo7ZEm`cL{AqpdPeO&FX6!x?#C^r z#l~jr<#^xq<9I)P?|V|D=#>MA4`u%0)qrt}6LCNnM@`3uS4T_NC%t6KW!k(KK?oto zn|I1lq4fQ#GO4n$M&7=@zIHAy8ta8$FnA8~{tp)5kBLP%xmaLOy60 z_TjXgHfwN{-yWx+;rt@~hBhm4;3J(go%=K(IXN)_A+e3gQLVy^m(M3ozyi@20bZkm%026d&de2ms*nMA zzNTlIWa!R}o;L{~?Uu-C`Fp(*|CQZsrtIWQ!BXys7bidZhOt!-W7ZUp{0D zK>Hg2BJd1H4%eX^MG97ft2UbS) z8DX08Gssx5mYA3MCG%s}j3N5h8>b1VyxD98xuKEKlqB0tUmUQP%pCYmcLQMUSyDz7 zg&%ML?osC;Yg_m_EbLiC&O~pqZc6J9U%B#nJKetYF*h`)$6;&XgrTw^F*$4PM^G1d{0k?Je`4~!w6yrkFLDYk~Yqgf1t)B%jSGF;%E zr^C9HcVA$vN#$ef)GaHoa3Fh_>sP?20gcPVaYPQ1raS<)UwnV^u6S$F&l4W{bme=Ugo%ZCP$4*IEMNQMr| zg!Rf^0Q_jkS_+WY;D!&8B>by8J3)X-d{K^y{g5J8Ua>~G`qWrVlxiMVdvL-rqlsyD^d|J&)a)gvN4JcTng1{nL zYEt4x9bG4LW8NM`vqR#VqytvB6OSF8MWdrs7=B7vAs*}0_+&?FIPh>G>aNA zxU%n&m$H019}o1oUNRd>hw%>S@{ogC6kZpPclT4De=x66f}pr^C}se4NTm}m&^9*1M)Y2pXR$E>0@eH+hHzT z$R1(uv5XI*^r|v~z4Z`ejZ#4?IUDFVNtb78$lki16qgsmwQsKBDoseRvJ&bA5TNI|Jg z3d%A0l;sFJ!n9u@^Spn_%+-nRKS7>QB=L^d$r+)Dhf0@)kJI6a;#KX&P%`nAqATqimd#TvU-<2=%5EQF>8EW|e#O%cFr^lvSB(GWFp>OX#(LbQ` z#gX~eY2YYuD_G#~hue&JhPE=wLKkLKgS@78$Qh1awk~k<$S3%6di&>FlFH34vJdZ^ zE$G1(1&{StrZ{PKn4{FEPU>v7p_XjW?E!Z~M%SaV7+>JD_PC-dab^On4JF5y98MQr|Qr7%~?Z}xgR zJw2c=G5C{%id^14g7e3hSRY?5V2&D6S~X35H(`FB>Zx(0edXwal zjlk~gvt$-cXOpCUUdw9@siyJ6e~;4`?A8f|NVL=GK5eWI5v3Y+d5*}3?j(OO{&rk1 zZ9s%9)v|HDY+DfK@&#KAmrO69*eMfO5ZK=1fW3LoC(hBlbnH}sv7lxl1wVPb< zG$<94&#iT@D1OlBF-aC*4V_rg3+Dn-tVpk2TtRw{PKa`2Kd(NXl_n zCMWLr+pO?ldW@Mxv%3Q%@Vkbce;pOV#p{2&e&Jw`Z!Ex*I?Nsp*tF4wLs9F{Gi^?6 z>VNFKr?EDd2gR#eNj~9IO`uA9DI!p1dLw9A`Cll8F#aF{9Jde4u*fJWlE>+eD$<{X zvmteT88((__sYZNpvf_e%RLt%0DClb!=>mC$Ffg{9+oAEEx{ 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 bbLRy5?7MPo)W3%>fIr<^2AUt$?IZsWCUftV literal 0 HcmV?d00001 diff --git a/charts/arcadia/Chart.lock b/charts/arcadia/Chart.lock new file mode 100644 index 000000000..b2d84c11c --- /dev/null +++ b/charts/arcadia/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: minio + repository: "" + version: 5.0.10 +digest: sha256:789ce227756398b7c76fd611551cc373f5fb87a1bfab09563539786d01db0cb3 +generated: "2023-10-29T01:33:42.169253+08:00" diff --git a/charts/arcadia/Chart.yaml b/charts/arcadia/Chart.yaml index 46a6f4b0c..3cf4d7da6 100644 --- a/charts/arcadia/Chart.yaml +++ b/charts/arcadia/Chart.yaml @@ -2,14 +2,20 @@ apiVersion: v2 name: arcadia description: A Helm chart(KubeBB Component) for KubeAGI Arcadia type: application -version: 0.1.9 +version: 0.1.10 appVersion: "0.0.1" + keywords: - - kubeagi - - NativeAI - LLMOps + - Kubernetes + sources: - https://github.com/kubeagi/arcadia + maintainers: - name: bjwswang url: https://github.com/bjwswang + +dependencies: + - name: minio + version: 5.0.10 diff --git a/charts/arcadia/charts/minio/Chart.yaml b/charts/arcadia/charts/minio/Chart.yaml new file mode 100644 index 000000000..8cea2298f --- /dev/null +++ b/charts/arcadia/charts/minio/Chart.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +appVersion: RELEASE.2023-02-10T18-48-39Z +description: Multi-Cloud Object Storage +home: https://min.io +icon: https://min.io/resources/img/logo/MINIO_wordmark.png +keywords: +- minio +- storage +- object-storage +- s3 +- cluster +maintainers: +- email: dev@minio.io + name: MinIO, Inc +name: minio +sources: +- https://github.com/minio/minio +version: 5.0.10 diff --git a/charts/arcadia/charts/minio/README.md b/charts/arcadia/charts/minio/README.md new file mode 100644 index 000000000..6de4fb16b --- /dev/null +++ b/charts/arcadia/charts/minio/README.md @@ -0,0 +1,260 @@ +# MinIO Helm Chart + +[![Slack](https://slack.min.io/slack?type=svg)](https://slack.min.io) [![license](https://img.shields.io/badge/license-AGPL%20V3-blue)](https://github.com/minio/minio/blob/master/LICENSE) + +MinIO is a High Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads. + +For more detailed documentation please visit [here](https://min.io/docs/minio/linux/index.html) + +## Introduction + +This chart bootstraps MinIO Cluster on [Kubernetes](http://kubernetes.io) using the [Helm](https://helm.sh) package manager. + +## Prerequisites + +- Helm cli with Kubernetes cluster configured. +- PV provisioner support in the underlying infrastructure. (We recommend using ) +- Use Kubernetes version v1.19 and later for best experience. + +## Configure MinIO Helm repo + +```bash +helm repo add minio https://charts.min.io/ +``` + +### Installing the Chart + +Install this chart using: + +```bash +helm install --namespace minio --set rootUser=rootuser,rootPassword=rootpass123 --generate-name minio/minio +``` + +The command deploys MinIO on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation. + +### Installing the Chart (toy-setup) + +Minimal toy setup for testing purposes can be deployed using: + +```bash +helm install --set resources.requests.memory=512Mi --set replicas=1 --set persistence.enabled=false --set mode=standalone --set rootUser=rootuser,rootPassword=rootpass123 --generate-name minio/minio +``` + +### Upgrading the Chart + +You can use Helm to update MinIO version in a live release. Assuming your release is named as `my-release`, get the values using the command: + +```bash +helm get values my-release > old_values.yaml +``` + +Then change the field `image.tag` in `old_values.yaml` file with MinIO image tag you want to use. Now update the chart using + +```bash +helm upgrade -f old_values.yaml my-release minio/minio +``` + +Default upgrade strategies are specified in the `values.yaml` file. Update these fields if you'd like to use a different strategy. + +### Configuration + +Refer the [Values file](./values.yaml) for all the possible config fields. + +You can specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, + +```bash +helm install --name my-release --set persistence.size=1Ti minio/minio +``` + +The above command deploys MinIO server with a 1Ti backing persistent volume. + +Alternately, you can provide a YAML file that specifies parameter values while installing the chart. For example, + +```bash +helm install --name my-release -f values.yaml minio/minio +``` + +### Persistence + +This chart provisions a PersistentVolumeClaim and mounts corresponding persistent volume to default location `/export`. You'll need physical storage available in the Kubernetes cluster for this to work. If you'd rather use `emptyDir`, disable PersistentVolumeClaim by: + +```bash +helm install --set persistence.enabled=false minio/minio +``` + +> *"An emptyDir volume is first created when a Pod is assigned to a Node, and exists as long as that Pod is running on that node. When a Pod is removed from a node for any reason, the data in the emptyDir is deleted forever."* + +### Existing PersistentVolumeClaim + +If a Persistent Volume Claim already exists, specify it during installation. + +1. Create the PersistentVolume +2. Create the PersistentVolumeClaim +3. Install the chart + +```bash +helm install --set persistence.existingClaim=PVC_NAME minio/minio +``` + +### NetworkPolicy + +To enable network policy for MinIO, +install [a networking plugin that implements the Kubernetes +NetworkPolicy spec](https://kubernetes.io/docs/tasks/administer-cluster/declare-network-policy#before-you-begin), +and set `networkPolicy.enabled` to `true`. + +For Kubernetes v1.5 & v1.6, you must also turn on NetworkPolicy by setting +the DefaultDeny namespace annotation. Note: this will enforce policy for *all* pods in the namespace: + +``` +kubectl annotate namespace default "net.beta.kubernetes.io/network-policy={\"ingress\":{\"isolation\":\"DefaultDeny\"}}" +``` + +With NetworkPolicy enabled, traffic will be limited to just port 9000. + +For more precise policy, set `networkPolicy.allowExternal=true`. This will +only allow pods with the generated client label to connect to MinIO. +This label will be displayed in the output of a successful install. + +### Existing secret + +Instead of having this chart create the secret for you, you can supply a preexisting secret, much +like an existing PersistentVolumeClaim. + +First, create the secret: + +```bash +kubectl create secret generic my-minio-secret --from-literal=rootUser=foobarbaz --from-literal=rootPassword=foobarbazqux +``` + +Then install the chart, specifying that you want to use an existing secret: + +```bash +helm install --set existingSecret=my-minio-secret minio/minio +``` + +The following fields are expected in the secret: + +| .data.\ in Secret | Corresponding variable | Description | Required | +|:------------------------|:-----------------------|:---------------|:---------| +| `rootUser` | `rootUser` | Root user. | yes | +| `rootPassword` | `rootPassword` | Root password. | yes | + +All corresponding variables will be ignored in values file. + +### Configure TLS + +To enable TLS for MinIO containers, acquire TLS certificates from a CA or create self-signed certificates. While creating / acquiring certificates ensure the corresponding domain names are set as per the standard [DNS naming conventions](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-identity) in a Kubernetes StatefulSet (for a distributed MinIO setup). Then create a secret using + +```bash +kubectl create secret generic tls-ssl-minio --from-file=path/to/private.key --from-file=path/to/public.crt +``` + +Then install the chart, specifying that you want to use the TLS secret: + +```bash +helm install --set tls.enabled=true,tls.certSecret=tls-ssl-minio minio/minio +``` + +### Installing certificates from third party CAs + +MinIO can connect to other servers, including MinIO nodes or other server types such as NATs and Redis. If these servers use certificates that were not registered with a known CA, add trust for these certificates to MinIO Server by bundling these certificates into a Kubernetes secret and providing it to Helm via the `trustedCertsSecret` value. If `.Values.tls.enabled` is `true` and you're installing certificates for third party CAs, remember to include MinIO's own certificate with key `public.crt`, if it also needs to be trusted. + +For instance, given that TLS is enabled and you need to add trust for MinIO's own CA and for the CA of a Keycloak server, a Kubernetes secret can be created from the certificate files using `kubectl`: + +``` +kubectl -n minio create secret generic minio-trusted-certs --from-file=public.crt --from-file=keycloak.crt +``` + +If TLS is not enabled, you would need only the third party CA: + +``` +kubectl -n minio create secret generic minio-trusted-certs --from-file=keycloak.crt +``` + +The name of the generated secret can then be passed to Helm using a values file or the `--set` parameter: + +``` +trustedCertsSecret: "minio-trusted-certs" + +or + +--set trustedCertsSecret=minio-trusted-certs +``` + +### Create buckets after install + +Install the chart, specifying the buckets you want to create after install: + +```bash +helm install --set buckets[0].name=bucket1,buckets[0].policy=none,buckets[0].purge=false minio/minio +``` + +Description of the configuration parameters used above - + +- `buckets[].name` - name of the bucket to create, must be a string with length > 0 +- `buckets[].policy` - can be one of none|download|upload|public +- `buckets[].purge` - purge if bucket exists already + +### Create policies after install + +Install the chart, specifying the policies you want to create after install: + +```bash +helm install --set policies[0].name=mypolicy,policies[0].statements[0].resources[0]='arn:aws:s3:::bucket1',policies[0].statements[0].actions[0]='s3:ListBucket',policies[0].statements[0].actions[1]='s3:GetObject' minio/minio +``` + +Description of the configuration parameters used above - + +- `policies[].name` - name of the policy to create, must be a string with length > 0 +- `policies[].statements[]` - list of statements, includes actions and resources +- `policies[].statements[].resources[]` - list of resources that applies the statement +- `policies[].statements[].actions[]` - list of actions granted + +### Create user after install + +Install the chart, specifying the users you want to create after install: + +```bash +helm install --set users[0].accessKey=accessKey,users[0].secretKey=secretKey,users[0].policy=none,users[1].accessKey=accessKey2,users[1].secretRef=existingSecret,users[1].secretKey=password,users[1].policy=none minio/minio +``` + +Description of the configuration parameters used above - + +- `users[].accessKey` - accessKey of user +- `users[].secretKey` - secretKey of usersecretRef +- `users[].existingSecret` - secret name that contains the secretKey of user +- `users[].existingSecretKey` - data key in existingSecret secret containing the secretKey +- `users[].policy` - name of the policy to assign to user + +### Create service account after install + +Install the chart, specifying the service accounts you want to create after install: + +```bash +helm install --set svcaccts[0].accessKey=accessKey,svcaccts[0].secretKey=secretKey,svcaccts[0].user=parentUser,svcaccts[1].accessKey=accessKey2,svcaccts[1].secretRef=existingSecret,svcaccts[1].secretKey=password,svcaccts[1].user=parentUser2 minio/minio +``` + +Description of the configuration parameters used above - + +- `svcaccts[].accessKey` - accessKey of service account +- `svcaccts[].secretKey` - secretKey of svcacctsecretRef +- `svcaccts[].existingSecret` - secret name that contains the secretKey of service account +- `svcaccts[].existingSecretKey` - data key in existingSecret secret containing the secretKey +- `svcaccts[].user` - name of the parent user to assign to service account + +## Uninstalling the Chart + +Assuming your release is named as `my-release`, delete it using the command: + +```bash +helm delete my-release +``` + +or + +```bash +helm uninstall my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. diff --git a/charts/arcadia/charts/minio/templates/NOTES.txt b/charts/arcadia/charts/minio/templates/NOTES.txt new file mode 100644 index 000000000..7051b1e62 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/NOTES.txt @@ -0,0 +1,43 @@ +{{- if eq .Values.service.type "ClusterIP" "NodePort" }} +MinIO can be accessed via port {{ .Values.service.port }} on the following DNS name from within your cluster: +{{ template "minio.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local + +To access MinIO from localhost, run the below commands: + + 1. export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + + 2. kubectl port-forward $POD_NAME 9000 --namespace {{ .Release.Namespace }} + +Read more about port forwarding here: http://kubernetes.io/docs/user-guide/kubectl/kubectl_port-forward/ + +You can now access MinIO server on http://localhost:9000. Follow the below steps to connect to MinIO server with mc client: + + 1. Download the MinIO mc client - https://min.io/docs/minio/linux/reference/minio-mc.html#quickstart + + 2. export MC_HOST_{{ template "minio.fullname" . }}-local=http://$(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "minio.secretName" . }} -o jsonpath="{.data.rootUser}" | base64 --decode):$(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "minio.secretName" . }} -o jsonpath="{.data.rootPassword}" | base64 --decode)@localhost:{{ .Values.service.port }} + + 3. mc ls {{ template "minio.fullname" . }}-local + +{{- end }} +{{- if eq .Values.service.type "LoadBalancer" }} +MinIO can be accessed via port {{ .Values.service.port }} on an external IP address. Get the service external IP address by: +kubectl get svc --namespace {{ .Release.Namespace }} -l app={{ template "minio.fullname" . }} + +Note that the public IP may take a couple of minutes to be available. + +You can now access MinIO server on http://:9000. Follow the below steps to connect to MinIO server with mc client: + + 1. Download the MinIO mc client - https://min.io/docs/minio/linux/reference/minio-mc.html#quickstart + + 2. export MC_HOST_{{ template "minio.fullname" . }}-local=http://$(kubectl get secret {{ template "minio.secretName" . }} --namespace {{ .Release.Namespace }} -o jsonpath="{.data.rootUser}" | base64 --decode):$(kubectl get secret {{ template "minio.secretName" . }} -o jsonpath="{.data.rootPassword}" | base64 --decode)@:{{ .Values.service.port }} + + 3. mc ls {{ template "minio.fullname" . }} + +Alternately, you can use your browser or the MinIO SDK to access the server - https://min.io/docs/minio/linux/reference/minio-server/minio-server.html +{{- end }} + +{{ if and (.Values.networkPolicy.enabled) (not .Values.networkPolicy.allowExternal) }} +Note: Since NetworkPolicy is enabled, only pods with label +{{ template "minio.fullname" . }}-client=true" +will be able to connect to this minio cluster. +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/_helper_create_bucket.txt b/charts/arcadia/charts/minio/templates/_helper_create_bucket.txt new file mode 100644 index 000000000..f8d5cd833 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/_helper_create_bucket.txt @@ -0,0 +1,127 @@ +#!/bin/sh +set -e ; # Have script exit in the event of a failed command. + +{{- if .Values.configPathmc }} +MC_CONFIG_DIR="{{ .Values.configPathmc }}" +MC="/usr/bin/mc --insecure --config-dir ${MC_CONFIG_DIR}" +{{- else }} +MC="/usr/bin/mc --insecure" +{{- end }} + +# connectToMinio +# Use a check-sleep-check loop to wait for MinIO service to be available +connectToMinio() { + SCHEME=$1 + ATTEMPTS=0 ; LIMIT=29 ; # Allow 30 attempts + set -e ; # fail if we can't read the keys. + ACCESS=$(cat /config/rootUser) ; SECRET=$(cat /config/rootPassword) ; + set +e ; # The connections to minio are allowed to fail. + echo "Connecting to MinIO server: $SCHEME://$MINIO_ENDPOINT:$MINIO_PORT" ; + MC_COMMAND="${MC} alias set myminio $SCHEME://$MINIO_ENDPOINT:$MINIO_PORT $ACCESS $SECRET" ; + $MC_COMMAND ; + STATUS=$? ; + until [ $STATUS = 0 ] + do + ATTEMPTS=`expr $ATTEMPTS + 1` ; + echo \"Failed attempts: $ATTEMPTS\" ; + if [ $ATTEMPTS -gt $LIMIT ]; then + exit 1 ; + fi ; + sleep 2 ; # 1 second intervals between attempts + $MC_COMMAND ; + STATUS=$? ; + done ; + set -e ; # reset `e` as active + return 0 +} + +# checkBucketExists ($bucket) +# Check if the bucket exists, by using the exit code of `mc ls` +checkBucketExists() { + BUCKET=$1 + CMD=$(${MC} stat myminio/$BUCKET > /dev/null 2>&1) + return $? +} + +# createBucket ($bucket, $policy, $purge) +# Ensure bucket exists, purging if asked to +createBucket() { + BUCKET=$1 + POLICY=$2 + PURGE=$3 + VERSIONING=$4 + OBJECTLOCKING=$5 + + # Purge the bucket, if set & exists + # Since PURGE is user input, check explicitly for `true` + if [ $PURGE = true ]; then + if checkBucketExists $BUCKET ; then + echo "Purging bucket '$BUCKET'." + set +e ; # don't exit if this fails + ${MC} rm -r --force myminio/$BUCKET + set -e ; # reset `e` as active + else + echo "Bucket '$BUCKET' does not exist, skipping purge." + fi + fi + +# Create the bucket if it does not exist and set objectlocking if enabled (NOTE: versioning will be not changed if OBJECTLOCKING is set because it enables versioning to the Buckets created) +if ! checkBucketExists $BUCKET ; then + if [ ! -z $OBJECTLOCKING ] ; then + if [ $OBJECTLOCKING = true ] ; then + echo "Creating bucket with OBJECTLOCKING '$BUCKET'" + ${MC} mb --with-lock myminio/$BUCKET + elif [ $OBJECTLOCKING = false ] ; then + echo "Creating bucket '$BUCKET'" + ${MC} mb myminio/$BUCKET + fi + elif [ -z $OBJECTLOCKING ] ; then + echo "Creating bucket '$BUCKET'" + ${MC} mb myminio/$BUCKET + else + echo "Bucket '$BUCKET' already exists." + fi + fi + + + # set versioning for bucket if objectlocking is disabled or not set + if [ -z $OBJECTLOCKING ] ; then + if [ ! -z $VERSIONING ] ; then + if [ $VERSIONING = true ] ; then + echo "Enabling versioning for '$BUCKET'" + ${MC} version enable myminio/$BUCKET + elif [ $VERSIONING = false ] ; then + echo "Suspending versioning for '$BUCKET'" + ${MC} version suspend myminio/$BUCKET + fi + fi + else + echo "Bucket '$BUCKET' versioning unchanged." + fi + + + # At this point, the bucket should exist, skip checking for existence + # Set policy on the bucket + echo "Setting policy of bucket '$BUCKET' to '$POLICY'." + if [ $POLICY != "custom" ]; then + ${MC} anonymous set $POLICY myminio/$BUCKET + else + ${MC} anonymous set-json /config/bucket_policy_$BUCKET.json myminio/$BUCKET + fi +} + +# Try connecting to MinIO instance +{{- if .Values.tls.enabled }} +scheme=https +{{- else }} +scheme=http +{{- end }} +connectToMinio $scheme + +{{ if .Values.buckets }} +{{ $global := . }} +# Create the buckets +{{- range .Values.buckets }} +createBucket {{ tpl .name $global }} {{ .policy | default "none" | quote }} {{ .purge | default false }} {{ .versioning | default false }} {{ .objectlocking | default false }} +{{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/_helper_create_policy.txt b/charts/arcadia/charts/minio/templates/_helper_create_policy.txt new file mode 100644 index 000000000..d565b161e --- /dev/null +++ b/charts/arcadia/charts/minio/templates/_helper_create_policy.txt @@ -0,0 +1,75 @@ +#!/bin/sh +set -e ; # Have script exit in the event of a failed command. + +{{- if .Values.configPathmc }} +MC_CONFIG_DIR="{{ .Values.configPathmc }}" +MC="/usr/bin/mc --insecure --config-dir ${MC_CONFIG_DIR}" +{{- else }} +MC="/usr/bin/mc --insecure" +{{- end }} + +# connectToMinio +# Use a check-sleep-check loop to wait for MinIO service to be available +connectToMinio() { + SCHEME=$1 + ATTEMPTS=0 ; LIMIT=29 ; # Allow 30 attempts + set -e ; # fail if we can't read the keys. + ACCESS=$(cat /config/rootUser) ; SECRET=$(cat /config/rootPassword) ; + set +e ; # The connections to minio are allowed to fail. + echo "Connecting to MinIO server: $SCHEME://$MINIO_ENDPOINT:$MINIO_PORT" ; + MC_COMMAND="${MC} alias set myminio $SCHEME://$MINIO_ENDPOINT:$MINIO_PORT $ACCESS $SECRET" ; + $MC_COMMAND ; + STATUS=$? ; + until [ $STATUS = 0 ] + do + ATTEMPTS=`expr $ATTEMPTS + 1` ; + echo \"Failed attempts: $ATTEMPTS\" ; + if [ $ATTEMPTS -gt $LIMIT ]; then + exit 1 ; + fi ; + sleep 2 ; # 1 second intervals between attempts + $MC_COMMAND ; + STATUS=$? ; + done ; + set -e ; # reset `e` as active + return 0 +} + +# checkPolicyExists ($policy) +# Check if the policy exists, by using the exit code of `mc admin policy info` +checkPolicyExists() { + POLICY=$1 + CMD=$(${MC} admin policy info myminio $POLICY > /dev/null 2>&1) + return $? +} + +# createPolicy($name, $filename) +createPolicy () { + NAME=$1 + FILENAME=$2 + + # Create the name if it does not exist + echo "Checking policy: $NAME (in /config/$FILENAME.json)" + if ! checkPolicyExists $NAME ; then + echo "Creating policy '$NAME'" + else + echo "Policy '$NAME' already exists." + fi + ${MC} admin policy add myminio $NAME /config/$FILENAME.json + +} + +# Try connecting to MinIO instance +{{- if .Values.tls.enabled }} +scheme=https +{{- else }} +scheme=http +{{- end }} +connectToMinio $scheme + +{{ if .Values.policies }} +# Create the policies +{{- range $idx, $policy := .Values.policies }} +createPolicy {{ $policy.name }} policy_{{ $idx }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/arcadia/charts/minio/templates/_helper_create_svcacct.txt b/charts/arcadia/charts/minio/templates/_helper_create_svcacct.txt new file mode 100644 index 000000000..59f51b177 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/_helper_create_svcacct.txt @@ -0,0 +1,106 @@ +#!/bin/sh +set -e ; # Have script exit in the event of a failed command. + +{{- if .Values.configPathmc }} +MC_CONFIG_DIR="{{ .Values.configPathmc }}" +MC="/usr/bin/mc --insecure --config-dir ${MC_CONFIG_DIR}" +{{- else }} +MC="/usr/bin/mc --insecure" +{{- end }} + +# AccessKey and secretkey credentials file are added to prevent shell execution errors caused by special characters. +# Special characters for example : ',",<,>,{,} +MINIO_ACCESSKEY_SECRETKEY_TMP="/tmp/accessKey_and_secretKey_svcacct_tmp" + +# connectToMinio +# Use a check-sleep-check loop to wait for MinIO service to be available +connectToMinio() { + SCHEME=$1 + ATTEMPTS=0 ; LIMIT=29 ; # Allow 30 attempts + set -e ; # fail if we can't read the keys. + ACCESS=$(cat /config/rootUser) ; SECRET=$(cat /config/rootPassword) ; + set +e ; # The connections to minio are allowed to fail. + echo "Connecting to MinIO server: $SCHEME://$MINIO_ENDPOINT:$MINIO_PORT" ; + MC_COMMAND="${MC} alias set myminio $SCHEME://$MINIO_ENDPOINT:$MINIO_PORT $ACCESS $SECRET" ; + $MC_COMMAND ; + STATUS=$? ; + until [ $STATUS = 0 ] + do + ATTEMPTS=`expr $ATTEMPTS + 1` ; + echo \"Failed attempts: $ATTEMPTS\" ; + if [ $ATTEMPTS -gt $LIMIT ]; then + exit 1 ; + fi ; + sleep 2 ; # 2 second intervals between attempts + $MC_COMMAND ; + STATUS=$? ; + done ; + set -e ; # reset `e` as active + return 0 +} + +# checkSvcacctExists () +# Check if the svcacct exists, by using the exit code of `mc admin user svcacct info` +checkSvcacctExists() { + CMD=$(${MC} admin user svcacct info myminio $(head -1 $MINIO_ACCESSKEY_SECRETKEY_TMP) > /dev/null 2>&1) + return $? +} + +# createSvcacct ($user) +createSvcacct () { + USER=$1 + FILENAME=$2 + #check accessKey_and_secretKey_tmp file + if [[ ! -f $MINIO_ACCESSKEY_SECRETKEY_TMP ]];then + echo "credentials file does not exist" + return 1 + fi + if [[ $(cat $MINIO_ACCESSKEY_SECRETKEY_TMP|wc -l) -ne 2 ]];then + echo "credentials file is invalid" + rm -f $MINIO_ACCESSKEY_SECRETKEY_TMP + return 1 + fi + SVCACCT=$(head -1 $MINIO_ACCESSKEY_SECRETKEY_TMP) + # Create the svcacct if it does not exist + if ! checkSvcacctExists ; then + echo "Creating svcacct '$SVCACCT'" + # Check if policy file is define + if [ -z $FILENAME ]; then + ${MC} admin user svcacct add --access-key $(head -1 $MINIO_ACCESSKEY_SECRETKEY_TMP) --secret-key $(tail -n1 $MINIO_ACCESSKEY_SECRETKEY_TMP) myminio $USER + else + ${MC} admin user svcacct add --access-key $(head -1 $MINIO_ACCESSKEY_SECRETKEY_TMP) --secret-key $(tail -n1 $MINIO_ACCESSKEY_SECRETKEY_TMP) --policy /config/$FILENAME.json myminio $USER + fi + else + echo "Svcacct '$SVCACCT' already exists." + fi + #clean up credentials files. + rm -f $MINIO_ACCESSKEY_SECRETKEY_TMP +} + +# Try connecting to MinIO instance +{{- if .Values.tls.enabled }} +scheme=https +{{- else }} +scheme=http +{{- end }} +connectToMinio $scheme + +{{ if .Values.svcaccts }} +{{ $global := . }} +# Create the svcaccts +{{- range $idx, $svc := .Values.svcaccts }} +echo {{ tpl .accessKey $global }} > $MINIO_ACCESSKEY_SECRETKEY_TMP +{{- if .existingSecret }} +cat /config/secrets-svc/{{ tpl .existingSecret $global }}/{{ tpl .existingSecretKey $global }} >> $MINIO_ACCESSKEY_SECRETKEY_TMP +# Add a new line if it doesn't exist +sed -i '$a\' $MINIO_ACCESSKEY_SECRETKEY_TMP +{{ else }} +echo {{ .secretKey }} >> $MINIO_ACCESSKEY_SECRETKEY_TMP +{{- end }} +{{- if $svc.policy}} +createSvcacct {{ .user }} svc_policy_{{ $idx }} +{{ else }} +createSvcacct {{ .user }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/_helper_create_user.txt b/charts/arcadia/charts/minio/templates/_helper_create_user.txt new file mode 100644 index 000000000..324bc9d48 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/_helper_create_user.txt @@ -0,0 +1,105 @@ +#!/bin/sh +set -e ; # Have script exit in the event of a failed command. + +{{- if .Values.configPathmc }} +MC_CONFIG_DIR="{{ .Values.configPathmc }}" +MC="/usr/bin/mc --insecure --config-dir ${MC_CONFIG_DIR}" +{{- else }} +MC="/usr/bin/mc --insecure" +{{- end }} + +# AccessKey and secretkey credentials file are added to prevent shell execution errors caused by special characters. +# Special characters for example : ',",<,>,{,} +MINIO_ACCESSKEY_SECRETKEY_TMP="/tmp/accessKey_and_secretKey_tmp" + +# connectToMinio +# Use a check-sleep-check loop to wait for MinIO service to be available +connectToMinio() { + SCHEME=$1 + ATTEMPTS=0 ; LIMIT=29 ; # Allow 30 attempts + set -e ; # fail if we can't read the keys. + ACCESS=$(cat /config/rootUser) ; SECRET=$(cat /config/rootPassword) ; + set +e ; # The connections to minio are allowed to fail. + echo "Connecting to MinIO server: $SCHEME://$MINIO_ENDPOINT:$MINIO_PORT" ; + MC_COMMAND="${MC} alias set myminio $SCHEME://$MINIO_ENDPOINT:$MINIO_PORT $ACCESS $SECRET" ; + $MC_COMMAND ; + STATUS=$? ; + until [ $STATUS = 0 ] + do + ATTEMPTS=`expr $ATTEMPTS + 1` ; + echo \"Failed attempts: $ATTEMPTS\" ; + if [ $ATTEMPTS -gt $LIMIT ]; then + exit 1 ; + fi ; + sleep 2 ; # 1 second intervals between attempts + $MC_COMMAND ; + STATUS=$? ; + done ; + set -e ; # reset `e` as active + return 0 +} + +# checkUserExists () +# Check if the user exists, by using the exit code of `mc admin user info` +checkUserExists() { + CMD=$(${MC} admin user info myminio $(head -1 $MINIO_ACCESSKEY_SECRETKEY_TMP) > /dev/null 2>&1) + return $? +} + +# createUser ($policy) +createUser() { + POLICY=$1 + #check accessKey_and_secretKey_tmp file + if [[ ! -f $MINIO_ACCESSKEY_SECRETKEY_TMP ]];then + echo "credentials file does not exist" + return 1 + fi + if [[ $(cat $MINIO_ACCESSKEY_SECRETKEY_TMP|wc -l) -ne 2 ]];then + echo "credentials file is invalid" + rm -f $MINIO_ACCESSKEY_SECRETKEY_TMP + return 1 + fi + USER=$(head -1 $MINIO_ACCESSKEY_SECRETKEY_TMP) + # Create the user if it does not exist + if ! checkUserExists ; then + echo "Creating user '$USER'" + cat $MINIO_ACCESSKEY_SECRETKEY_TMP | ${MC} admin user add myminio + else + echo "User '$USER' already exists." + fi + #clean up credentials files. + rm -f $MINIO_ACCESSKEY_SECRETKEY_TMP + + # set policy for user + if [ ! -z $POLICY -a $POLICY != " " ] ; then + echo "Adding policy '$POLICY' for '$USER'" + ${MC} admin policy set myminio $POLICY user=$USER + else + echo "User '$USER' has no policy attached." + fi +} + +# Try connecting to MinIO instance +{{- if .Values.tls.enabled }} +scheme=https +{{- else }} +scheme=http +{{- end }} +connectToMinio $scheme + +{{ if .Values.users }} +{{ $global := . }} +# Create the users +{{- range .Values.users }} +echo {{ tpl .accessKey $global }} > $MINIO_ACCESSKEY_SECRETKEY_TMP +{{- if .existingSecret }} +cat /config/secrets/{{ tpl .existingSecret $global }}/{{ tpl .existingSecretKey $global }} >> $MINIO_ACCESSKEY_SECRETKEY_TMP +# Add a new line if it doesn't exist +sed -i '$a\' $MINIO_ACCESSKEY_SECRETKEY_TMP +createUser {{ .policy }} +{{ else }} +echo {{ .secretKey }} >> $MINIO_ACCESSKEY_SECRETKEY_TMP +createUser {{ .policy }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/_helper_custom_command.txt b/charts/arcadia/charts/minio/templates/_helper_custom_command.txt new file mode 100644 index 000000000..b583a7782 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/_helper_custom_command.txt @@ -0,0 +1,58 @@ +#!/bin/sh +set -e ; # Have script exit in the event of a failed command. + +{{- if .Values.configPathmc }} +MC_CONFIG_DIR="{{ .Values.configPathmc }}" +MC="/usr/bin/mc --insecure --config-dir ${MC_CONFIG_DIR}" +{{- else }} +MC="/usr/bin/mc --insecure" +{{- end }} + +# connectToMinio +# Use a check-sleep-check loop to wait for MinIO service to be available +connectToMinio() { + SCHEME=$1 + ATTEMPTS=0 ; LIMIT=29 ; # Allow 30 attempts + set -e ; # fail if we can't read the keys. + ACCESS=$(cat /config/rootUser) ; SECRET=$(cat /config/rootPassword) ; + set +e ; # The connections to minio are allowed to fail. + echo "Connecting to MinIO server: $SCHEME://$MINIO_ENDPOINT:$MINIO_PORT" ; + MC_COMMAND="${MC} alias set myminio $SCHEME://$MINIO_ENDPOINT:$MINIO_PORT $ACCESS $SECRET" ; + $MC_COMMAND ; + STATUS=$? ; + until [ $STATUS = 0 ] + do + ATTEMPTS=`expr $ATTEMPTS + 1` ; + echo \"Failed attempts: $ATTEMPTS\" ; + if [ $ATTEMPTS -gt $LIMIT ]; then + exit 1 ; + fi ; + sleep 2 ; # 1 second intervals between attempts + $MC_COMMAND ; + STATUS=$? ; + done ; + set -e ; # reset `e` as active + return 0 +} + +# runCommand ($@) +# Run custom mc command +runCommand() { + ${MC} "$@" + return $? +} + +# Try connecting to MinIO instance +{{- if .Values.tls.enabled }} +scheme=https +{{- else }} +scheme=http +{{- end }} +connectToMinio $scheme + +{{ if .Values.customCommands }} +# Run custom commands +{{- range .Values.customCommands }} +runCommand {{ .command }} +{{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/_helper_policy.tpl b/charts/arcadia/charts/minio/templates/_helper_policy.tpl new file mode 100644 index 000000000..f2150530b --- /dev/null +++ b/charts/arcadia/charts/minio/templates/_helper_policy.tpl @@ -0,0 +1,28 @@ +{{- $statements_length := len .statements -}} +{{- $statements_length := sub $statements_length 1 -}} +{ + "Version": "2012-10-17", + "Statement": [ +{{- range $i, $statement := .statements }} + { + "Effect": "Allow", + "Action": [ +"{{ $statement.actions | join "\",\n\"" }}" + ]{{ if $statement.resources }}, + "Resource": [ +"{{ $statement.resources | join "\",\n\"" }}" + ]{{ end }} +{{- if $statement.conditions }} +{{- $condition_len := len $statement.conditions }} +{{- $condition_len := sub $condition_len 1 }} + , + "Condition": { + {{- range $k,$v := $statement.conditions }} + {{- range $operator,$object := $v }} + "{{ $operator }}": { {{ $object }} }{{- if lt $k $condition_len }},{{- end }} + {{- end }}{{- end }} + }{{- end }} + }{{ if lt $i $statements_length }},{{end }} +{{- end }} + ] +} diff --git a/charts/arcadia/charts/minio/templates/_helpers.tpl b/charts/arcadia/charts/minio/templates/_helpers.tpl new file mode 100644 index 000000000..2fe061d34 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/_helpers.tpl @@ -0,0 +1,218 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "minio.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "minio.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "minio.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for networkpolicy. +*/}} +{{- define "minio.networkPolicy.apiVersion" -}} +{{- if semverCompare ">=1.4-0, <1.7-0" .Capabilities.KubeVersion.Version -}} +{{- print "extensions/v1beta1" -}} +{{- else if semverCompare ">=1.7-0, <1.16-0" .Capabilities.KubeVersion.Version -}} +{{- print "networking.k8s.io/v1beta1" -}} +{{- else if semverCompare "^1.16-0" .Capabilities.KubeVersion.Version -}} +{{- print "networking.k8s.io/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for deployment. +*/}} +{{- define "minio.deployment.apiVersion" -}} +{{- if semverCompare "<1.9-0" .Capabilities.KubeVersion.Version -}} +{{- print "apps/v1beta2" -}} +{{- else -}} +{{- print "apps/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for statefulset. +*/}} +{{- define "minio.statefulset.apiVersion" -}} +{{- if semverCompare "<1.16-0" .Capabilities.KubeVersion.Version -}} +{{- print "apps/v1beta2" -}} +{{- else -}} +{{- print "apps/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for ingress. +*/}} +{{- define "minio.ingress.apiVersion" -}} +{{- if semverCompare "<1.14-0" .Capabilities.KubeVersion.GitVersion -}} +{{- print "extensions/v1beta1" -}} +{{- else if semverCompare "<1.19-0" .Capabilities.KubeVersion.GitVersion -}} +{{- print "networking.k8s.io/v1beta1" -}} +{{- else -}} +{{- print "networking.k8s.io/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for console ingress. +*/}} +{{- define "minio.consoleIngress.apiVersion" -}} +{{- if semverCompare "<1.14-0" .Capabilities.KubeVersion.GitVersion -}} +{{- print "extensions/v1beta1" -}} +{{- else if semverCompare "<1.19-0" .Capabilities.KubeVersion.GitVersion -}} +{{- print "networking.k8s.io/v1beta1" -}} +{{- else -}} +{{- print "networking.k8s.io/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Determine secret name. +*/}} +{{- define "minio.secretName" -}} +{{- if .Values.existingSecret -}} +{{- .Values.existingSecret }} +{{- else -}} +{{- include "minio.fullname" . -}} +{{- end -}} +{{- end -}} + +{{/* +Determine name for scc role and rolebinding +*/}} +{{- define "minio.sccRoleName" -}} +{{- printf "%s-%s" "scc" (include "minio.fullname" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Properly format optional additional arguments to MinIO binary +*/}} +{{- define "minio.extraArgs" -}} +{{- range .Values.extraArgs -}} +{{ " " }}{{ . }} +{{- end -}} +{{- end -}} + +{{/* +Return the proper Docker Image Registry Secret Names +*/}} +{{- define "minio.imagePullSecrets" -}} +{{/* +Helm 2.11 supports the assignment of a value to a variable defined in a different scope, +but Helm 2.9 and 2.10 does not support it, so we need to implement this if-else logic. +Also, we can not use a single if because lazy evaluation is not an option +*/}} +{{- if .Values.global }} +{{- if .Values.global.imagePullSecrets }} +imagePullSecrets: +{{- range .Values.global.imagePullSecrets }} + - name: {{ . }} +{{- end }} +{{- else if .Values.imagePullSecrets }} +imagePullSecrets: + {{ toYaml .Values.imagePullSecrets }} +{{- end -}} +{{- else if .Values.imagePullSecrets }} +imagePullSecrets: + {{ toYaml .Values.imagePullSecrets }} +{{- end -}} +{{- end -}} + +{{/* +Formats volumeMount for MinIO TLS keys and trusted certs +*/}} +{{- define "minio.tlsKeysVolumeMount" -}} +{{- if .Values.tls.enabled }} +- name: cert-secret-volume + mountPath: {{ .Values.certsPath }} +{{- end }} +{{- if or .Values.tls.enabled (ne .Values.trustedCertsSecret "") }} +{{- $casPath := printf "%s/CAs" .Values.certsPath | clean }} +- name: trusted-cert-secret-volume + mountPath: {{ $casPath }} +{{- end }} +{{- end -}} + +{{/* +Formats volume for MinIO TLS keys and trusted certs +*/}} +{{- define "minio.tlsKeysVolume" -}} +{{- if .Values.tls.enabled }} +- name: cert-secret-volume + secret: + secretName: {{ tpl .Values.tls.certSecret $ }} + items: + - key: {{ .Values.tls.publicCrt }} + path: public.crt + - key: {{ .Values.tls.privateKey }} + path: private.key +{{- end }} +{{- if or .Values.tls.enabled (ne .Values.trustedCertsSecret "") }} +{{- $certSecret := eq .Values.trustedCertsSecret "" | ternary .Values.tls.certSecret .Values.trustedCertsSecret }} +{{- $publicCrt := eq .Values.trustedCertsSecret "" | ternary .Values.tls.publicCrt "" }} +- name: trusted-cert-secret-volume + secret: + secretName: {{ $certSecret }} + {{- if ne $publicCrt "" }} + items: + - key: {{ $publicCrt }} + path: public.crt + {{- end }} +{{- end }} +{{- end -}} + +{{/* +Returns the available value for certain key in an existing secret (if it exists), +otherwise it generates a random value. +*/}} +{{- define "minio.getValueFromSecret" }} + {{- $len := (default 16 .Length) | int -}} + {{- $obj := (lookup "v1" "Secret" .Namespace .Name).data -}} + {{- if $obj }} + {{- index $obj .Key | b64dec -}} + {{- else -}} + {{- randAlphaNum $len -}} + {{- end -}} +{{- end }} + +{{- define "minio.root.username" -}} + {{- if .Values.rootUser }} + {{- .Values.rootUser | toString }} + {{- else }} + {{- include "minio.getValueFromSecret" (dict "Namespace" .Release.Namespace "Name" (include "minio.fullname" .) "Length" 20 "Key" "rootUser") }} + {{- end }} +{{- end -}} + +{{- define "minio.root.password" -}} + {{- if .Values.rootPassword }} + {{- .Values.rootPassword | toString }} + {{- else }} + {{- include "minio.getValueFromSecret" (dict "Namespace" .Release.Namespace "Name" (include "minio.fullname" .) "Length" 40 "Key" "rootPassword") }} + {{- end }} +{{- end -}} diff --git a/charts/arcadia/charts/minio/templates/configmap.yaml b/charts/arcadia/charts/minio/templates/configmap.yaml new file mode 100644 index 000000000..0c70b290c --- /dev/null +++ b/charts/arcadia/charts/minio/templates/configmap.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "minio.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +data: + initialize: |- +{{ include (print $.Template.BasePath "/_helper_create_bucket.txt") . | indent 4 }} + add-user: |- +{{ include (print $.Template.BasePath "/_helper_create_user.txt") . | indent 4 }} + add-policy: |- +{{ include (print $.Template.BasePath "/_helper_create_policy.txt") . | indent 4 }} +{{- range $idx, $policy := .Values.policies }} + # Policy: {{ $policy.name }} + policy_{{ $idx }}.json: |- +{{ include (print $.Template.BasePath "/_helper_policy.tpl") . | indent 4 }} +{{ end }} +{{- range $idx, $policy := .Values.buckets }} + # BucketPolicy: {{ $policy.name }} + bucket_policy_{{ $policy.name }}.json: |- +{{ $policy.customPolicy | toJson | indent 4 }} +{{ end }} +{{- range $idx, $svc := .Values.svcaccts }} +{{- if $svc.policy }} + # SVC: {{ $svc.accessKey }} + svc_policy_{{ $idx }}.json: |- +{{ include (print $.Template.BasePath "/_helper_policy.tpl") .policy | indent 4 }} +{{- end }} +{{ end }} + add-svcacct: |- +{{ include (print $.Template.BasePath "/_helper_create_svcacct.txt") . | indent 4 }} + custom-command: |- +{{ include (print $.Template.BasePath "/_helper_custom_command.txt") . | indent 4 }} diff --git a/charts/arcadia/charts/minio/templates/console-ingress.yaml b/charts/arcadia/charts/minio/templates/console-ingress.yaml new file mode 100644 index 000000000..2ce9a93bf --- /dev/null +++ b/charts/arcadia/charts/minio/templates/console-ingress.yaml @@ -0,0 +1,58 @@ +{{- if .Values.consoleIngress.enabled -}} +{{- $fullName := printf "%s-console" (include "minio.fullname" .) -}} +{{- $servicePort := .Values.consoleService.port -}} +{{- $ingressPath := .Values.consoleIngress.path -}} +apiVersion: {{ template "minio.consoleIngress.apiVersion" . }} +kind: Ingress +metadata: + name: {{ $fullName }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.consoleIngress.labels }} +{{ toYaml . | indent 4 }} +{{- end }} + +{{- with .Values.consoleIngress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.consoleIngress.ingressClassName }} + ingressClassName: {{ .Values.consoleIngress.ingressClassName }} +{{- end }} +{{- if .Values.consoleIngress.tls }} + tls: + {{- range .Values.consoleIngress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.consoleIngress.hosts }} + - http: + paths: + - path: {{ $ingressPath }} + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + pathType: Prefix + backend: + service: + name: {{ $fullName }} + port: + number: {{ $servicePort }} + {{- else }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $servicePort }} + {{- end }} + {{- if . }} + host: {{ . | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/console-service.yaml b/charts/arcadia/charts/minio/templates/console-service.yaml new file mode 100644 index 000000000..46da35974 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/console-service.yaml @@ -0,0 +1,48 @@ +{{ $scheme := "http" }} +{{- if .Values.tls.enabled }} +{{ $scheme = "https" }} +{{ end }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "minio.fullname" . }}-console + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- if .Values.consoleService.annotations }} + annotations: +{{ toYaml .Values.consoleService.annotations | indent 4 }} +{{- end }} +spec: +{{- if (or (eq .Values.consoleService.type "ClusterIP" "") (empty .Values.consoleService.type)) }} + type: ClusterIP + {{- if not (empty .Values.consoleService.clusterIP) }} + clusterIP: {{ .Values.consoleService.clusterIP }} + {{end}} +{{- else if eq .Values.consoleService.type "LoadBalancer" }} + type: {{ .Values.consoleService.type }} + loadBalancerIP: {{ default "" .Values.consoleService.loadBalancerIP }} +{{- else }} + type: {{ .Values.consoleService.type }} +{{- end }} + ports: + - name: {{ $scheme }} + port: {{ .Values.consoleService.port }} + protocol: TCP +{{- if (and (eq .Values.consoleService.type "NodePort") ( .Values.consoleService.nodePort)) }} + nodePort: {{ .Values.consoleService.nodePort }} +{{- else }} + targetPort: {{ .Values.minioConsolePort }} +{{- end}} +{{- if .Values.consoleService.externalIPs }} + externalIPs: +{{- range $i , $ip := .Values.consoleService.externalIPs }} + - {{ $ip }} +{{- end }} +{{- end }} + selector: + app: {{ template "minio.name" . }} + release: {{ .Release.Name }} diff --git a/charts/arcadia/charts/minio/templates/deployment.yaml b/charts/arcadia/charts/minio/templates/deployment.yaml new file mode 100644 index 000000000..ad92c892b --- /dev/null +++ b/charts/arcadia/charts/minio/templates/deployment.yaml @@ -0,0 +1,206 @@ +{{- if eq .Values.mode "standalone" }} +{{ $scheme := "http" }} +{{- if .Values.tls.enabled }} +{{ $scheme = "https" }} +{{ end }} +{{ $bucketRoot := or ($.Values.bucketRoot) ($.Values.mountPath) }} +apiVersion: {{ template "minio.deployment.apiVersion" . }} +kind: Deployment +metadata: + name: {{ template "minio.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- if .Values.additionalLabels }} +{{ toYaml .Values.additionalLabels | trimSuffix "\n" | indent 4 }} +{{- end }} +{{- if .Values.additionalAnnotations }} + annotations: +{{ toYaml .Values.additionalAnnotations | trimSuffix "\n" | indent 4 }} +{{- end }} +spec: + strategy: + type: {{ .Values.DeploymentUpdate.type }} + {{- if eq .Values.DeploymentUpdate.type "RollingUpdate" }} + rollingUpdate: + maxSurge: {{ .Values.DeploymentUpdate.maxSurge }} + maxUnavailable: {{ .Values.DeploymentUpdate.maxUnavailable }} + {{- end}} + replicas: 1 + selector: + matchLabels: + app: {{ template "minio.name" . }} + release: {{ .Release.Name }} + template: + metadata: + name: {{ template "minio.fullname" . }} + labels: + app: {{ template "minio.name" . }} + release: {{ .Release.Name }} +{{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} +{{- end }} + annotations: +{{- if not .Values.ignoreChartChecksums }} + checksum/secrets: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }} + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} +{{- end }} +{{- if .Values.podAnnotations }} +{{ toYaml .Values.podAnnotations | trimSuffix "\n" | indent 8 }} +{{- end }} + spec: + {{- if .Values.priorityClassName }} + priorityClassName: "{{ .Values.priorityClassName }}" + {{- end }} + {{- if .Values.runtimeClassName }} + runtimeClassName: "{{ .Values.runtimeClassName }}" + {{- end }} +{{- if and .Values.securityContext.enabled .Values.persistence.enabled }} + securityContext: + runAsUser: {{ .Values.securityContext.runAsUser }} + runAsGroup: {{ .Values.securityContext.runAsGroup }} + fsGroup: {{ .Values.securityContext.fsGroup }} + {{- if and (ge .Capabilities.KubeVersion.Major "1") (ge .Capabilities.KubeVersion.Minor "20") }} + fsGroupChangePolicy: {{ .Values.securityContext.fsGroupChangePolicy }} + {{- end }} +{{- end }} +{{ if .Values.serviceAccount.create }} + serviceAccountName: {{ .Values.serviceAccount.name }} +{{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - "/bin/sh" + - "-ce" + - "/usr/bin/docker-entrypoint.sh minio server {{ $bucketRoot }} -S {{ .Values.certsPath }} --address :{{ .Values.minioAPIPort }} --console-address :{{ .Values.minioConsolePort }} {{- template "minio.extraArgs" . }}" + volumeMounts: + - name: minio-user + mountPath: "/tmp/credentials" + readOnly: true + - name: export + mountPath: {{ .Values.mountPath }} + {{- if and .Values.persistence.enabled .Values.persistence.subPath }} + subPath: "{{ .Values.persistence.subPath }}" + {{- end }} + {{- if .Values.extraSecret }} + - name: extra-secret + mountPath: "/tmp/minio-config-env" + {{- end }} + {{- include "minio.tlsKeysVolumeMount" . | indent 12 }} + {{- if .Values.extraVolumeMounts }} + {{- toYaml .Values.extraVolumeMounts | nindent 12 }} + {{- end }} + ports: + - name: {{ $scheme }} + containerPort: {{ .Values.minioAPIPort }} + - name: {{ $scheme }}-console + containerPort: {{ .Values.minioConsolePort }} + env: + - name: MINIO_ROOT_USER + valueFrom: + secretKeyRef: + name: {{ template "minio.secretName" . }} + key: rootUser + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "minio.secretName" . }} + key: rootPassword + {{- if .Values.extraSecret }} + - name: MINIO_CONFIG_ENV_FILE + value: "/tmp/minio-config-env/config.env" + {{- end}} + {{- if .Values.metrics.serviceMonitor.public }} + - name: MINIO_PROMETHEUS_AUTH_TYPE + value: "public" + {{- end}} + {{- if .Values.oidc.enabled }} + - name: MINIO_IDENTITY_OPENID_CONFIG_URL + value: {{ .Values.oidc.configUrl }} + - name: MINIO_IDENTITY_OPENID_CLIENT_ID + value: {{ .Values.oidc.clientId }} + - name: MINIO_IDENTITY_OPENID_CLIENT_SECRET + value: {{ .Values.oidc.clientSecret }} + - name: MINIO_IDENTITY_OPENID_CLAIM_NAME + value: {{ .Values.oidc.claimName }} + - name: MINIO_IDENTITY_OPENID_CLAIM_PREFIX + value: {{ .Values.oidc.claimPrefix }} + - name: MINIO_IDENTITY_OPENID_SCOPES + value: {{ .Values.oidc.scopes }} + - name: MINIO_IDENTITY_OPENID_REDIRECT_URI + value: {{ .Values.oidc.redirectUri }} + - name: MINIO_IDENTITY_OPENID_COMMENT + value: {{ .Values.oidc.comment }} + {{- end}} + {{- if .Values.etcd.endpoints }} + - name: MINIO_ETCD_ENDPOINTS + value: {{ join "," .Values.etcd.endpoints | quote }} + {{- if .Values.etcd.clientCert }} + - name: MINIO_ETCD_CLIENT_CERT + value: "/tmp/credentials/etcd_client_cert.pem" + {{- end }} + {{- if .Values.etcd.clientCertKey }} + - name: MINIO_ETCD_CLIENT_CERT_KEY + value: "/tmp/credentials/etcd_client_cert_key.pem" + {{- end }} + {{- if .Values.etcd.pathPrefix }} + - name: MINIO_ETCD_PATH_PREFIX + value: {{ .Values.etcd.pathPrefix }} + {{- end }} + {{- if .Values.etcd.corednsPathPrefix }} + - name: MINIO_ETCD_COREDNS_PATH + value: {{ .Values.etcd.corednsPathPrefix }} + {{- end }} + {{- end }} + {{- range $key, $val := .Values.environment }} + - name: {{ $key }} + value: {{ $val | quote }} + {{- end}} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.extraContainers }} + {{- if eq (typeOf .) "string" }} + {{- tpl . $ | nindent 8 }} + {{- else }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} +{{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} +{{- end }} +{{- include "minio.imagePullSecrets" . | indent 6 }} +{{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} +{{- end }} +{{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} +{{- end }} + volumes: + - name: export + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (include "minio.fullname" .) }} + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.extraSecret }} + - name: extra-secret + secret: + secretName: {{ .Values.extraSecret }} + {{- end }} + - name: minio-user + secret: + secretName: {{ template "minio.secretName" . }} + {{- include "minio.tlsKeysVolume" . | indent 8 }} + {{- if .Values.extraVolumes }} + {{ toYaml .Values.extraVolumes | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/ingress.yaml b/charts/arcadia/charts/minio/templates/ingress.yaml new file mode 100644 index 000000000..8d9a837dc --- /dev/null +++ b/charts/arcadia/charts/minio/templates/ingress.yaml @@ -0,0 +1,58 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "minio.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: {{ template "minio.ingress.apiVersion" . }} +kind: Ingress +metadata: + name: {{ $fullName }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.labels }} +{{ toYaml . | indent 4 }} +{{- end }} + +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.ingressClassName }} + ingressClassName: {{ .Values.ingress.ingressClassName }} +{{- end }} +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - http: + paths: + - path: {{ $ingressPath }} + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + pathType: Prefix + backend: + service: + name: {{ $fullName }} + port: + number: {{ $servicePort }} + {{- else }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $servicePort }} + {{- end }} + {{- if . }} + host: {{ . | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/networkpolicy.yaml b/charts/arcadia/charts/minio/templates/networkpolicy.yaml new file mode 100644 index 000000000..ac219b937 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/networkpolicy.yaml @@ -0,0 +1,27 @@ +{{- if .Values.networkPolicy.enabled }} +kind: NetworkPolicy +apiVersion: {{ template "minio.networkPolicy.apiVersion" . }} +metadata: + name: {{ template "minio.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + podSelector: + matchLabels: + app: {{ template "minio.name" . }} + release: {{ .Release.Name }} + ingress: + - ports: + - port: {{ .Values.minioAPIPort }} + - port: {{ .Values.minioConsolePort }} + {{- if not .Values.networkPolicy.allowExternal }} + from: + - podSelector: + matchLabels: + {{ template "minio.name" . }}-client: "true" + {{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/poddisruptionbudget.yaml b/charts/arcadia/charts/minio/templates/poddisruptionbudget.yaml new file mode 100644 index 000000000..41c649aa2 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/poddisruptionbudget.yaml @@ -0,0 +1,18 @@ +{{- if .Values.podDisruptionBudget.enabled }} +{{- if .Capabilities.APIVersions.Has "policy/v1beta1/PodDisruptionBudget" }} +apiVersion: policy/v1beta1 +{{- else }} +apiVersion: policy/v1 +{{- end }} +kind: PodDisruptionBudget +metadata: + name: minio + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} +spec: + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + selector: + matchLabels: + app: {{ template "minio.name" . }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/post-job.yaml b/charts/arcadia/charts/minio/templates/post-job.yaml new file mode 100644 index 000000000..47839b81f --- /dev/null +++ b/charts/arcadia/charts/minio/templates/post-job.yaml @@ -0,0 +1,272 @@ +{{- if or .Values.buckets .Values.users .Values.policies .Values.customCommands .Values.svcaccts }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ template "minio.fullname" . }}-post-job + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }}-post-job + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation + {{- with .Values.postJob.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + template: + metadata: + labels: + app: {{ template "minio.name" . }}-job + release: {{ .Release.Name }} + {{- if .Values.podLabels }} + {{- toYaml .Values.podLabels | nindent 8 }} + {{- end }} + {{- if .Values.postJob.podAnnotations }} + annotations: + {{- toYaml .Values.postJob.podAnnotations | nindent 8 }} + {{- end }} + spec: + restartPolicy: OnFailure + {{- include "minio.imagePullSecrets" . | nindent 6 }} + {{- if .Values.nodeSelector }} + nodeSelector: + {{- toYaml .Values.postJob.nodeSelector | nindent 8 }} + {{- end }} + {{- with .Values.postJob.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.postJob.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.postJob.securityContext.enabled }} + securityContext: + runAsUser: {{ .Values.postJob.securityContext.runAsUser }} + runAsGroup: {{ .Values.postJob.securityContext.runAsGroup }} + fsGroup: {{ .Values.postJob.securityContext.fsGroup }} + {{- end }} + volumes: + - name: minio-configuration + projected: + sources: + - configMap: + name: {{ template "minio.fullname" . }} + - secret: + name: {{ template "minio.secretName" . }} + {{- range (concat .Values.users (default (list) .Values.svcaccts)) }} + {{- if .existingSecret }} + - secret: + name: {{ tpl .existingSecret $ }} + items: + - key: {{ .existingSecretKey }} + path: secrets/{{ tpl .existingSecret $ }}/{{ tpl .existingSecretKey $ }} + {{- end }} + {{- end }} + {{- range ( default list .Values.svcaccts ) }} + {{- if .existingSecret }} + - secret: + name: {{ tpl .existingSecret $ }} + items: + - key: {{ .existingSecretKey }} + path: secrets-svc/{{ tpl .existingSecret $ }}/{{ tpl .existingSecretKey $ }} + {{- end }} + {{- end }} + {{- if .Values.tls.enabled }} + - name: cert-secret-volume-mc + secret: + secretName: {{ .Values.tls.certSecret }} + items: + - key: {{ .Values.tls.publicCrt }} + path: CAs/public.crt + {{ end }} + containers: + {{- if .Values.buckets }} + - name: minio-make-bucket + image: "{{ .Values.mcImage.repository }}:{{ .Values.mcImage.tag }}" + {{- if .Values.makeBucketJob.securityContext.enabled }} + securityContext: + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + runAsNonRoot: true + capabilities: + drop: + - ALL + runAsUser: {{ .Values.makeBucketJob.securityContext.runAsUser }} + runAsGroup: {{ .Values.makeBucketJob.securityContext.runAsGroup }} + {{- end }} + imagePullPolicy: {{ .Values.mcImage.pullPolicy }} + {{- if .Values.makeBucketJob.exitCommand }} + command: [ "/bin/sh", "-c" ] + args: [ "/bin/sh /config/initialize; EV=$?; {{ .Values.makeBucketJob.exitCommand }} && exit $EV" ] + {{- else }} + command: [ "/bin/sh", "/config/initialize" ] + {{- end }} + env: + - name: MINIO_ENDPOINT + value: {{ template "minio.fullname" . }} + - name: MINIO_PORT + value: {{ .Values.service.port | quote }} + volumeMounts: + - name: minio-configuration + mountPath: /config + {{- if .Values.tls.enabled }} + - name: cert-secret-volume-mc + mountPath: {{ .Values.configPathmc }}certs + {{ end }} + resources: + {{- toYaml .Values.makeBucketJob.resources | nindent 12 }} + {{- end }} + {{- if .Values.users }} + - name: minio-make-user + image: "{{ .Values.mcImage.repository }}:{{ .Values.mcImage.tag }}" + {{- if .Values.makeUserJob.securityContext.enabled }} + securityContext: + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + runAsNonRoot: true + capabilities: + drop: + - ALL + runAsUser: {{ .Values.makeUserJob.securityContext.runAsUser }} + runAsGroup: {{ .Values.makeUserJob.securityContext.runAsGroup }} + {{- end }} + imagePullPolicy: {{ .Values.mcImage.pullPolicy }} + {{- if .Values.makeUserJob.exitCommand }} + command: [ "/bin/sh", "-c" ] + args: [ "/bin/sh /config/add-user; EV=$?; {{ .Values.makeUserJob.exitCommand }} && exit $EV" ] + {{- else }} + command: [ "/bin/sh", "/config/add-user" ] + {{- end }} + env: + - name: MINIO_ENDPOINT + value: {{ template "minio.fullname" . }} + - name: MINIO_PORT + value: {{ .Values.service.port | quote }} + volumeMounts: + - name: minio-configuration + mountPath: /config + {{- if .Values.tls.enabled }} + - name: cert-secret-volume-mc + mountPath: {{ .Values.configPathmc }}certs + {{ end }} + resources: + {{- toYaml .Values.makeUserJob.resources | nindent 12 }} + {{- end }} + {{- if .Values.policies }} + - name: minio-make-policy + image: "{{ .Values.mcImage.repository }}:{{ .Values.mcImage.tag }}" + {{- if .Values.makePolicyJob.securityContext.enabled }} + securityContext: + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + runAsNonRoot: true + capabilities: + drop: + - ALL + runAsUser: {{ .Values.makePolicyJob.securityContext.runAsUser }} + runAsGroup: {{ .Values.makePolicyJob.securityContext.runAsGroup }} + {{- end }} + imagePullPolicy: {{ .Values.mcImage.pullPolicy }} + {{- if .Values.makePolicyJob.exitCommand }} + command: [ "/bin/sh", "-c" ] + args: [ "/bin/sh /config/add-policy; EV=$?; {{ .Values.makePolicyJob.exitCommand }} && exit $EV" ] + {{- else }} + command: [ "/bin/sh", "/config/add-policy" ] + {{- end }} + env: + - name: MINIO_ENDPOINT + value: {{ template "minio.fullname" . }} + - name: MINIO_PORT + value: {{ .Values.service.port | quote }} + volumeMounts: + - name: minio-configuration + mountPath: /config + {{- if .Values.tls.enabled }} + - name: cert-secret-volume-mc + mountPath: {{ .Values.configPathmc }}certs + {{ end }} + resources: + {{- toYaml .Values.makePolicyJob.resources | nindent 12 }} + {{- end }} + {{- if .Values.customCommands }} + - name: minio-custom-command + image: "{{ .Values.mcImage.repository }}:{{ .Values.mcImage.tag }}" + {{- if .Values.customCommandJob.securityContext.enabled }} + securityContext: + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + runAsNonRoot: true + capabilities: + drop: + - ALL + runAsUser: {{ .Values.customCommandJob.securityContext.runAsUser }} + runAsGroup: {{ .Values.customCommandJob.securityContext.runAsGroup }} + {{- end }} + imagePullPolicy: {{ .Values.mcImage.pullPolicy }} + {{- if .Values.customCommandJob.exitCommand }} + command: [ "/bin/sh", "-c" ] + args: [ "/bin/sh /config/custom-command; EV=$?; {{ .Values.customCommandJob.exitCommand }} && exit $EV" ] + {{- else }} + command: [ "/bin/sh", "/config/custom-command" ] + {{- end }} + env: + - name: MINIO_ENDPOINT + value: {{ template "minio.fullname" . }} + - name: MINIO_PORT + value: {{ .Values.service.port | quote }} + volumeMounts: + - name: minio-configuration + mountPath: /config + {{- if .Values.tls.enabled }} + - name: cert-secret-volume-mc + mountPath: {{ .Values.configPathmc }}certs + {{ end }} + resources: + {{- toYaml .Values.customCommandJob.resources | nindent 12 }} + {{- end }} + {{- if .Values.svcaccts }} + - name: minio-make-svcacct + image: "{{ .Values.mcImage.repository }}:{{ .Values.mcImage.tag }}" + {{- if .Values.makeServiceAccountJob.securityContext.enabled }} + securityContext: + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + runAsNonRoot: true + capabilities: + drop: + - ALL + runAsUser: {{ .Values.makeServiceAccountJob.securityContext.runAsUser }} + runAsGroup: {{ .Values.makeServiceAccountJob.securityContext.runAsGroup }} + {{- end }} + imagePullPolicy: {{ .Values.mcImage.pullPolicy }} + {{- if .Values.makeServiceAccountJob.exitCommand }} + command: [ "/bin/sh", "-c" ] + args: ["/bin/sh /config/add-svcacct; EV=$?; {{ .Values.makeServiceAccountJob.exitCommand }} && exit $EV" ] + {{- else }} + command: ["/bin/sh", "/config/add-svcacct"] + {{- end }} + env: + - name: MINIO_ENDPOINT + value: {{ template "minio.fullname" . }} + - name: MINIO_PORT + value: {{ .Values.service.port | quote }} + volumeMounts: + - name: minio-configuration + mountPath: /config + {{- if .Values.tls.enabled }} + - name: cert-secret-volume-mc + mountPath: {{ .Values.configPathmc }}certs + {{ end }} + resources: + {{- toYaml .Values.makeServiceAccountJob.resources | nindent 12 }} + {{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/pvc.yaml b/charts/arcadia/charts/minio/templates/pvc.yaml new file mode 100644 index 000000000..369aade41 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/pvc.yaml @@ -0,0 +1,35 @@ +{{- if eq .Values.mode "standalone" }} +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ template "minio.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- if .Values.persistence.annotations }} + annotations: +{{ toYaml .Values.persistence.annotations | trimSuffix "\n" | indent 4 }} +{{- end }} +spec: + accessModes: + - {{ .Values.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} + +{{- if .Values.persistence.storageClass }} +{{- if (eq "-" .Values.persistence.storageClass) }} + storageClassName: "" +{{- else }} + storageClassName: "{{ .Values.persistence.storageClass }}" +{{- end }} +{{- end }} +{{- if .Values.persistence.VolumeName }} + volumeName: "{{ .Values.persistence.VolumeName }}" +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/secrets.yaml b/charts/arcadia/charts/minio/templates/secrets.yaml new file mode 100644 index 000000000..da2ecab4a --- /dev/null +++ b/charts/arcadia/charts/minio/templates/secrets.yaml @@ -0,0 +1,22 @@ +{{- if not .Values.existingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "minio.secretName" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +type: Opaque +data: + rootUser: {{ include "minio.root.username" . | b64enc | quote }} + rootPassword: {{ include "minio.root.password" . | b64enc | quote }} + {{- if .Values.etcd.clientCert }} + etcd_client.crt: {{ .Values.etcd.clientCert | toString | b64enc | quote }} + {{- end }} + {{- if .Values.etcd.clientCertKey }} + etcd_client.key: {{ .Values.etcd.clientCertKey | toString | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/securitycontextconstraints.yaml b/charts/arcadia/charts/minio/templates/securitycontextconstraints.yaml new file mode 100644 index 000000000..4bac7e372 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/securitycontextconstraints.yaml @@ -0,0 +1,45 @@ +{{- if and .Values.securityContext.enabled .Values.persistence.enabled (.Capabilities.APIVersions.Has "security.openshift.io/v1") }} +apiVersion: security.openshift.io/v1 +kind: SecurityContextConstraints +metadata: + name: {{ template "minio.fullname" . }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +allowHostDirVolumePlugin: false +allowHostIPC: false +allowHostNetwork: false +allowHostPID: false +allowHostPorts: false +allowPrivilegeEscalation: true +allowPrivilegedContainer: false +allowedCapabilities: [] +readOnlyRootFilesystem: false +defaultAddCapabilities: [] +requiredDropCapabilities: +- KILL +- MKNOD +- SETUID +- SETGID +fsGroup: + type: MustRunAs + ranges: + - max: {{ .Values.securityContext.fsGroup }} + min: {{ .Values.securityContext.fsGroup }} +runAsUser: + type: MustRunAs + uid: {{ .Values.securityContext.runAsUser }} +seLinuxContext: + type: MustRunAs +supplementalGroups: + type: RunAsAny +volumes: +- configMap +- downwardAPI +- emptyDir +- persistentVolumeClaim +- projected +- secret +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/service.yaml b/charts/arcadia/charts/minio/templates/service.yaml new file mode 100644 index 000000000..741528df4 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/service.yaml @@ -0,0 +1,49 @@ +{{ $scheme := "http" }} +{{- if .Values.tls.enabled }} +{{ $scheme = "https" }} +{{ end }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "minio.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + monitoring: "true" +{{- if .Values.service.annotations }} + annotations: +{{ toYaml .Values.service.annotations | indent 4 }} +{{- end }} +spec: +{{- if (or (eq .Values.service.type "ClusterIP" "") (empty .Values.service.type)) }} + type: ClusterIP + {{- if not (empty .Values.service.clusterIP) }} + clusterIP: {{ .Values.service.clusterIP }} + {{end}} +{{- else if eq .Values.service.type "LoadBalancer" }} + type: {{ .Values.service.type }} + loadBalancerIP: {{ default "" .Values.service.loadBalancerIP }} +{{- else }} + type: {{ .Values.service.type }} +{{- end }} + ports: + - name: {{ $scheme }} + port: {{ .Values.service.port }} + protocol: TCP +{{- if (and (eq .Values.service.type "NodePort") ( .Values.service.nodePort)) }} + nodePort: {{ .Values.service.nodePort }} +{{- else }} + targetPort: {{ .Values.minioAPIPort }} +{{- end}} +{{- if .Values.service.externalIPs }} + externalIPs: +{{- range $i , $ip := .Values.service.externalIPs }} + - {{ $ip }} +{{- end }} +{{- end }} + selector: + app: {{ template "minio.name" . }} + release: {{ .Release.Name }} diff --git a/charts/arcadia/charts/minio/templates/serviceaccount.yaml b/charts/arcadia/charts/minio/templates/serviceaccount.yaml new file mode 100644 index 000000000..6a4bd94b3 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/serviceaccount.yaml @@ -0,0 +1,7 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.serviceAccount.name | quote }} + namespace: {{ .Release.Namespace | quote }} +{{- end -}} diff --git a/charts/arcadia/charts/minio/templates/servicemonitor.yaml b/charts/arcadia/charts/minio/templates/servicemonitor.yaml new file mode 100644 index 000000000..955273b52 --- /dev/null +++ b/charts/arcadia/charts/minio/templates/servicemonitor.yaml @@ -0,0 +1,117 @@ +{{- if and .Values.metrics.serviceMonitor.enabled .Values.metrics.serviceMonitor.includeNode}} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ template "minio.fullname" . }} + {{- if .Values.metrics.serviceMonitor.namespace }} + namespace: {{ .Values.metrics.serviceMonitor.namespace }} + {{ else }} + namespace: {{ .Release.Namespace | quote }} + {{- end }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + {{- if .Values.metrics.serviceMonitor.additionalLabels }} +{{ toYaml .Values.metrics.serviceMonitor.additionalLabels | indent 4 }} + {{- end }} +{{- if .Values.metrics.serviceMonitor.annotations }} + annotations: +{{ toYaml .Values.metrics.serviceMonitor.annotations | trimSuffix "\n" | indent 4 }} +{{- end }} +spec: + endpoints: + {{- if .Values.tls.enabled }} + - port: https + scheme: https + tlsConfig: + ca: + secret: + name: {{ .Values.tls.certSecret }} + key: {{ .Values.tls.publicCrt }} + serverName: {{ template "minio.fullname" . }} + {{ else }} + - port: http + scheme: http + {{- end }} + path: /minio/v2/metrics/node + {{- if .Values.metrics.serviceMonitor.interval }} + interval: {{ .Values.metrics.serviceMonitor.interval }} + {{- end }} + {{- if .Values.metrics.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }} + {{- end }} + {{- if .Values.metrics.serviceMonitor.relabelConfigs }} +{{ toYaml .Values.metrics.serviceMonitor.relabelConfigs | indent 6 }} + {{- end }} + {{- if not .Values.metrics.serviceMonitor.public }} + bearerTokenSecret: + name: {{ template "minio.fullname" . }}-prometheus + key: token + {{- end }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace | quote }} + selector: + matchLabels: + app: {{ include "minio.name" . }} + release: {{ .Release.Name }} + monitoring: "true" +{{- end }} +{{- if .Values.metrics.serviceMonitor.enabled }} +--- +apiVersion: monitoring.coreos.com/v1 +kind: Probe +metadata: + name: {{ template "minio.fullname" . }}-cluster + {{- if .Values.metrics.serviceMonitor.namespace }} + namespace: {{ .Values.metrics.serviceMonitor.namespace }} + {{ else }} + namespace: {{ .Release.Namespace | quote }} + {{- end }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + {{- if .Values.metrics.serviceMonitor.additionalLabels }} +{{ toYaml .Values.metrics.serviceMonitor.additionalLabels | indent 4 }} + {{- end }} +spec: + jobName: {{ template "minio.fullname" . }} + {{- if .Values.tls.enabled }} + tlsConfig: + ca: + secret: + name: {{ .Values.tls.certSecret }} + key: {{ .Values.tls.publicCrt }} + serverName: {{ template "minio.fullname" . }} + {{- end }} + prober: + url: {{ template "minio.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.port }} + path: /minio/v2/metrics/cluster + {{- if .Values.tls.enabled }} + scheme: https + {{ else }} + scheme: http + {{- end }} + {{- if .Values.metrics.serviceMonitor.relabelConfigsCluster }} +{{ toYaml .Values.metrics.serviceMonitor.relabelConfigsCluster | indent 2 }} + {{- end }} + targets: + staticConfig: + static: + - {{ template "minio.fullname" . }}.{{ .Release.Namespace }} + {{- if not .Values.metrics.serviceMonitor.public }} + {{- if .Values.metrics.serviceMonitor.interval }} + interval: {{ .Values.metrics.serviceMonitor.interval }} + {{- end }} + {{- if .Values.metrics.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }} + {{- end }} + bearerTokenSecret: + name: {{ template "minio.fullname" . }}-prometheus + key: token + {{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/templates/statefulset.yaml b/charts/arcadia/charts/minio/templates/statefulset.yaml new file mode 100644 index 000000000..ba7c3021a --- /dev/null +++ b/charts/arcadia/charts/minio/templates/statefulset.yaml @@ -0,0 +1,259 @@ +{{- if eq .Values.mode "distributed" }} +{{ $poolCount := .Values.pools | int }} +{{ $nodeCount := .Values.replicas | int }} +{{ $replicas := mul $poolCount $nodeCount }} +{{ $drivesPerNode := .Values.drivesPerNode | int }} +{{ $scheme := "http" }} +{{- if .Values.tls.enabled }} +{{ $scheme = "https" }} +{{ end }} +{{ $mountPath := .Values.mountPath }} +{{ $bucketRoot := or ($.Values.bucketRoot) ($.Values.mountPath) }} +{{ $subPath := .Values.persistence.subPath }} +{{ $penabled := .Values.persistence.enabled }} +{{ $accessMode := .Values.persistence.accessMode }} +{{ $storageClass := .Values.persistence.storageClass }} +{{ $psize := .Values.persistence.size }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "minio.fullname" . }}-svc + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +spec: + publishNotReadyAddresses: true + clusterIP: None + ports: + - name: {{ $scheme }} + port: {{ .Values.service.port }} + protocol: TCP + targetPort: {{ .Values.minioAPIPort }} + selector: + app: {{ template "minio.name" . }} + release: {{ .Release.Name }} +--- +apiVersion: {{ template "minio.statefulset.apiVersion" . }} +kind: StatefulSet +metadata: + name: {{ template "minio.fullname" . }} + namespace: {{ .Release.Namespace | quote }} + labels: + app: {{ template "minio.name" . }} + chart: {{ template "minio.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- if .Values.additionalLabels }} +{{ toYaml .Values.additionalLabels | trimSuffix "\n" | indent 4 }} +{{- end }} +{{- if .Values.additionalAnnotations }} + annotations: +{{ toYaml .Values.additionalAnnotations | trimSuffix "\n" | indent 4 }} +{{- end }} +spec: + updateStrategy: + type: {{ .Values.StatefulSetUpdate.updateStrategy }} + podManagementPolicy: "Parallel" + serviceName: {{ template "minio.fullname" . }}-svc + replicas: {{ $replicas }} + selector: + matchLabels: + app: {{ template "minio.name" . }} + release: {{ .Release.Name }} + template: + metadata: + name: {{ template "minio.fullname" . }} + labels: + app: {{ template "minio.name" . }} + release: {{ .Release.Name }} +{{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} +{{- end }} + annotations: +{{- if not .Values.ignoreChartChecksums }} + checksum/secrets: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }} + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} +{{- end }} +{{- if .Values.podAnnotations }} +{{ toYaml .Values.podAnnotations | trimSuffix "\n" | indent 8 }} +{{- end }} + spec: + {{- if .Values.priorityClassName }} + priorityClassName: "{{ .Values.priorityClassName }}" + {{- end }} + {{- if .Values.runtimeClassName }} + runtimeClassName: "{{ .Values.runtimeClassName }}" + {{- end }} +{{- if and .Values.securityContext.enabled .Values.persistence.enabled }} + securityContext: + runAsUser: {{ .Values.securityContext.runAsUser }} + runAsGroup: {{ .Values.securityContext.runAsGroup }} + fsGroup: {{ .Values.securityContext.fsGroup }} + {{- if and (ge .Capabilities.KubeVersion.Major "1") (ge .Capabilities.KubeVersion.Minor "20") }} + fsGroupChangePolicy: {{ .Values.securityContext.fsGroupChangePolicy }} + {{- end }} +{{- end }} +{{ if .Values.serviceAccount.create }} + serviceAccountName: {{ .Values.serviceAccount.name }} +{{- end }} + containers: + - name: {{ .Chart.Name }} + image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + + command: [ "/bin/sh", + "-ce", + "/usr/bin/docker-entrypoint.sh minio server {{- range $i := until $poolCount }}{{ $factor := mul $i $nodeCount }}{{ $endIndex := add $factor $nodeCount }}{{ $beginIndex := mul $i $nodeCount }} {{ $scheme }}://{{ template `minio.fullname` $ }}-{{ `{` }}{{ $beginIndex }}...{{ sub $endIndex 1 }}{{ `}`}}.{{ template `minio.fullname` $ }}-svc.{{ $.Release.Namespace }}.svc.{{ $.Values.clusterDomain }}{{if (gt $drivesPerNode 1)}}{{ $bucketRoot }}-{{ `{` }}0...{{ sub $drivesPerNode 1 }}{{ `}` }}{{else}}{{ $bucketRoot }}{{end}}{{- end}} -S {{ .Values.certsPath }} --address :{{ .Values.minioAPIPort }} --console-address :{{ .Values.minioConsolePort }} {{- template `minio.extraArgs` . }}" ] + volumeMounts: + {{- if $penabled }} + {{- if (gt $drivesPerNode 1) }} + {{- range $i := until $drivesPerNode }} + - name: export-{{ $i }} + mountPath: {{ $mountPath }}-{{ $i }} + {{- if and $penabled $subPath }} + subPath: {{ $subPath }} + {{- end }} + {{- end }} + {{- else }} + - name: export + mountPath: {{ $mountPath }} + {{- if and $penabled $subPath }} + subPath: {{ $subPath }} + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.extraSecret }} + - name: extra-secret + mountPath: "/tmp/minio-config-env" + {{- end }} + {{- include "minio.tlsKeysVolumeMount" . | indent 12 }} + {{- if .Values.extraVolumeMounts }} + {{- toYaml .Values.extraVolumeMounts | nindent 12 }} + {{- end }} + ports: + - name: {{ $scheme }} + containerPort: {{ .Values.minioAPIPort }} + - name: {{ $scheme }}-console + containerPort: {{ .Values.minioConsolePort }} + env: + - name: MINIO_ROOT_USER + valueFrom: + secretKeyRef: + name: {{ template "minio.secretName" . }} + key: rootUser + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "minio.secretName" . }} + key: rootPassword + {{- if .Values.extraSecret }} + - name: MINIO_CONFIG_ENV_FILE + value: "/tmp/minio-config-env/config.env" + {{- end}} + {{- if .Values.metrics.serviceMonitor.public }} + - name: MINIO_PROMETHEUS_AUTH_TYPE + value: "public" + {{- end}} + {{- if .Values.oidc.enabled }} + - name: MINIO_IDENTITY_OPENID_CONFIG_URL + value: {{ .Values.oidc.configUrl }} + - name: MINIO_IDENTITY_OPENID_CLIENT_ID + value: {{ .Values.oidc.clientId }} + - name: MINIO_IDENTITY_OPENID_CLIENT_SECRET + value: {{ .Values.oidc.clientSecret }} + - name: MINIO_IDENTITY_OPENID_CLAIM_NAME + value: {{ .Values.oidc.claimName }} + - name: MINIO_IDENTITY_OPENID_CLAIM_PREFIX + value: {{ .Values.oidc.claimPrefix }} + - name: MINIO_IDENTITY_OPENID_SCOPES + value: {{ .Values.oidc.scopes }} + - name: MINIO_IDENTITY_OPENID_REDIRECT_URI + value: {{ .Values.oidc.redirectUri }} + - name: MINIO_IDENTITY_OPENID_COMMENT + value: {{ .Values.oidc.comment }} + {{- end}} + {{- range $key, $val := .Values.environment }} + - name: {{ $key }} + value: {{ $val | quote }} + {{- end}} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.extraContainers }} + {{- if eq (typeOf .) "string" }} + {{- tpl . $ | nindent 8 }} + {{- else }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} +{{- include "minio.imagePullSecrets" . | indent 6 }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + {{- if and (gt $replicas 1) (ge .Capabilities.KubeVersion.Major "1") (ge .Capabilities.KubeVersion.Minor "19") }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: +{{ toYaml . | indent 8 }} + {{- end }} + {{- end }} + volumes: + - name: minio-user + secret: + secretName: {{ template "minio.secretName" . }} + {{- if .Values.extraSecret }} + - name: extra-secret + secret: + secretName: {{ .Values.extraSecret }} + {{- end }} + {{- include "minio.tlsKeysVolume" . | indent 8 }} + {{- if .Values.extraVolumes }} + {{ toYaml .Values.extraVolumes | nindent 8 }} + {{- end }} +{{- if .Values.persistence.enabled }} + volumeClaimTemplates: + {{- if gt $drivesPerNode 1 }} + {{- range $diskId := until $drivesPerNode}} + - metadata: + name: export-{{ $diskId }} + {{- if $.Values.persistence.annotations }} + annotations: +{{ toYaml $.Values.persistence.annotations | trimSuffix "\n" | indent 10 }} + {{- end }} + spec: + accessModes: [ {{ $accessMode | quote }} ] + {{- if $storageClass }} + storageClassName: {{ $storageClass }} + {{- end }} + resources: + requests: + storage: {{ $psize }} + {{- end }} + {{- else }} + - metadata: + name: export + {{- if $.Values.persistence.annotations }} + annotations: +{{ toYaml $.Values.persistence.annotations | trimSuffix "\n" | indent 10 }} + {{- end }} + spec: + accessModes: [ {{ $accessMode | quote }} ] + {{- if $storageClass }} + storageClassName: {{ $storageClass }} + {{- end }} + resources: + requests: + storage: {{ $psize }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/arcadia/charts/minio/values.yaml b/charts/arcadia/charts/minio/values.yaml new file mode 100644 index 000000000..f6928418c --- /dev/null +++ b/charts/arcadia/charts/minio/values.yaml @@ -0,0 +1,557 @@ +## Provide a name in place of minio for `app:` labels +## +nameOverride: "" + +## Provide a name to substitute for the full names of resources +## +fullnameOverride: "" + +## set kubernetes cluster domain where minio is running +## +clusterDomain: cluster.local + +## Set default image, imageTag, and imagePullPolicy. mode is used to indicate the +## +image: + repository: hyperledgerk8s/minio-minio + tag: RELEASE.2023-02-10T18-48-39Z + pullPolicy: IfNotPresent + +imagePullSecrets: [] +# - name: "image-pull-secret" + +## Set default image, imageTag, and imagePullPolicy for the `mc` (the minio +## client used to create a default bucket). +## +mcImage: + repository: hyperledgerk8s/minio-mc + tag: RELEASE.2023-01-28T20-29-38Z + pullPolicy: IfNotPresent + +## minio mode, i.e. standalone or distributed +mode: standalone + +## Additional labels to include with deployment or statefulset +additionalLabels: {} + +## Additional annotations to include with deployment or statefulset +additionalAnnotations: {} + +## Typically the deployment/statefulset includes checksums of secrets/config, +## So that when these change on a subsequent helm install, the deployment/statefulset +## is restarted. This can result in unnecessary restarts under GitOps tooling such as +## flux, so set to "true" to disable this behaviour. +ignoreChartChecksums: false + +## Additional arguments to pass to minio binary +extraArgs: [] + +## Additional volumes to minio container +extraVolumes: [] + +## Additional volumeMounts to minio container +extraVolumeMounts: [] + +## Additional sidecar containers +extraContainers: [] + +## Internal port number for MinIO S3 API container +## Change service.port to change external port number +minioAPIPort: "9000" + +## Internal port number for MinIO Browser Console container +## Change consoleService.port to change external port number +minioConsolePort: "9001" + +## Update strategy for Deployments +DeploymentUpdate: + type: RollingUpdate + maxUnavailable: 0 + maxSurge: 100% + +## Update strategy for StatefulSets +StatefulSetUpdate: + updateStrategy: RollingUpdate + +## Pod priority settings +## ref: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/ +## +priorityClassName: "" + +## Pod runtime class name +## ref https://kubernetes.io/docs/concepts/containers/runtime-class/ +## +runtimeClassName: "" + +## Set default rootUser, rootPassword +## AccessKey and secretKey is generated when not set +## Distributed MinIO ref: https://min.io/docs/minio/linux/operations/install-deploy-manage/deploy-minio-multi-node-multi-drive.html +## +rootUser: "" +rootPassword: "" + +## Use existing Secret that store following variables: +## +## | Chart var | .data. in Secret | +## |:----------------------|:-------------------------| +## | rootUser | rootUser | +## | rootPassword | rootPassword | +## +## All mentioned variables will be ignored in values file. +## .data.rootUser and .data.rootPassword are mandatory, +## others depend on enabled status of corresponding sections. +existingSecret: "" + +## Directory on the MinIO pof +certsPath: "/tmp/minio/certs/" +configPathmc: "/tmp/minio/mc/" + +## Path where PV would be mounted on the MinIO Pod +mountPath: "/export" +## Override the root directory which the minio server should serve from. +## If left empty, it defaults to the value of {{ .Values.mountPath }} +## If defined, it must be a sub-directory of the path specified in {{ .Values.mountPath }} +## +bucketRoot: "" + +# Number of drives attached to a node +drivesPerNode: 1 +# Number of MinIO containers running +replicas: 3 +# Number of expanded MinIO clusters +pools: 1 + +## TLS Settings for MinIO +tls: + enabled: false + ## Create a secret with private.key and public.crt files and pass that here. Ref: https://github.com/minio/minio/tree/master/docs/tls/kubernetes#2-create-kubernetes-secret + certSecret: "" + publicCrt: public.crt + privateKey: private.key + +## Trusted Certificates Settings for MinIO. Ref: https://min.io/docs/minio/linux/operations/network-encryption.html#third-party-certificate-authorities +## Bundle multiple trusted certificates into one secret and pass that here. Ref: https://github.com/minio/minio/tree/master/docs/tls/kubernetes#2-create-kubernetes-secret +## When using self-signed certificates, remember to include MinIO's own certificate in the bundle with key public.crt. +## If certSecret is left empty and tls is enabled, this chart installs the public certificate from .Values.tls.certSecret. +trustedCertsSecret: "" + +## Enable persistence using Persistent Volume Claims +## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ +## +persistence: + enabled: false + annotations: {} + + ## A manually managed Persistent Volume and Claim + ## Requires persistence.enabled: true + ## If defined, PVC must be created manually before volume will be bound + existingClaim: "" + + ## minio data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + ## Storage class of PV to bind. By default it looks for standard storage class. + ## If the PV uses a different storage class, specify that here. + storageClass: "" + VolumeName: "" + accessMode: ReadWriteOnce + size: 5Gi + + ## If subPath is set mount a sub folder of a volume instead of the root of the volume. + ## This is especially handy for volume plugins that don't natively support sub mounting (like glusterfs). + ## + subPath: "" + +## Expose the MinIO service to be accessed from outside the cluster (LoadBalancer service). +## or access it from within the cluster (ClusterIP service). Set the service type and the port to serve it. +## ref: http://kubernetes.io/docs/user-guide/services/ +## +service: + type: ClusterIP + clusterIP: ~ + port: "9000" + nodePort: 32000 + +## Configure Ingress based on the documentation here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +## + +ingress: + enabled: false + ingressClassName: "portal-ingress" + labels: + {} + # node-role.kubernetes.io/ingress: platform + + annotations: + {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + # kubernetes.io/ingress.allow-http: "false" + # kubernetes.io/ingress.global-static-ip-name: "" + # nginx.ingress.kubernetes.io/secure-backends: "true" + # nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + # nginx.ingress.kubernetes.io/whitelist-source-range: 0.0.0.0/0 + path: / + hosts: + - minio-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +consoleService: + type: ClusterIP + clusterIP: ~ + port: "9001" + nodePort: 32001 + +consoleIngress: + enabled: true + ingressClassName: "portal-ingress" + labels: + {} + # node-role.kubernetes.io/ingress: platform + + annotations: + {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + # kubernetes.io/ingress.allow-http: "false" + # kubernetes.io/ingress.global-static-ip-name: "" + # nginx.ingress.kubernetes.io/secure-backends: "true" + # nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" + # nginx.ingress.kubernetes.io/whitelist-source-range: 0.0.0.0/0 + path: / + hosts: + - console.minio.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +## Node labels for pod assignment +## Ref: https://kubernetes.io/docs/user-guide/node-selection/ +## +nodeSelector: {} +tolerations: [] +affinity: {} +topologySpreadConstraints: [] + +## Add stateful containers to have security context, if enabled MinIO will run as this +## user and group NOTE: securityContext is only enabled if persistence.enabled=true +securityContext: + enabled: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: "OnRootMismatch" + +# Additational pod annotations +podAnnotations: {} + +# Additional pod labels +podLabels: {} + +## Configure resource requests and limits +## ref: http://kubernetes.io/docs/user-guide/compute-resources/ +## +resources: + requests: + memory: 512Mi + +## List of policies to be created after minio install +## +## In addition to default policies [readonly|readwrite|writeonly|consoleAdmin|diagnostics] +## you can define additional policies with custom supported actions and resources +policies: +## writeexamplepolicy policy grants creation or deletion of buckets with name +## starting with example. In addition, grants objects write permissions on buckets starting with +## example. +# - name: custom +# statements: +# - resources: +# - 'arn:aws:s3:::kubeagi' +# actions: +# - "s3:ListBucket" +# - "s3:GetBucketLocation" +# effect: 'Allow' +# principal: +# AWS: +# - "*" +# - resources: +# - 'arn:aws:s3:::kubeagi/*' +# actions: +# - "s3:GetObject" +# effect: 'Allow' +# principal: +# AWS: +# - "*" +## readonlyexamplepolicy policy grants access to buckets with name starting with example. +## In addition, grants objects read permissions on buckets starting with example. +# - name: readonlyexamplepolicy +# statements: +# - resources: +# - 'arn:aws:s3:::example*/*' +# actions: +# - "s3:GetObject" +# - resources: +# - 'arn:aws:s3:::example*' +# actions: +# - "s3:GetBucketLocation" +# - "s3:ListBucket" +# - "s3:ListBucketMultipartUploads" +## conditionsexample policy creates all access to example bucket with aws:username="johndoe" and source ip range 10.0.0.0/8 and 192.168.0.0/24 only +# - name: conditionsexample +# statements: +# - resources: +# - 'arn:aws:s3:::example/*' +# actions: +# - 's3:*' +# conditions: +# - StringEquals: '"aws:username": "johndoe"' +# - IpAddress: | +# "aws:SourceIp": [ +# "10.0.0.0/8", +# "192.168.0.0/24" +# ] +# +## Additional Annotations for the Kubernetes Job makePolicyJob +makePolicyJob: + securityContext: + enabled: false + runAsUser: 1000 + runAsGroup: 1000 + resources: + requests: + memory: 128Mi + # Command to run after the main command on exit + exitCommand: "" + +## List of users to be created after minio install +## +users: + [] + ## Username, password and policy to be assigned to the user + ## Default policies are [readonly|readwrite|writeonly|consoleAdmin|diagnostics] + ## Add new policies as explained here https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management.html#access-management + ## NOTE: this will fail if LDAP is enabled in your MinIO deployment + ## make sure to disable this if you are using LDAP. + # - accessKey: q18aRFqWOAX7pEin + # secretKey: nCbZIP6q4s8KtQpL7n8CD2N88H6XABGf + # policy: readwrite + # Or you can refer to specific secret + # - accessKey: externalSecret + # existingSecret: my-secret + # existingSecretKey: password + # policy: readonly + +## Additional Annotations for the Kubernetes Job makeUserJob +makeUserJob: + securityContext: + enabled: true + runAsUser: 1000 + runAsGroup: 1000 + resources: + requests: + memory: 128Mi + # Command to run after the main command on exit + exitCommand: "" + +## List of service accounts to be created after minio install +## +# svcaccts: +## accessKey, secretKey and parent user to be assigned to the service accounts +## Add new service accounts as explained here https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management/minio-user-management.html#service-accounts +# - accessKey: console-svcacct +# secretKey: console123 +# user: console +## Or you can refer to specific secret +# - accessKey: externalSecret +# existingSecret: my-secret +# existingSecretKey: password +# user: console +## You also can pass custom policy +# - accessKey: console-svcacct +# secretKey: console123 +# user: console +# policy: +# statements: +# - resources: +# - 'arn:aws:s3:::example*/*' +# actions: +# - "s3:AbortMultipartUpload" +# - "s3:GetObject" +# - "s3:DeleteObject" +# - "s3:PutObject" +# - "s3:ListMultipartUploadParts" + +makeServiceAccountJob: + securityContext: + enabled: false + runAsUser: 1000 + runAsGroup: 1000 + resources: + requests: + memory: 128Mi + # Command to run after the main command on exit + exitCommand: "" + +## List of buckets to be created after minio install +## +buckets: + # Name of the bucket + - name: kubeagi + # Policy to be set on the + # bucket [none|download|upload|public|custom] + # if set to custom, customPolicy must be set. + policy: "custom" + # Purge if bucket exists already + purge: false + # set versioning for + # bucket [true|false] + versioning: false + # set objectlocking for + # bucket [true|false] NOTE: versioning is enabled by default if you use locking + objectlocking: false + # set custom policy + customPolicy: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + AWS: + - "*" + Action: + - s3:ListBucket + - s3:GetBucketLocation + Resource: + - arn:aws:s3:::kubeagi + - Effect: Allow + Principal: + AWS: + - "*" + Action: + - s3:GetObject + Resource: + - arn:aws:s3:::kubeagi/* + +## Additional Annotations for the Kubernetes Job makeBucketJob +makeBucketJob: + securityContext: + enabled: false + runAsUser: 1000 + runAsGroup: 1000 + resources: + requests: + memory: 128Mi + # Command to run after the main command on exit + exitCommand: "" + +## List of command to run after minio install +## NOTE: the mc command TARGET is always "myminio" +customCommands: + # - command: "admin policy set myminio consoleAdmin group='cn=ops,cn=groups,dc=example,dc=com'" + +## Additional Annotations for the Kubernetes Job customCommandJob +customCommandJob: + securityContext: + enabled: false + runAsUser: 1000 + runAsGroup: 1000 + resources: + requests: + memory: 128Mi + # Command to run after the main command on exit + exitCommand: "" + +## Merge jobs +postJob: + podAnnotations: {} + annotations: {} + securityContext: + enabled: false + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + nodeSelector: {} + tolerations: [] + affinity: {} + +## Use this field to add environment variables relevant to MinIO server. These fields will be passed on to MinIO container(s) +## when Chart is deployed +environment: + ## Please refer for comprehensive list https://min.io/docs/minio/linux/reference/minio-server/minio-server.html + ## MINIO_SUBNET_LICENSE: "License key obtained from https://subnet.min.io" + ## MINIO_BROWSER: "off" + +## The name of a secret in the same kubernetes namespace which contain secret values +## This can be useful for LDAP password, etc +## The key in the secret must be 'config.env' +## +# extraSecret: minio-extraenv + +## OpenID Identity Management +## The following section documents environment variables for enabling external identity management using an OpenID Connect (OIDC)-compatible provider. +## See https://min.io/docs/minio/linux/operations/external-iam/configure-openid-external-identity-management.html for a tutorial on using these variables. +oidc: + enabled: false + configUrl: "https://identity-provider-url/.well-known/openid-configuration" + clientId: "minio" + clientSecret: "" + claimName: "policy" + scopes: "openid,profile,email" + redirectUri: "https://console-endpoint-url/oauth_callback" + # Can leave empty + claimPrefix: "" + comment: "" + +networkPolicy: + enabled: false + allowExternal: true + +## PodDisruptionBudget settings +## ref: https://kubernetes.io/docs/concepts/workloads/pods/disruptions/ +## +podDisruptionBudget: + enabled: false + maxUnavailable: 1 + +## Specify the service account to use for the MinIO pods. If 'create' is set to 'false' +## and 'name' is left unspecified, the account 'default' will be used. +serviceAccount: + create: true + ## The name of the service account to use. If 'create' is 'true', a service account with that name + ## will be created. + name: "minio-sa" + +metrics: + serviceMonitor: + enabled: false + # scrape each node/pod individually for additional metrics + includeNode: false + public: true + additionalLabels: {} + # for node metrics + relabelConfigs: {} + # for cluster metrics + relabelConfigsCluster: + {} + # metricRelabelings: + # - regex: (server|pod) + # action: labeldrop + # namespace: monitoring + # interval: 30s + # scrapeTimeout: 10s + +## ETCD settings: https://github.com/minio/minio/blob/master/docs/sts/etcd.md +## Define endpoints to enable this section. +etcd: + endpoints: [] + pathPrefix: "" + corednsPathPrefix: "" + clientCert: "" + clientCertKey: "" diff --git a/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_datasources.yaml b/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_datasources.yaml index 1141776db..0f237d92e 100644 --- a/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_datasources.yaml +++ b/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_datasources.yaml @@ -15,7 +15,14 @@ spec: singular: datasource scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .spec.displayName + name: display-name + type: string + - jsonPath: .metadata.labels.arcadia\.kubeagi\.k8s\.com\.cn/datasource-type + name: type + type: string + name: v1alpha1 schema: openAPIV3Schema: description: Datasource is the Schema for the datasources API @@ -35,12 +42,58 @@ spec: spec: description: DatasourceSpec defines the desired state of Datasource properties: - authsecret: - description: AuthSecret defines datasource authsecret + creator: + description: Creator defines datasource creator(AUTO-FILLED by webhook) type: string - url: - description: URL defines datasource url + description: + description: Description defines datasource description type: string + displayName: + description: DisplayName defines datasource display name + type: string + endpoint: + description: Enpoint defines connection info + properties: + authSecret: + description: AuthSecret if the chart repository requires auth + authentication, set the username and password to secret, with + the field user and password respectively. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: Namespace is the namespace of resource being + referenced + type: string + required: + - kind + - name + type: object + insecure: + description: Insecure if the endpoint needs a secure connection + type: boolean + url: + description: URL chart repository address + type: string + type: object + oss: + description: OSS defines info for object storage service + properties: + bucket: + type: string + object: + type: string + type: object type: object status: description: DatasourceStatus defines the observed state of Datasource diff --git a/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_embedders.yaml b/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_embedders.yaml index 9abe4115f..281c9d367 100644 --- a/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_embedders.yaml +++ b/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_embedders.yaml @@ -15,85 +15,112 @@ spec: singular: embedder scope: Namespaced versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: Embedder is the Schema for the embedders API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Embedder is the Schema for the embeddings API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: EmbedderSpec defines the desired state of embedder. - properties: - auth: - description: Auth keeps the authentication credentials when access - embedder, keeps in k8s secret. - type: string - displayName: - type: string - type: - description: Type defines the type of embedder. - type: string - url: - description: URL keeps the URL of the embedder service(Must required) - type: string - required: - - type - type: object - status: - description: EmbedderStatus defines the observed state of embedder. - properties: - conditions: - description: Conditions of the resource. - items: - description: A Condition that may apply to a resource. + type: string + metadata: + type: object + spec: + description: EmbedderSpec defines the desired state of Embedder + properties: + displayName: + description: Name of the Embedding service + type: string + endpoint: + description: Enpoint defines connection info + properties: + authSecret: + description: AuthSecret if the chart repository requires auth + authentication, set the username and password to secret, with + the field user and password respectively. properties: - lastSuccessfulTime: - description: LastSuccessfulTime is repository Last Successful - Update Time - format: date-time + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. type: string - lastTransitionTime: - description: LastTransitionTime is the last time this condition - transitioned from one status to another. - format: date-time + kind: + description: Kind is the type of resource being referenced type: string - message: - description: A Message containing details about this condition's - last transition from one status to another, if any. + name: + description: Name is the name of resource being referenced type: string - reason: - description: A Reason for this condition's last transition from - one status to another. - type: string - status: - description: Status of this condition; is it currently True, - False, or Unknown - type: string - type: - description: Type of this condition. At most one of each condition - type may apply to a resource at any point in time. + namespace: + description: Namespace is the namespace of resource being + referenced type: string required: - - lastTransitionTime - - reason - - status - - type + - kind + - name type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} + insecure: + description: Insecure if the endpoint needs a secure connection + type: boolean + url: + description: URL chart repository address + type: string + type: object + serviceType: + description: ServiceType indicates the source type of embedding service + type: string + type: object + status: + description: EmbeddingsStatus defines the observed state of Embedder + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastSuccessfulTime: + description: LastSuccessfulTime is repository Last Successful + Update Time + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown + type: string + type: + description: Type of this condition. At most one of each condition + type may apply to a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_llms.yaml b/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_llms.yaml index 71be044a1..ad0e74be9 100644 --- a/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_llms.yaml +++ b/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_llms.yaml @@ -35,18 +35,46 @@ spec: spec: description: LLMSpec defines the desired state of LLM properties: - auth: - description: Auth keeps the authentication credentials when access - llm keeps in k8s secret - type: string displayName: type: string + endpoint: + description: Enpoint defines connection info + properties: + authSecret: + description: AuthSecret if the chart repository requires auth + authentication, set the username and password to secret, with + the field user and password respectively. + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: Namespace is the namespace of resource being + referenced + type: string + required: + - kind + - name + type: object + insecure: + description: Insecure if the endpoint needs a secure connection + type: boolean + url: + description: URL chart repository address + type: string + type: object type: description: Type defines the type of llm type: string - url: - description: URL keeps the URL of the llm service(Must required) - type: string required: - type type: object diff --git a/charts/arcadia/templates/post-oss.yaml b/charts/arcadia/templates/post-oss.yaml new file mode 100644 index 000000000..0873c30da --- /dev/null +++ b/charts/arcadia/templates/post-oss.yaml @@ -0,0 +1,19 @@ +apiVersion: arcadia.kubeagi.k8s.com.cn/v1alpha1 +kind: Datasource +metadata: + name: {{ .Release.Name }}-minio + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": post-install,post-upgrade +spec: + displayName: "内置系统数据源" + description: "Arcadia 内置系统数据源" + endpoint: + url: {{ .Release.Name }}-minio.{{ .Release.Namespace }}.svc.cluster.local:9000 + authSecret: + kind: Secret + name: {{ .Release.Name }}-minio + insecure: true + oss: + # pre-defined buckets for arcadia + bucket: {{ .Values.global.oss.bucket }} diff --git a/charts/arcadia/templates/rbac.yaml b/charts/arcadia/templates/rbac.yaml index cddaca4c9..25e76545f 100644 --- a/charts/arcadia/templates/rbac.yaml +++ b/charts/arcadia/templates/rbac.yaml @@ -29,6 +29,7 @@ rules: verbs: - get - list + - watch - apiGroups: - arcadia.kubeagi.k8s.com.cn resources: diff --git a/charts/arcadia/values.yaml b/charts/arcadia/values.yaml index edb9fc952..faad4722e 100644 --- a/charts/arcadia/values.yaml +++ b/charts/arcadia/values.yaml @@ -1,3 +1,10 @@ +global: + oss: + bucket: &default-oss-bucket "arcadia" +# @section deployment is used as default deployment configurations for arcadia +# @param image Image to be used +# @param imagePullPolcy ImagePullPolicy +# @param resources Resources to be used deployment: image: kubebb/arcadia:v0.0.1 imagePullPolcy: IfNotPresent @@ -8,3 +15,35 @@ deployment: requests: cpu: 10m memory: 64Mi + +# @section oss is used as default Object-Storage-Service for arcadia which provides the capability to +# - host user-uploaded data files as local datasource +# - host user-uploaded models +# The following params comes from kubebb/minio in repository https://kubebb.github.io/components +# @param oss.enabled Enable Object-Storage-Service in arcadia +# @param oss.buckets List of default buckets in arcadia +minio: + mode: standalone + rootUser: "admin" + rootPassword: "Passw0rd!" + persistence: + enabled: true + storageClass: "" + size: 30Gi + users: + - accessKey: q18aRFqWOAX7pEin + secretKey: nCbZIP6q4s8KtQpL7n8CD2N88H6XABGf + policy: readwrite + buckets: + # Name of the bucket + - name: *default-oss-bucket + # Policy to be set on the + # bucket [none|download|upload|public|custom] + # if set to custom, customPolicy must be set. + policy: "none" + # set versioning for + # bucket [true|false] + versioning: false + # set objectlocking for + # bucket [true|false] NOTE: versioning is enabled by default if you use locking + objectlocking: false diff --git a/config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasources.yaml b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasources.yaml index cca1aa855..0f237d92e 100644 --- a/config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasources.yaml +++ b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasources.yaml @@ -15,7 +15,14 @@ spec: singular: datasource scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .spec.displayName + name: display-name + type: string + - jsonPath: .metadata.labels.arcadia\.kubeagi\.k8s\.com\.cn/datasource-type + name: type + type: string + name: v1alpha1 schema: openAPIV3Schema: description: Datasource is the Schema for the datasources API @@ -87,8 +94,6 @@ spec: object: type: string type: object - required: - - endpoint type: object status: description: DatasourceStatus defines the observed state of Datasource diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index f98ff14bf..c7527a02c 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -12,6 +12,7 @@ rules: verbs: - get - list + - watch - apiGroups: - arcadia.kubeagi.k8s.com.cn resources: diff --git a/config/samples/arcadia_v1alpha1_datasource.yaml b/config/samples/arcadia_v1alpha1_datasource.yaml deleted file mode 100644 index 46635447f..000000000 --- a/config/samples/arcadia_v1alpha1_datasource.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: arcadia.kubeagi.k8s.com.cn/v1alpha1 -kind: Datasource -metadata: - name: arcadia-oss-minio - namespace: arcadia -spec: - displayName: "对象存储数据源" - description: "这是一个Minio的对象存储数据源" - endpoint: - url: arcadia-oss-minio.arcadia.svc.cluster.local:9000 - authSecret: - kind: secret - name: arcadia-oss-minio - insecure: true - oss: - bucket: "kubebb" - object: "" diff --git a/config/samples/arcadia_v1alpha1_local_datasource.yaml b/config/samples/arcadia_v1alpha1_local_datasource.yaml new file mode 100644 index 000000000..0e7005ea2 --- /dev/null +++ b/config/samples/arcadia_v1alpha1_local_datasource.yaml @@ -0,0 +1,8 @@ +apiVersion: arcadia.kubeagi.k8s.com.cn/v1alpha1 +kind: Datasource +metadata: + name: arcadia-local + namespace: arcadia +spec: + displayName: "本地存储数据源" + description: "这是一个使用系统数据源作为存储的本地数据源" \ No newline at end of file diff --git a/controllers/datasource_controller.go b/controllers/datasource_controller.go index be69f0e81..9275ac5b9 100644 --- a/controllers/datasource_controller.go +++ b/controllers/datasource_controller.go @@ -44,7 +44,7 @@ type DatasourceReconciler struct { //+kubebuilder:rbac:groups=arcadia.kubeagi.k8s.com.cn,resources=datasources,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=arcadia.kubeagi.k8s.com.cn,resources=datasources/status,verbs=get;update;patch //+kubebuilder:rbac:groups=arcadia.kubeagi.k8s.com.cn,resources=datasources/finalizers,verbs=update -//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -80,10 +80,13 @@ func (r *DatasourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // initialize labels - err := r.Initialize(ctx, logger, instance) + requeue, err := r.Initialize(ctx, logger, instance) if err != nil { return reconcile.Result{}, fmt.Errorf("failed to initiali datasource: %w", err) } + if requeue { + return reconcile.Result{Requeue: true}, nil + } // check datasource if err := r.Checkdatasource(ctx, logger, instance); err != nil { @@ -101,7 +104,7 @@ func (r *DatasourceReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *DatasourceReconciler) Initialize(ctx context.Context, logger logr.Logger, instance *arcadiav1alpha1.Datasource) error { +func (r *DatasourceReconciler) Initialize(ctx context.Context, logger logr.Logger, instance *arcadiav1alpha1.Datasource) (bool, error) { instanceDeepCopy := instance.DeepCopy() l := len(instanceDeepCopy.Finalizers) @@ -124,10 +127,10 @@ func (r *DatasourceReconciler) Initialize(ctx context.Context, logger logr.Logge } if update { - return r.Client.Update(ctx, instanceDeepCopy) + return true, r.Client.Update(ctx, instanceDeepCopy) } - return nil + return false, nil } // Checkdatasource to update status @@ -137,17 +140,24 @@ func (r *DatasourceReconciler) Checkdatasource(ctx context.Context, logger logr. // create datasource var ds datasource.Datasource + var info any switch instance.Spec.Type() { + case arcadiav1alpha1.DatasourceTypeLocal: + // FIXME: implement local datasource check when system datasource defined by https://github.com/kubeagi/arcadia/issues/156 + // 1. read system datasource endpoint + // 2. check against pre-denfined rules for local datasource rules + return r.UpdateStatus(ctx, instance, nil) case arcadiav1alpha1.DatasourceTypeOSS: endpoiont := instance.Spec.Enpoint.DeepCopy() // set auth secret's namespace to the datasource's namespace if endpoiont.AuthSecret != nil { endpoiont.AuthSecret.WithNameSpace(instance.Namespace) } - ds, err = datasource.NewOSS(ctx, r.Client, endpoiont, instance.Spec.OSS.DeepCopy()) + ds, err = datasource.NewOSS(ctx, r.Client, endpoiont) if err != nil { return r.UpdateStatus(ctx, instance, err) } + info = instance.Spec.OSS.DeepCopy() default: ds, err = datasource.NewUnknown(ctx, r.Client) if err != nil { @@ -156,7 +166,7 @@ func (r *DatasourceReconciler) Checkdatasource(ctx context.Context, logger logr. } // check datasource - if err := ds.Check(ctx, nil); err != nil { + if err := ds.Check(ctx, info); err != nil { return r.UpdateStatus(ctx, instance, err) } diff --git a/graphql-server/go-server/pkg/datasource/datasource.go b/graphql-server/go-server/pkg/datasource/datasource.go index d7d850cc1..0b247a82e 100644 --- a/graphql-server/go-server/pkg/datasource/datasource.go +++ b/graphql-server/go-server/pkg/datasource/datasource.go @@ -70,10 +70,10 @@ func CreateDatasource(ctx context.Context, name, namespace, url, authsecret stri APIVersion: v1alpha1.GroupVersion.String(), }, Spec: v1alpha1.DatasourceSpec{ - Enpoint: v1alpha1.Endpoint{ + Enpoint: &v1alpha1.Endpoint{ URL: url, AuthSecret: &v1alpha1.TypedObjectReference{ - Kind: "secret", + Kind: "Secret", Name: authsecret, }, Insecure: insecure, diff --git a/pkg/datasource/datasource.go b/pkg/datasource/datasource.go index b62bc6c1d..f3ead2581 100644 --- a/pkg/datasource/datasource.go +++ b/pkg/datasource/datasource.go @@ -34,9 +34,11 @@ var ( ) type Datasource interface { - Check(ctx context.Context, options any) error + Check(ctx context.Context, info any) error } +var _ Datasource = (*Unknown)(nil) + type Unknown struct { } @@ -44,17 +46,38 @@ func NewUnknown(ctx context.Context, c client.Client) (*Unknown, error) { return &Unknown{}, nil } -func (u *Unknown) Check(ctx context.Context, options any) error { +func (u *Unknown) Check(ctx context.Context, info any) error { return ErrUnknowDatasourceType } +var _ Datasource = (*Local)(nil) + +// Local is a special datasource which use the system datasource as oss to store user-uploaded local files +// - `oss` in `Local` represents the system datasource oss client along with the `Local`'s oss info +type Local struct { + oss *OSS +} + +func NewLocal(ctx context.Context, c client.Client, endpoint *v1alpha1.Endpoint) (*Local, error) { + oss, err := NewOSS(ctx, c, endpoint) + if err != nil { + return nil, err + } + return &Local{oss: oss}, nil +} + +// Check `Local` with `OSS` +func (local *Local) Check(ctx context.Context, options any) error { + return local.oss.Check(ctx, options) +} + var _ Datasource = (*OSS)(nil) type OSS struct { *minio.Client } -func NewOSS(ctx context.Context, c client.Client, endpoint *v1alpha1.Endpoint, ossInfo *v1alpha1.OSS) (*OSS, error) { +func NewOSS(ctx context.Context, c client.Client, endpoint *v1alpha1.Endpoint) (*OSS, error) { var accessKeyID, secretAccessKey string if endpoint.AuthSecret != nil { secret := corev1.Secret{} @@ -84,6 +107,16 @@ func NewOSS(ctx context.Context, c client.Client, endpoint *v1alpha1.Endpoint, o } // TODO: implement Check with specific `options` -func (oss *OSS) Check(ctx context.Context, options any) error { +func (oss *OSS) Check(ctx context.Context, info any) error { + if info == nil { + return nil + } + _, ok := info.(*v1alpha1.OSS) + if !ok { + return errors.New("invalid check info for OSS") + } + + // TODO:check bucket/object exists + return nil } diff --git a/config/samples/example-test.sh b/tests/example-test.sh similarity index 86% rename from config/samples/example-test.sh rename to tests/example-test.sh index 11b77df91..a23fb0a27 100755 --- a/config/samples/example-test.sh +++ b/tests/example-test.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Copyright contributors to the Kubebb Core project +# Copyright contributors to the KubeAGI project # # SPDX-License-Identifier: Apache-2.0 # @@ -32,7 +32,7 @@ KindConfigPath=${TempFilePath}/kind-config.yaml InstallDirPath=${TempFilePath}/building-base DefaultPassWord=${DefaultPassWord:-'passw0rd'} LOG_DIR=${LOG_DIR:-"/tmp/kubeagi-example-test/logs"} -RootPath=$(dirname -- "$(readlink -f -- "$0")")/../.. +RootPath=$(dirname -- "$(readlink -f -- "$0")")/.. Timeout="${TimeoutSeconds}s" mkdir ${TempFilePath} || true @@ -169,26 +169,28 @@ function waitCRDStatusReady() { sleep 5 done } + info "1. create kind cluster" make kind -info "2. install minio as arcadia oss" -info "2.1 add repo kubebb" -helm repo add kubebb https://kubebb.github.io/components/ -helm repo update -info "2.2 install minio" -kubectl create namespace arcadia -helm install arcadia-oss -n arcadia kubebb/minio -waitPodReady "arcadia" "release=arcadia-oss" - -info "3. install arcadia" +info "2. load arcadia image to kind" docker tag controller:latest controller:example-e2e kind load docker-image controller:example-e2e --name=$KindName -make deploy IMG="controller:example-e2e" -kubectl wait deploy -n arcadia arcadia-controller-manager --for condition=Available=True --timeout=$Timeout -info "4. CRD datasource check" -kubectl apply -f config/samples/arcadia_v1alpha1_datasource.yaml -waitCRDStatusReady "Datasource" "arcadia" "arcadia-oss-minio" +info "3. install arcadia" +kubectl create namespace arcadia +helm install -narcadia arcadia charts/arcadia --set deployment.image=controller:example-e2e + +info "4. check system datasource arcadia-minio(system datasource)" +waitCRDStatusReady "Datasource" "arcadia" "arcadia-minio" + +info "5. create and verify local datasource" +kubectl apply -f config/samples/arcadia_v1alpha1_local_datasource.yaml +waitCRDStatusReady "Datasource" "arcadia" "arcadia-local" +datasourceType=$(kubectl get datasource -n arcadia arcadia-local -o=jsonpath='{.metadata.labels.arcadia\.kubeagi\.k8s\.com\.cn/datasource-type}') +if [[ $datasourceType != "local" ]]; then + error "Datasource should local but got $datasourceType" + exit 1 +fi info "all finished! ✅" From a39191a20d88c128330a8791d5a05894957ec5eb Mon Sep 17 00:00:00 2001 From: zqq454224016 <454224016@qq.com> Date: Mon, 30 Oct 2023 18:05:30 +0800 Subject: [PATCH 02/12] feat: check datasource bucket and object Signed-off-by: zqq454224016 <454224016@qq.com> --- controllers/datasource_controller.go | 1 - pkg/datasource/datasource.go | 21 +++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/controllers/datasource_controller.go b/controllers/datasource_controller.go index 9275ac5b9..a95a8bad8 100644 --- a/controllers/datasource_controller.go +++ b/controllers/datasource_controller.go @@ -90,7 +90,6 @@ func (r *DatasourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) // check datasource if err := r.Checkdatasource(ctx, logger, instance); err != nil { - logger.Error(err, "Failed to check datasource") // Update conditioned status return reconcile.Result{}, err } diff --git a/pkg/datasource/datasource.go b/pkg/datasource/datasource.go index f3ead2581..9ad34bb72 100644 --- a/pkg/datasource/datasource.go +++ b/pkg/datasource/datasource.go @@ -31,6 +31,8 @@ import ( var ( ErrUnknowDatasourceType = errors.New("unknow datasource type") + ErrAccessPermission = errors.New("dont have permission") + ErrAccessObject = errors.New("access error object") ) type Datasource interface { @@ -106,7 +108,6 @@ func NewOSS(ctx context.Context, c client.Client, endpoint *v1alpha1.Endpoint) ( return &OSS{Client: mc}, nil } -// TODO: implement Check with specific `options` func (oss *OSS) Check(ctx context.Context, info any) error { if info == nil { return nil @@ -116,7 +117,23 @@ func (oss *OSS) Check(ctx context.Context, info any) error { return errors.New("invalid check info for OSS") } - // TODO:check bucket/object exists + if info.(*v1alpha1.OSS).Bucket != "" { + _, err := oss.Client.BucketExists(ctx, info.(*v1alpha1.OSS).Bucket) + if err != nil { + return err + } + if info.(*v1alpha1.OSS).Object != "" { + _, err := oss.Client.StatObject(ctx, info.(*v1alpha1.OSS).Bucket, info.(*v1alpha1.OSS).Object, minio.StatObjectOptions{}) + if err != nil { + switch minio.ToErrorResponse(err).Code { + case "AccessDenied": + return ErrAccessPermission + default: + return ErrAccessObject + } + } + } + } return nil } From 54af00295504acff994e54a71a84bbfbd2164456 Mon Sep 17 00:00:00 2001 From: bjwswang Date: Tue, 31 Oct 2023 05:57:10 +0000 Subject: [PATCH 03/12] fix:bugs when check datasource;add hook weight to makesure oss created after minio Signed-off-by: bjwswang --- charts/arcadia/Chart.yaml | 2 +- .../charts/minio/templates/post-job.yaml | 1 + charts/arcadia/templates/post-oss.yaml | 1 + controllers/datasource_controller.go | 29 ++++++++++++++----- pkg/datasource/datasource.go | 23 ++++++--------- 5 files changed, 33 insertions(+), 23 deletions(-) diff --git a/charts/arcadia/Chart.yaml b/charts/arcadia/Chart.yaml index 3cf4d7da6..a8a902b00 100644 --- a/charts/arcadia/Chart.yaml +++ b/charts/arcadia/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: arcadia description: A Helm chart(KubeBB Component) for KubeAGI Arcadia type: application -version: 0.1.10 +version: 0.1.11 appVersion: "0.0.1" keywords: diff --git a/charts/arcadia/charts/minio/templates/post-job.yaml b/charts/arcadia/charts/minio/templates/post-job.yaml index 47839b81f..ebdc85276 100644 --- a/charts/arcadia/charts/minio/templates/post-job.yaml +++ b/charts/arcadia/charts/minio/templates/post-job.yaml @@ -12,6 +12,7 @@ metadata: annotations: "helm.sh/hook": post-install,post-upgrade "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation + "helm.sh/hook-weight": "-1" {{- with .Values.postJob.annotations }} {{- toYaml . | nindent 4 }} {{- end }} diff --git a/charts/arcadia/templates/post-oss.yaml b/charts/arcadia/templates/post-oss.yaml index 0873c30da..bbdea332e 100644 --- a/charts/arcadia/templates/post-oss.yaml +++ b/charts/arcadia/templates/post-oss.yaml @@ -5,6 +5,7 @@ metadata: namespace: {{ .Release.Namespace }} annotations: "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "1" spec: displayName: "内置系统数据源" description: "Arcadia 内置系统数据源" diff --git a/controllers/datasource_controller.go b/controllers/datasource_controller.go index a95a8bad8..50b1c61f0 100644 --- a/controllers/datasource_controller.go +++ b/controllers/datasource_controller.go @@ -19,6 +19,7 @@ package controllers import ( "context" "fmt" + "reflect" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" @@ -26,8 +27,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" arcadiav1alpha1 "github.com/kubeagi/arcadia/api/v1alpha1" @@ -99,7 +103,13 @@ func (r *DatasourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) // SetupWithManager sets up the controller with the Manager. func (r *DatasourceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&arcadiav1alpha1.Datasource{}). + For(&arcadiav1alpha1.Datasource{}, builder.WithPredicates(predicate.Funcs{ + UpdateFunc: func(ue event.UpdateEvent) bool { + oldDatsource := ue.ObjectOld.(*arcadiav1alpha1.Datasource) + newDatasource := ue.ObjectNew.(*arcadiav1alpha1.Datasource) + return !reflect.DeepEqual(oldDatsource.Spec, newDatasource.Spec) + }, + })). Complete(r) } @@ -173,27 +183,30 @@ func (r *DatasourceReconciler) Checkdatasource(ctx context.Context, logger logr. return r.UpdateStatus(ctx, instance, nil) } +// UpdateStatus uppon error func (r *DatasourceReconciler) UpdateStatus(ctx context.Context, instance *arcadiav1alpha1.Datasource, err error) error { instanceCopy := instance.DeepCopy() + var newCondition arcadiav1alpha1.Condition if err != nil { - // Set status to unavailable - instanceCopy.Status.SetConditions(arcadiav1alpha1.Condition{ + // set condition to False + newCondition = arcadiav1alpha1.Condition{ Type: arcadiav1alpha1.TypeReady, Status: corev1.ConditionFalse, Reason: arcadiav1alpha1.ReasonUnavailable, Message: err.Error(), LastTransitionTime: metav1.Now(), - }) + } } else { - // Set status to available - instanceCopy.Status.SetConditions(arcadiav1alpha1.Condition{ + // set condition to True + newCondition = arcadiav1alpha1.Condition{ Type: arcadiav1alpha1.TypeReady, Status: corev1.ConditionTrue, Reason: arcadiav1alpha1.ReasonAvailable, - Message: "health check success", + Message: "Check Success", LastTransitionTime: metav1.Now(), LastSuccessfulTime: metav1.Now(), - }) + } } + instanceCopy.Status.SetConditions(newCondition) return r.Client.Status().Update(ctx, instanceCopy) } diff --git a/pkg/datasource/datasource.go b/pkg/datasource/datasource.go index 9ad34bb72..d1097b857 100644 --- a/pkg/datasource/datasource.go +++ b/pkg/datasource/datasource.go @@ -31,8 +31,6 @@ import ( var ( ErrUnknowDatasourceType = errors.New("unknow datasource type") - ErrAccessPermission = errors.New("dont have permission") - ErrAccessObject = errors.New("access error object") ) type Datasource interface { @@ -75,6 +73,7 @@ func (local *Local) Check(ctx context.Context, options any) error { var _ Datasource = (*OSS)(nil) +// OSS is a wrapper to object storage service type OSS struct { *minio.Client } @@ -90,7 +89,7 @@ func NewOSS(ctx context.Context, c client.Client, endpoint *v1alpha1.Endpoint) ( return nil, err } accessKeyID = string(secret.Data["rootUser"]) - secretAccessKey = string(secret.Data["rootUser"]) + secretAccessKey = string(secret.Data["rootPassword"]) // TODO: implement https(secure check) // if !endpoint.Insecure { @@ -108,30 +107,26 @@ func NewOSS(ctx context.Context, c client.Client, endpoint *v1alpha1.Endpoint) ( return &OSS{Client: mc}, nil } +// Check oss agains info() func (oss *OSS) Check(ctx context.Context, info any) error { if info == nil { return nil } - _, ok := info.(*v1alpha1.OSS) + ossInfo, ok := info.(*v1alpha1.OSS) if !ok { return errors.New("invalid check info for OSS") } - if info.(*v1alpha1.OSS).Bucket != "" { - _, err := oss.Client.BucketExists(ctx, info.(*v1alpha1.OSS).Bucket) + if ossInfo.Bucket != "" { + _, err := oss.Client.BucketExists(ctx, ossInfo.Bucket) if err != nil { return err } - if info.(*v1alpha1.OSS).Object != "" { - _, err := oss.Client.StatObject(ctx, info.(*v1alpha1.OSS).Bucket, info.(*v1alpha1.OSS).Object, minio.StatObjectOptions{}) + if ossInfo.Object != "" { + _, err := oss.Client.StatObject(ctx, ossInfo.Bucket, ossInfo.Object, minio.StatObjectOptions{}) if err != nil { - switch minio.ToErrorResponse(err).Code { - case "AccessDenied": - return ErrAccessPermission - default: - return ErrAccessObject - } + return err } } } From 5f2273a2144836490b9725a08d9e333d3136e988 Mon Sep 17 00:00:00 2001 From: 0xff-dev Date: Mon, 30 Oct 2023 13:29:58 +0800 Subject: [PATCH 04/12] feat: support oidc auth --- go.mod | 6 +- go.sum | 13 ++- graphql-server/go-server/README.md | 34 +++++++ .../go-server/graph/datasource.resolvers.go | 17 +++- graphql-server/go-server/graph/resolver.go | 3 +- graphql-server/go-server/main.go | 21 +++-- graphql-server/go-server/pkg/auth/auth.go | 62 +++++++++++++ graphql-server/go-server/pkg/client/client.go | 11 +++ .../go-server/pkg/datasource/datasource.go | 8 +- graphql-server/go-server/pkg/oidc/oidc.go | 93 +++++++++++++++++++ 10 files changed, 249 insertions(+), 19 deletions(-) create mode 100644 graphql-server/go-server/pkg/auth/auth.go create mode 100644 graphql-server/go-server/pkg/oidc/oidc.go diff --git a/go.mod b/go.mod index 7a6064fff..e719b676d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/99designs/gqlgen v0.17.40 github.com/amikos-tech/chroma-go v0.0.0-20230901221218-d0087270239e + github.com/coreos/go-oidc/v3 v3.7.0 github.com/go-logr/logr v1.2.0 github.com/gofiber/fiber/v2 v2.49.1 github.com/golang-jwt/jwt v3.2.2+incompatible @@ -26,6 +27,7 @@ require ( require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect @@ -112,13 +114,13 @@ require ( golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.10.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 49bf962a2..2e7c4a427 100644 --- a/go.sum +++ b/go.sum @@ -128,6 +128,8 @@ github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u9 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc/v3 v3.7.0 h1:FTdj0uexT4diYIPlF4yoFVI5MRO1r5+SEcIpEw9vC0o= +github.com/coreos/go-oidc/v3 v3.7.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -179,6 +181,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -633,6 +637,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -748,8 +753,8 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= -golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -851,6 +856,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= @@ -953,8 +959,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/graphql-server/go-server/README.md b/graphql-server/go-server/README.md index 4a77b57a9..629e4e3c2 100644 --- a/graphql-server/go-server/README.md +++ b/graphql-server/go-server/README.md @@ -48,3 +48,37 @@ func (r *queryResolver) FindX(ctx context.Context, input string) (string, error) The file `model/model_gen.go` has a new structure x that we defined. All we have to do is just implement the `FindX` function. And the content of the function is up to you to play with. + + +## How to run + +in the root dir of the project + +```shell +# 1. build +make build-graphql-server + +# 2. run parameters +$ ./go-bff-server -h +Usage of ./main: + -client-id string + oidc client id + -client-secret string + oidc client secret + -enable-playgroud + whether to open the graphql playground (default true) + -host string + bind to the host, default is 0.0.0.0 + -issuer-url string + oidc issuer url + -kubeconfig string + Paths to a kubeconfig. Only required if out-of-cluster. + -master-url string + k8s master url + -port int + service listening port (default 8081) + +# 3. run +./go-bff-server --client-id=bff-client --client-secret=some-secret --master-url=https://k8s-adress --issuer-url=https://oidc-server +``` + diff --git a/graphql-server/go-server/graph/datasource.resolvers.go b/graphql-server/go-server/graph/datasource.resolvers.go index 5bc4ed788..56768fea9 100644 --- a/graphql-server/go-server/graph/datasource.resolvers.go +++ b/graphql-server/go-server/graph/datasource.resolvers.go @@ -8,11 +8,18 @@ import ( "context" "github.com/kubeagi/arcadia/graphql-server/go-server/graph/model" + "github.com/kubeagi/arcadia/graphql-server/go-server/pkg/auth" + "github.com/kubeagi/arcadia/graphql-server/go-server/pkg/client" "github.com/kubeagi/arcadia/graphql-server/go-server/pkg/datasource" ) // CreateDatasource is the resolver for the createDatasource field. func (r *mutationResolver) CreateDatasource(ctx context.Context, input model.CreateDatasource) (*model.Datasource, error) { + token := auth.ForOIDCToken(ctx) + c, err := client.GetClientByIDToken(token) + if err != nil { + return nil, err + } url, authSecret := "", "" var insecure bool if input.URL != nil { @@ -24,11 +31,17 @@ func (r *mutationResolver) CreateDatasource(ctx context.Context, input model.Cre if input.Insecure != nil { insecure = *input.Insecure } - return datasource.CreateDatasource(ctx, input.Name, input.Namespace, url, authSecret, insecure) + return datasource.CreateDatasource(ctx, c, input.Name, input.Namespace, url, authSecret, insecure) } // Ds is the resolver for the ds field. func (r *queryResolver) Ds(ctx context.Context, input model.QueryDatasource) ([]*model.Datasource, error) { + token := auth.ForOIDCToken(ctx) + + c, err := client.GetClientByIDToken(token) + if err != nil { + return nil, err + } name := "" labelSelector, fieldSelector := "", "" if input.Name != nil { @@ -40,7 +53,7 @@ func (r *queryResolver) Ds(ctx context.Context, input model.QueryDatasource) ([] if input.LabelSelector != nil { labelSelector = *input.LabelSelector } - return datasource.DatasourceList(ctx, name, input.Namespace, labelSelector, fieldSelector) + return datasource.DatasourceList(ctx, c, name, input.Namespace, labelSelector, fieldSelector) } // Mutation returns MutationResolver implementation. diff --git a/graphql-server/go-server/graph/resolver.go b/graphql-server/go-server/graph/resolver.go index a25c09c61..9f9867920 100644 --- a/graphql-server/go-server/graph/resolver.go +++ b/graphql-server/go-server/graph/resolver.go @@ -4,4 +4,5 @@ package graph // // It serves as dependency injection for your app, add any dependencies you require here. -type Resolver struct{} +type Resolver struct{ +} diff --git a/graphql-server/go-server/main.go b/graphql-server/go-server/main.go index 1cdae09e4..5cec57ee2 100644 --- a/graphql-server/go-server/main.go +++ b/graphql-server/go-server/main.go @@ -27,21 +27,29 @@ import ( "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "k8s.io/klog/v2" - ctrl "sigs.k8s.io/controller-runtime" "github.com/kubeagi/arcadia/graphql-server/go-server/graph" - "github.com/kubeagi/arcadia/graphql-server/go-server/pkg/client" + "github.com/kubeagi/arcadia/graphql-server/go-server/pkg/auth" + "github.com/kubeagi/arcadia/graphql-server/go-server/pkg/oidc" + + _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" ) var ( host = flag.String("host", "", "bind to the host, default is 0.0.0.0") port = flag.Int("port", 8081, "service listening port") enablePlayground = flag.Bool("enable-playgroud", true, "whether to open the graphql playground") + + issuerURL = flag.String("issuer-url", "", "oidc issuer url") + masterURL = flag.String("master-url", "", "k8s master url") + clientSecret = flag.String("client-secret", "", "oidc client secret") + clientID = flag.String("client-id", "", "oidc client id") ) func main() { - cfg := ctrl.GetConfigOrDie() - client.InitClient(cfg) + flag.Parse() + + oidc.InitOIDCArgs(*issuerURL, *masterURL, *clientSecret, *clientID) srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}})) srv.AroundFields(func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) { @@ -53,9 +61,10 @@ func main() { }) if *enablePlayground { - http.Handle("/", playground.Handler("Arcadia", "/query")) + http.Handle("/", auth.AuthInterceptor(oidc.Verifier, playground.Handler("Arcadia-BFF-Server", "/query"))) } - http.Handle("/query", srv) + http.Handle("/query", auth.AuthInterceptor(oidc.Verifier, srv.ServeHTTP)) + klog.Infof("listening server on port: %d", *port) log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *host, *port), nil)) } diff --git a/graphql-server/go-server/pkg/auth/auth.go b/graphql-server/go-server/pkg/auth/auth.go new file mode 100644 index 000000000..750336a80 --- /dev/null +++ b/graphql-server/go-server/pkg/auth/auth.go @@ -0,0 +1,62 @@ +/* +Copyright 2023 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +import ( + "context" + "net/http" + "strings" + + "github.com/coreos/go-oidc/v3/oidc" +) + +type idtokenKey struct{} + +func isBearerToken(token string) (bool, string) { + if len(token) < 6 { + return false, "" + } + head := strings.ToLower(token[:6]) + payload := strings.TrimSpace(token[6:]) + return head == "bearer" && len(payload) > 0, payload +} + +func AuthInterceptor(oidcVerifier *oidc.IDTokenVerifier, hf http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + idtoken := r.Header.Get("Authorization") + ok, rawToken := isBearerToken(idtoken) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte("Unauthorized. Please provide an oidc token")) + return + } + _, err := oidcVerifier.Verify(context.TODO(), rawToken) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(err.Error())) + return + } + ctx := context.WithValue(r.Context(), idtokenKey{}, rawToken) + r = r.WithContext(ctx) + hf(w, r) + } +} + +func ForOIDCToken(ctx context.Context) string { + v, _ := ctx.Value(idtokenKey{}).(string) + return v +} diff --git a/graphql-server/go-server/pkg/client/client.go b/graphql-server/go-server/pkg/client/client.go index 5a18aceae..bae5f81c1 100644 --- a/graphql-server/go-server/pkg/client/client.go +++ b/graphql-server/go-server/pkg/client/client.go @@ -21,6 +21,9 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + "github.com/kubeagi/arcadia/graphql-server/go-server/pkg/oidc" ) var ( @@ -37,3 +40,11 @@ func InitClient(cfg *rest.Config) { func GetClient() dynamic.Interface { return c } + +func GetClientByIDToken(idtoken string) (dynamic.Interface, error) { + cfg, err := clientcmd.BuildConfigFromKubeconfigGetter("", oidc.OIDCKubeGetter(idtoken)) + if err != nil { + return nil, err + } + return dynamic.NewForConfig(cfg) +} diff --git a/graphql-server/go-server/pkg/datasource/datasource.go b/graphql-server/go-server/pkg/datasource/datasource.go index 0b247a82e..7a36150e3 100644 --- a/graphql-server/go-server/pkg/datasource/datasource.go +++ b/graphql-server/go-server/pkg/datasource/datasource.go @@ -23,10 +23,10 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" "github.com/kubeagi/arcadia/api/v1alpha1" "github.com/kubeagi/arcadia/graphql-server/go-server/graph/model" - "github.com/kubeagi/arcadia/graphql-server/go-server/pkg/client" ) func datasource2model(obj *unstructured.Unstructured) *model.Datasource { @@ -58,8 +58,7 @@ func datasource2model(obj *unstructured.Unstructured) *model.Datasource { return &md } -func CreateDatasource(ctx context.Context, name, namespace, url, authsecret string, insecure bool) (*model.Datasource, error) { - c := client.GetClient() +func CreateDatasource(ctx context.Context, c dynamic.Interface, name, namespace, url, authsecret string, insecure bool) (*model.Datasource, error) { datasource := v1alpha1.Datasource{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -94,9 +93,8 @@ func CreateDatasource(ctx context.Context, name, namespace, url, authsecret stri return ds, nil } -func DatasourceList(ctx context.Context, name, namespace, labelSelector, fieldSelector string) ([]*model.Datasource, error) { +func DatasourceList(ctx context.Context, c dynamic.Interface, name, namespace, labelSelector, fieldSelector string) ([]*model.Datasource, error) { dsSchema := schema.GroupVersionResource{Group: v1alpha1.GroupVersion.Group, Version: v1alpha1.GroupVersion.Version, Resource: "datasources"} - c := client.GetClient() if name != "" { u, err := c.Resource(dsSchema).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { diff --git a/graphql-server/go-server/pkg/oidc/oidc.go b/graphql-server/go-server/pkg/oidc/oidc.go new file mode 100644 index 000000000..38f3f576a --- /dev/null +++ b/graphql-server/go-server/pkg/oidc/oidc.go @@ -0,0 +1,93 @@ +/* +Copyright 2023 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oidc + +import ( + "context" + "crypto/tls" + "net/http" + "sync" + + "github.com/coreos/go-oidc/v3/oidc" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/klog/v2" +) + +var ( + once sync.Once + Verifier *oidc.IDTokenVerifier + + issuerURL1, masterURL1, clientSecret1, clientID1 string +) + +func InitOIDCArgs(issuerURL, masterURL, clientSecret, clientID string) { + once.Do(func() { + issuerURL1 = issuerURL + masterURL1 = masterURL + clientSecret1 = clientSecret + clientID1 = clientID + + ctx := oidc.ClientContext(context.TODO(), &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + }) + provider, err := oidc.NewProvider(ctx, issuerURL) + if err != nil { + panic(err) + } + Verifier = provider.Verifier(&oidc.Config{ClientID: clientID}) + klog.V(5).Infof("oidc token validation was successful. issuerurl: %s, masterurl: %s, clientid: %s, clientsecret: %s", issuerURL1, masterURL1, clientID1, clientSecret1) + }) +} + +func OIDCKubeGetter(idtoken string) func() (*clientcmdapi.Config, error) { + return func() (*clientcmdapi.Config, error) { + return &clientcmdapi.Config{ + Kind: "Config", + APIVersion: "v1", + Clusters: map[string]*clientcmdapi.Cluster{ + "kube-oidc-proxy": { + Server: masterURL1, + InsecureSkipTLSVerify: true, + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + "oidc@kube-oidc-proxy": { + Cluster: "kube-oidc-proxy", + AuthInfo: "oidc", + }, + }, + CurrentContext: "oidc@kube-oidc-proxy", + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "oidc": { + AuthProvider: &clientcmdapi.AuthProviderConfig{ + Name: "oidc", + Config: map[string]string{ + "client-id": clientID1, + "client-secret": clientID1, + "id-token": idtoken, + "idp-issuer-url": issuerURL1, + }, + }, + }, + }, + }, nil + } +} From 8de4d5a332f9eed9d9071a30cf6034b9af553338 Mon Sep 17 00:00:00 2001 From: bjwswang Date: Tue, 31 Oct 2023 07:36:13 +0000 Subject: [PATCH 05/12] feat: a kubeagi yaml file to install arcadia with kubebb Signed-off-by: bjwswang --- .kubeagi_repo.yaml | 10 ---------- README.md | 7 +++++++ kubeagi.yaml | 30 ++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 10 deletions(-) delete mode 100644 .kubeagi_repo.yaml create mode 100644 kubeagi.yaml diff --git a/.kubeagi_repo.yaml b/.kubeagi_repo.yaml deleted file mode 100644 index 55d91dc77..000000000 --- a/.kubeagi_repo.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: core.kubebb.k8s.com.cn/v1alpha1 -kind: Repository -metadata: - name: kubeagi - namespace: kubebb-system -spec: - url: https://kubeagi.github.io/arcadia - pullStategy: - intervalSeconds: 120 - retry: 5 diff --git a/README.md b/README.md index e5366a9c1..2900e6a7a 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,13 @@ Our design and development in Arcadia design follows operator pattern which exte helm install --namespace arcadia --create-namespace arcadia arcadia/arcadia ``` +More conveniently,you can use [kubebb](https://github.com/kubebb) to install and upgrade arcadia automatically: +> Pre-requsities +> - [kubebb](https://kubebb.github.io/website/docs/quick-start/core_quickstart) +```shell +kubectl apply -f ./kubeagi.yaml +``` + ## CLI We provide a Command Line Tool `arctl` to interact with `arcadia`. See [here](./arctl/README.md) for more details. diff --git a/kubeagi.yaml b/kubeagi.yaml new file mode 100644 index 000000000..20e4a9c3e --- /dev/null +++ b/kubeagi.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: arcadia +--- +apiVersion: core.kubebb.k8s.com.cn/v1alpha1 +kind: Repository +metadata: + name: kubeagi + namespace: arcadia +spec: + url: https://kubeagi.github.io/arcadia + pullStategy: + intervalSeconds: 120 + retry: 5 +--- +apiVersion: core.kubebb.k8s.com.cn/v1alpha1 +kind: Subscription +metadata: + name: arcadia + namespace: arcadia +spec: + name: arcadia # release name in helm + componentPlanInstallMethod: auto + component: + name: kubeagi.arcadia + namespace: arcadia + override: + set: + - deployment.imagePullPolcy=Always \ No newline at end of file From ca63b19eeb2aa432a48bd828787d979a124fe38c Mon Sep 17 00:00:00 2001 From: bjwswang Date: Tue, 31 Oct 2023 15:18:58 +0000 Subject: [PATCH 06/12] feat: define CRD Dataset and VersionedDataset Signed-off-by: bjwswang --- PROJECT | 22 +- api/v1alpha1/condition.go | 1 + api/v1alpha1/dataset.go | 33 +++ api/v1alpha1/dataset_types.go | 72 ++++++ api/v1alpha1/datasource_types.go | 2 +- api/v1alpha1/versioneddataset.go | 21 ++ api/v1alpha1/versioneddataset_types.go | 86 +++++++ api/v1alpha1/zz_generated.deepcopy.go | 236 ++++++++++++++++++ assets/dataset_storage.drawio | 1 + assets/dataset_storage.drawio.png | Bin 0 -> 61114 bytes assets/system_datasource.drawio | 159 +++++++++++- assets/system_datasource.drawio.png | Bin 63013 -> 80489 bytes .../arcadia.kubeagi.k8s.com.cn_datasets.yaml | 107 ++++++++ ...rcadia.kubeagi.k8s.com.cn_datasources.yaml | 2 + ....kubeagi.k8s.com.cn_versioneddatasets.yaml | 234 +++++++++++++++++ config/crd/kustomization.yaml | 6 + .../crd/patches/cainjection_in_datasets.yaml | 7 + .../cainjection_in_versioneddatasets.yaml | 7 + config/crd/patches/webhook_in_datasets.yaml | 16 ++ .../patches/webhook_in_versioneddatasets.yaml | 16 ++ config/rbac/dataset_editor_role.yaml | 24 ++ config/rbac/dataset_viewer_role.yaml | 20 ++ config/rbac/role.yaml | 52 ++++ config/rbac/versioneddataset_editor_role.yaml | 24 ++ config/rbac/versioneddataset_viewer_role.yaml | 20 ++ config/samples/arcadia_v1alpha1_dataset.yaml | 6 + .../arcadia_v1alpha1_versioneddataset.yaml | 6 + config/samples/kustomization.yaml | 2 + controllers/dataset_controller.go | 62 +++++ controllers/versioneddataset_controller.go | 62 +++++ hack/install-operator-sdk.sh | 5 +- main.go | 14 ++ 32 files changed, 1318 insertions(+), 7 deletions(-) create mode 100644 api/v1alpha1/dataset.go create mode 100644 api/v1alpha1/dataset_types.go create mode 100644 api/v1alpha1/versioneddataset.go create mode 100644 api/v1alpha1/versioneddataset_types.go create mode 100644 assets/dataset_storage.drawio create mode 100644 assets/dataset_storage.drawio.png create mode 100644 config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasets.yaml create mode 100644 config/crd/bases/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml create mode 100644 config/crd/patches/cainjection_in_datasets.yaml create mode 100644 config/crd/patches/cainjection_in_versioneddatasets.yaml create mode 100644 config/crd/patches/webhook_in_datasets.yaml create mode 100644 config/crd/patches/webhook_in_versioneddatasets.yaml create mode 100644 config/rbac/dataset_editor_role.yaml create mode 100644 config/rbac/dataset_viewer_role.yaml create mode 100644 config/rbac/versioneddataset_editor_role.yaml create mode 100644 config/rbac/versioneddataset_viewer_role.yaml create mode 100644 config/samples/arcadia_v1alpha1_dataset.yaml create mode 100644 config/samples/arcadia_v1alpha1_versioneddataset.yaml create mode 100644 controllers/dataset_controller.go create mode 100644 controllers/versioneddataset_controller.go diff --git a/PROJECT b/PROJECT index d06ede5eb..194360c00 100644 --- a/PROJECT +++ b/PROJECT @@ -1,7 +1,3 @@ -# Code generated by tool. DO NOT EDIT. -# This file is used to track the info used to scaffold your project -# and allow the plugins properly work. -# More info: https://book.kubebuilder.io/reference/project-config.html componentConfig: true domain: kubeagi.k8s.com.cn layout: @@ -58,4 +54,22 @@ resources: kind: Embedders path: github.com/kubeagi/arcadia/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: kubeagi.k8s.com.cn + group: arcadia + kind: Dataset + path: github.com/kubeagi/arcadia/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: kubeagi.k8s.com.cn + group: arcadia + kind: VersionedDataset + path: github.com/kubeagi/arcadia/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/condition.go b/api/v1alpha1/condition.go index ea9c51ff2..da93fedc0 100644 --- a/api/v1alpha1/condition.go +++ b/api/v1alpha1/condition.go @@ -42,6 +42,7 @@ type ConditionReason string // Some common Condition reasons. const ( + ReasonPublished ConditionReason = "Published" ReasonAvailable ConditionReason = "Available" ReasonUnavailable ConditionReason = "Unavailable" ReasonCreating ConditionReason = "Creating" diff --git a/api/v1alpha1/dataset.go b/api/v1alpha1/dataset.go new file mode 100644 index 000000000..f073fba11 --- /dev/null +++ b/api/v1alpha1/dataset.go @@ -0,0 +1,33 @@ +/* +Copyright 2023 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +type DatasetContentType string + +const ( + DatasetContentTypeText DatasetContentType = "text" + DatasetContentTypeImage DatasetContentType = "image" + DatasetContentTypeVoice DatasetContentType = "voice" + DatasetContentTypeVideo DatasetContentType = "video" +) + +var ( + // LabelDatasetScene defines the content type of this dataset + LabelDatasetContentType = Group + "/content-type" + // LabelDatasetBestCase defines the best case to use this dataset + LabelDatasetBestCase = Group + "/best-case" +) diff --git a/api/v1alpha1/dataset_types.go b/api/v1alpha1/dataset_types.go new file mode 100644 index 000000000..674130dbb --- /dev/null +++ b/api/v1alpha1/dataset_types.go @@ -0,0 +1,72 @@ +/* +Copyright 2023 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// DatasetSpec defines the desired state of Dataset +type DatasetSpec struct { + // Creator defines dataset creator(AUTO-FILLED by webhook) + Creator string `json:"creator,omitempty"` + + // DisplayName defines dataset display name + DiplayName string `json:"displayName"` + + // ContentType defines dataset + ContentType string `json:"contentType"` + + // bestCase defines the best case to use this dataset + BestCase string `json:"bestCase,omitempty"` +} + +// DatasetStatus defines the observed state of Dataset +type DatasetStatus struct { + // ConditionedStatus is the current status + ConditionedStatus `json:",inline"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="display-name",type=string,JSONPath=`.spec.displayName` +//+kubebuilder:printcolumn:name="type",type=string,JSONPath=`.spec.contentType` + +// Dataset is the Schema for the datasets API +type Dataset struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DatasetSpec `json:"spec,omitempty"` + Status DatasetStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// DatasetList contains a list of Dataset +type DatasetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Dataset `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Dataset{}, &DatasetList{}) +} diff --git a/api/v1alpha1/datasource_types.go b/api/v1alpha1/datasource_types.go index de186f6b9..0fe805690 100644 --- a/api/v1alpha1/datasource_types.go +++ b/api/v1alpha1/datasource_types.go @@ -29,7 +29,7 @@ type DatasourceSpec struct { Creator string `json:"creator,omitempty"` // DisplayName defines datasource display name - DiplayName string `json:"displayName,omitempty"` + DiplayName string `json:"displayName"` // Description defines datasource description Description string `json:"description,omitempty"` diff --git a/api/v1alpha1/versioneddataset.go b/api/v1alpha1/versioneddataset.go new file mode 100644 index 000000000..7520ecbb9 --- /dev/null +++ b/api/v1alpha1/versioneddataset.go @@ -0,0 +1,21 @@ +/* +Copyright 2023 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +var ( + LabelVersionedDatasetVersion = Group + "/version" +) diff --git a/api/v1alpha1/versioneddataset_types.go b/api/v1alpha1/versioneddataset_types.go new file mode 100644 index 000000000..d669e0de1 --- /dev/null +++ b/api/v1alpha1/versioneddataset_types.go @@ -0,0 +1,86 @@ +/* +Copyright 2023 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +type File struct { + // From defines the datasource which provides this `File` + From *TypedObjectReference `json:"from"` + // Path defines the detail path to get this `File` + Path string `json:"path"` +} + +type FileStatus struct { + // UploadCondition records the status of file upload + UploadCondition Condition `json:"uploadCondition,omitempty"` + // ProcessCondition records the status of data processing + ProcessCondition Condition `json:"processCondition,omitempty"` +} + +// VersionedDatasetSpec defines the desired state of VersionedDataset +type VersionedDatasetSpec struct { + // Dataset which this `VersionedDataset` belongs to + Dataset *TypedObjectReference `json:"dataset"` + + // Version + Version string `json:"version"` + + // Files included in this `VersionedDataset` + Files []File `json:"files,omitempty"` +} + +// VersionedDatasetStatus defines the observed state of VersionedDataset +type VersionedDatasetStatus struct { + // ConditionedStatus is the current status + ConditionedStatus `json:",inline"` + + // FilesStatus contains the status to all files in VersionedDatasetSpec + FilesStatus []FileStatus `json:"filesStatus,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="dataset",type=string,JSONPath=`.spec.dataset.name` +//+kubebuilder:printcolumn:name="version",type=string,JSONPath=`.spec.version` + +// VersionedDataset is the Schema for the versioneddatasets API +type VersionedDataset struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec VersionedDatasetSpec `json:"spec,omitempty"` + Status VersionedDatasetStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// VersionedDatasetList contains a list of VersionedDataset +type VersionedDatasetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VersionedDataset `json:"items"` +} + +func init() { + SchemeBuilder.Register(&VersionedDataset{}, &VersionedDatasetList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 07b3efe93..8572c54f1 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -65,6 +65,96 @@ func (in *ConditionedStatus) DeepCopy() *ConditionedStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Dataset) DeepCopyInto(out *Dataset) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dataset. +func (in *Dataset) DeepCopy() *Dataset { + if in == nil { + return nil + } + out := new(Dataset) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Dataset) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DatasetList) DeepCopyInto(out *DatasetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Dataset, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatasetList. +func (in *DatasetList) DeepCopy() *DatasetList { + if in == nil { + return nil + } + out := new(DatasetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DatasetList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DatasetSpec) DeepCopyInto(out *DatasetSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatasetSpec. +func (in *DatasetSpec) DeepCopy() *DatasetSpec { + if in == nil { + return nil + } + out := new(DatasetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DatasetStatus) DeepCopyInto(out *DatasetStatus) { + *out = *in + in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatasetStatus. +func (in *DatasetStatus) DeepCopy() *DatasetStatus { + if in == nil { + return nil + } + out := new(DatasetStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Datasource) DeepCopyInto(out *Datasource) { *out = *in @@ -280,6 +370,43 @@ func (in *Endpoint) DeepCopy() *Endpoint { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *File) DeepCopyInto(out *File) { + *out = *in + if in.From != nil { + in, out := &in.From, &out.From + *out = new(TypedObjectReference) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new File. +func (in *File) DeepCopy() *File { + if in == nil { + return nil + } + out := new(File) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileStatus) DeepCopyInto(out *FileStatus) { + *out = *in + in.UploadCondition.DeepCopyInto(&out.UploadCondition) + in.ProcessCondition.DeepCopyInto(&out.ProcessCondition) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileStatus. +func (in *FileStatus) DeepCopy() *FileStatus { + if in == nil { + return nil + } + out := new(FileStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LLM) DeepCopyInto(out *LLM) { *out = *in @@ -603,3 +730,112 @@ func (in *TypedObjectReference) DeepCopy() *TypedObjectReference { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VersionedDataset) DeepCopyInto(out *VersionedDataset) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionedDataset. +func (in *VersionedDataset) DeepCopy() *VersionedDataset { + if in == nil { + return nil + } + out := new(VersionedDataset) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VersionedDataset) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VersionedDatasetList) DeepCopyInto(out *VersionedDatasetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VersionedDataset, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionedDatasetList. +func (in *VersionedDatasetList) DeepCopy() *VersionedDatasetList { + if in == nil { + return nil + } + out := new(VersionedDatasetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VersionedDatasetList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VersionedDatasetSpec) DeepCopyInto(out *VersionedDatasetSpec) { + *out = *in + if in.Dataset != nil { + in, out := &in.Dataset, &out.Dataset + *out = new(TypedObjectReference) + (*in).DeepCopyInto(*out) + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = make([]File, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionedDatasetSpec. +func (in *VersionedDatasetSpec) DeepCopy() *VersionedDatasetSpec { + if in == nil { + return nil + } + out := new(VersionedDatasetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VersionedDatasetStatus) DeepCopyInto(out *VersionedDatasetStatus) { + *out = *in + in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus) + if in.FilesStatus != nil { + in, out := &in.FilesStatus, &out.FilesStatus + *out = make([]FileStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionedDatasetStatus. +func (in *VersionedDatasetStatus) DeepCopy() *VersionedDatasetStatus { + if in == nil { + return nil + } + out := new(VersionedDatasetStatus) + in.DeepCopyInto(out) + return out +} diff --git a/assets/dataset_storage.drawio b/assets/dataset_storage.drawio new file mode 100644 index 000000000..7e2928f4e --- /dev/null +++ b/assets/dataset_storage.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/assets/dataset_storage.drawio.png b/assets/dataset_storage.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..5a210f221f3d7aa29d204e40d1a95b72de8ffa84 GIT binary patch literal 61114 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 AtpET3 literal 0 HcmV?d00001 diff --git a/assets/system_datasource.drawio b/assets/system_datasource.drawio index b84cfffb6..a9f29d8f1 100644 --- a/assets/system_datasource.drawio +++ b/assets/system_datasource.drawio @@ -1 +1,158 @@ -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 \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/system_datasource.drawio.png b/assets/system_datasource.drawio.png index 5782f41e838689baa4b6bfb716bdca294720fb72..e484b12b3272ca8fe7035815c42e07b30a0f4300 100644 GIT binary patch literal 80489 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

1D3V(->uq*AYMnC4Jf{W*Vi% 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

{WagZ3u*uxv798KE= zmmLjifS+3rr0^*vu-F%BR&;_2WZY+yhVpKKOjgC(;=cgqyWfT(lxxu&%iz-!LzfBC zM|xUiP;$YgVxJ9rlfvgA89sxQJM1b2WUT<0_tQ=1-0)IjeQMT?3=rec?(e*qUd|p{ zh91VAvRqL3;)}?A2i4u{(>e7B&%=HmSxk?kA*p{7h++ey72cK^cEkb>hlSVebv3^u zjlxCub$+~_E3&@(zTZp`gH;G7i+gbgF<}_D+;ciZMs{}PpkrzdC{6F1#V>!u4X;QN z3Up>XCE@^tBUzxSN>VdtsOP`@bBGZ-$jNe4x}V!QBEDh#SbPG=im> zH@LF4Jy{9sDp+<-i{*z>gzEYziL|j`-@kv~FrOE?y^3M}vO4g&;92W%*AR-b`GP&m zYPWfz<^~@~kCEXQCC*a#@LalO94MpEZT;eK1T?9ComlfK8lpEb`EDx2qOwwQRWhI^ z>Pk!|?BFn&=N;o~khsRd9cLV$F7zd_<(6ogf?>S*A_Nm~MJ`&F`< zi~h*cX18-LR@dPyk+`NClYIk9jm{u!cf!th^?u~5el!dEmff7@4q}NIS%#% zc$OzO;i4lCF8>S4_2xVR`XYH9t+l-kip~pUf@Gk)%dY!IqP!Q7G_m@n5=Cm$-(c#30FqjR`Wid`OR9@Z^r_19XQcvuE7E7H3f5{S~h>u5|v z&)Hb?vd3**y$Pvb;xV(f#!%&Rkir{Qu$cs8-fc9!O)yh(eiSrN!j&q$DEV>qFLy9VL-;gVf* zK4e~Mpg?@51n{JME{;i}v!AoNPMvIPB-%{m=V88U{bJK|yxd&iw@ z!xoM7v;Mtu-i4cCDx{tL!B^u-X zfe(KwD4a_VcRo3Q|XT2FtfQ4$HYq5muF}xwbeNOR7SH zPI{tM!!9-)r{*=)3?&zc&DGe&cpeMao(c%3I;k7d`gsXGHC(2!{2#G_6@mS0SoRB! z8J~G}7Cr=~e5j10cPqW{ZKYk(FL*|zd03x0Ke-dg?|T1rg(KBqmZ?qeLWXs>z&1Dc zZkv!|TMsvr-7|*FQIFa`2(d^cUw0J!6ek{>I^sn(YGLV|q#>Ms8U$p$M5_pF+gmFo zZGBfSC*Um01z$@GH-jVMXj4E-5gr|!u0*fxIg!wjb(SQo%Fe&NN7Iy*ml>shKZ(A4 zSx3^~st!xU2PC-%)G)Sy~d2#yqI$(M!nL2{X zF_<0c(iKDWLD9=4f3I7z)|_4CC4aQ*`>(m2zE6zZ?PlKDkRGv>lR|b?U2}wvedznkH(6r*FpUB{^dvZ`E1P%t}=zm?TAs4@GISt%SWWwZW2M?RH9;V z+s3_r8gs;9xv7x4WhD%sMZ7R(NBI{Q;NDj%V8nyHn%HK}ipwNfV9zofH9V$+I6mTWuNy${r7%y z{yoWF0quy8N4p}GQEu`^GzsqhqiS7O(I_Ige4GSp_$4cMGf@R-*|7OPkQCJyB0W@{QHTcQbC4|Yva2; z^HaPmb@`RlpJ@2yUKu%FNeS2@kxr6KI^BGtGi@q~$+dZ(OM_QN;torwh^hq+nJeIy&_RkuRzH=dI z{FxB~yZ{^+*w|t{d;38h)UHL+xX+A;mOgfw(YrdA@;{+FO#=*HL4I}F26eSJj}43c zGR3>i9q<`*mj;#aVVWt*oQ9Ndzg>`G5e;T*%XiE)l%SYQKPi2(aCnljw{yCn!i>L6 zVDw$>9o1SvHaH1S^Y?tOcE{%*B3bidNGN#(25c%VnO5u1Kd)i6i+Jlx^v2U$4r^DO zW934|e=9v54bK^OzVv2q!p2E#^$93c4$947)WomzA z&^5penE?-{w_K3#30UND$m7Wg(Q-#GJgnwthua1{PnEV8yBBwY1H3-V#3<)o;@Diq z2x0M?IVE|J?MAx2H*$82cDh~e+K%#C7+AZ#ZB=E6*Ik^H*69|QT-?2piBE8CetaE#?_d<#4c*pWw|dp4O0vEH~`=yHZ6&Z9+-5luo>p zR)}1bQR=yuI2neD1fR+?m-W(Eg@_lPI2`e}`penRlaJlNj!fe)1AZNFLdz>3@(O_= zXd-;Wh8^goVt>f|{gNinfYwQIM}8b_*Ryhdam|mw<#Z5LpMruFj;7ed-|A7bpH=Nu zH&+uw2HW&2smx`=+akDTD)%8z+_NSeovQ}hqM>RVs~chz*7c;TowUf;UgXxZq#Yu? zO85et2>9+@t+$I`Ynrd<1?wAfcj~8MPHKQ({#C(7E&|n+~j(yw9nNR zGk1Mhlh7+rNh3IyTj-mlsuA+S1C`GbUrML@eq2W{SDDLVy;qfv)n|RDNh5{vH0!jc0%W3 zoqv=JRd?TrQY>F*&=~sc2`akVU(5wvl=-6y~i!Vux!iWsgr!)Db4OUwVBn=<4f!#m_e2=Jg<=3Qx5;xiMa> zspoWEAC;&6HVNqb#51XCg-z|D_{myu?o70b59 z>y+-IYc~bu8l1%qWT2s`k(%lUB_e-9Kiixfiw6;9+B8nU*Ko5wO8f3$Yt^mg>v707 zohg)YzC^ll>;+g$0x8x(-geBp3jieb(L=X!fAb`9QO45fNV!4w_Ks~Za!t!l$Sx|ju>O2ji54;p_P&qde~g80x5Cip|YT*CQb~jDQr~j zSeY|y9)8wNpAkCcs~=J}`-7j)cG8SQAT9263ryFnEQeHEg*by=?!u;CP8z(A^p4i_ zz;cXwa95rnw~_ z&>vk2Y>a0F$}mKcjPE==POK|fSx<5%wy{IiVLg>NsQ04Iq&Y3Ld&Vrm5qG0(rgR@f z^Eth7Y7wxdeV|a5?;Wsw#F|qadkddUDlgAWJ9Jnr{+g=^d0^!Vp(FfJTjoss$n{sV zAO;3$iqrh!SD(`nRPySVe(UBEmE0b^=ZNb-&d$ssFI^E8yt=fm>xgZ*>~fM(M`*sS z=U3D)9=unBJBY~FsbTKmv}~>V~p zcvE5j zl$F;^E<(Z`!$-}dd}PPgGo3Sg4Xfuhyf2F7(Av9ubca7kx&jlD7O z33o+$qI}gsT@7W0%YODcv!je&t}C*(>5j(|L-0yiM@`^6f&39F*2Ecd;{HHKb`_%tBq+$zcv-JWS%)yWo~EK4c{> z&_02zL~&pxE-V@0==1lP3L!9tdUoY^YVi9PS~5Qbm=E}1sTC9=+a`S?1y8-l3-{KS z5GRqQg`k&Nyr$*g(aatf; z;W22u+b^Cd!G}u$4t>{QZMJ%$1X|A>WTi15Vk->a2`LooTRR781Kzs~g@<9NvJxPp zVAv47ZsZ1;9ery~>RTiNCq#L!KsXl!pUpw2;TyuOrl9+E#}AJj;26&!c=*7cK3)u7 z38Wr$l>VKsw~8QEUH|meU1RmWa+yyDKVtKN2HpUNWa5*M84xsk%srkwQ+T6OI;1<6 zxX9R~5M1Yn>&w8$(ZZ%sG~^R`8dT~d_8t#i(&Dpa<2#!5pB_GNmwXqX?_%QP2e9ha zeF1m4gOY6lsh=V#vmJ6!$*faWdz$VPyxf5+eNW`0s3{2|!F{@4>V9sZPdW2PaA4k@p_W8vXh2b5caM=Kf_#s%fDme42 zzF1M3@YrHn-vBk3C8unHm@=stT-`_Jc_uQUoFh*tpDIWQA_^V*OITf)kl@W9#Nb4E ze}>X&U|m0%f&29p>U0$7^qowMKZ%*9DU~EV;k6+LK@y+crb|{ASu9l$*ZWb3?0DNi z)&5~d;KPGx;CBF1o{&8N0*aXdH=4Qc7naoDjWOB;CD-yhFltr5{hX2*92%Z| zK>j|Rh}o3j`Iqt>e|&+_u>?Z$kjZ@ZViiTEMHwncMTA0te~56G)D-+d>Q2pcfmEy& z0?$=OnHKiDM;<)JKT!Q!>I%=`JRb2ZvKL@O84nHb#sx&{MK3+Q})-a_3n>Wvm&DnZ);xJ zzfJN;o1_TA2~H*O|JkE$yZhBAukLJ)*srwqc5&ydpO9#(9ftVWNhJJ&#-?^}jkfF9 zwS*rs^$sUqDDD^)KL-G>aPiA9@@cAPX$(SCr?S++q3GWy3Sw=V>QUdGGo+!YF z%R%{ZOT!MKaaLGjuX;ci;df8-mu||b0_$;J4{P#S;`zkXGg02WNX`+H^9}3zj71dt zhogGs$uXlKzp7}}+QGuWpRgK%*K!A#1D<;Ho+~gNMs{@DYO*AOBd*J@Qt~dR1s@u? zw3iy>0F{4?bPT15-tO#v`-7RT>+FDq^H+GhD>mOHN}^1QK&BY8&XzoU9pHsz%T*7V zW83IJ8i*vm*r*V!Lry7qll|WCMK~(VvUTF=q)!r}^29relPJ5Vmh@PdZqJF;3x3>N z#8$rQr9-klA}NpSirM6DMi+;<@D_~Jt#l9IvHao%H@oa=Yz`kLanN{y5Qd2 z+^jSg)7jZOP>apzN@0PK1d1maU8WV#!OdX(G^8pNpnp2f44rBq0?%7Ns5$ zAK4-BFk{ev{(J9I_whOtQgWr7iRW*QW?Izwi;rvv@)qfwbg#J_$;Zq}dQrn{IL6G+ z-gqemg?KY^EqGZyU2G#=7)S`)`*T5Q_nSVrATEdMLY4i%>y+Q__kLHUmi_M<7{SW7i*?qR^Y`pwG$=Mz`7f}!lBkYgN{I8)!X&I$5i2+P zbKN{QHZm5+dN@yT-up~LDeRw69B-yB+&{+K2pdRoE5Vq=Yy=bskZg}N4io*lE!VDoNbHKa! z8tcVt{aumnFWi>Xd%L${<3}Wx(~;=_nptJ#y>$I@Ryl|{ZiqGgB<|zsLkCPwYm(Nr z?U#wg38+q(4ubT224EFvPFFBQQDyWOX%f=pM2BDE30zZ@Ki+GRNPEe+q{?U+UEYz| zhuZyW79YP6Ct@hbAUZT*dWEqxD+JpYj;&uME$GegTd!~EcFVi2Gw=5P%rVJk#RaEi z5p+KAr47jo9glgxHPl1HgmN{FxUMZw{|xtrnZ5S20*=5H5d(qg{6 z@Wa}(HI>AY+$Elom({UORX`ZCK9*Fc37xX+=V(O*;pdPaZfDGniC)HdMv0N-E(JOX&3%Lr)oWDIyX;=THG@(q1mcGbux+gGPOjwAef1Iv zG!t=o-ICKsq}xp&kAm~2agr89Pn2TYK#X1-bNTZS3cg*q@@c8+Dy=2<+hbkCHOdzo ziE@Zt1(pt=veCPnSWuh8s$(ufw6%@i`M3Z%4P#^ehAqJM@Uwo*{T1M*FSn4l>!G%V zRS7Y2#0@Lbh&4 z$>L^?0EhJx#7SPT?63%|KNj}*R3T#_PSOybQW7iw7)DsO_WdA}-gQH)eJ-q}l3%~T z&5F^j~GfkE2dP`#wqh8b{+{|HFm$h z^#{yGB0Pa;@3)X0H0tm)1%WsQ%60Cd-sCM9rSxZJ1z8W0%3srB=D1e0YL)Xt2XPYf zKI#EPx=F77d!v|ygE)qwV$EqATwEd}AqO0snpxgQ4B8IWt~52g7knGEY&~%ti)7+d zUwYAix1S6!I*X$q?SZyoCMOG-LPTd%i3<*On@8f;(PL6LsGzAd-XI;Yzppg zf;l?zn}@W(L;Qea4v(yn2=rMToV_#fmHVL6##%FkFR2mGNkDG=R{v&I8Ay|OHqFO` zYz}30%T1<)06NMq$apiGltwoggM7YVhI(|k{?N>XVKi` z^||&m<@9RSY51^tmv7er8pAXW#?UvA_mlK~f(N<%0l>V+_wE+~5Bd)k9NptqkEed| zwXxnf90(j8lJMjS_VRToMdv@k{bY=}70bmGcfyTpQ$#>1rI39q)Xol!nR=ccG%jUp&kp=OX=ED;Vg`}fK>|ax zzcku$;Dq(EMsEcGk`flY4wAsfi9aF)Vv_aoF?Td$DR55k1ta0UNFN zN)&)M9H9jnLVXD+>NdV$Ho^^_xng!k4(+TgLo#i72^tSyTr~iXMq$s3@~V7nNaJXS zU@`FbzkjORg9us)tf}w8fByXW&KrZO%lmxp%Zu_3LP*g-O70G`zX%0ua=D%0$YKk} zfVjOW!khvUiyxB%&J0Rg10(t2*& z+=L)!J;ia-!ft?XP5iSLGg?ctdf7bUq%iytSY>#+OW2H)X(`upQRxp!5NzR!qRnZwihVV!jKN?O?0F!&|3-2NF}D)%U=?L{|L zL^psWvhwp~K_vs)wa-qeKT@A-zpYt#aC`~K#4CUn9ADzT!o=ycqf@()UvP8?Q{9Q2Y^Pb5>MkeT6b+w$kdjl>$ejmWjmZRlZhjx|t*QuA5+E8r` z8gTeTz`1a@Ari#BLvUgmEOvAnFu!8LK!y1nX8k4|5Q>{t_P1t5mS1 za*%9nv0*!3I7E<~DHJJkdV719hos1kgYw*VYNZDFHw)hyqWID7ou}~GAMvMD)-8n# z7ImI9{#} zrj;Nv*v$4Gt@`n@&jh59AsW z=3C%20S5FVnMa9{i79Thb}g0Zbk?l|_wnN=WI|VNCJiA#@&*>~p##{v9t>uY)yri> ztJA`1FcY(5v`E|JY;S?nW?p#E7jdbDt9VuXxjevzF$B_JbZ|V_U=@Vv9j5k&!gkLE ze})o1484!d4#msPU#z0Zw9tbg5v(X0(Y5h%k5*DBgxX?av+BvigR1@*R7&CKXTz(Gs^1-=Z{>@2AbAg&5dqvdQ$YLI}4X%F)uZ6~3h zxIuPdEGmDz-+K%;K)AGl^o7%T$Y`|p8aSU_ULfeefd=s22G@Zo#7n6kaMEAqzz@gu zsOCVn!t_A-h^BW~T|&7zUQrQ6wjd(bNA}GhAwvWlUQbjf=)hCIfgX`~#3E1?1!Ne; zjf$?Tuh#387(qn z1~q$Yc}`}jJo*a($|p}HY}9h&DPK2NiMZS0j%apjT}R^MDZP?M&LZw9p%=~{ROcF1 zEz>k6wxctGle1T=ce|UWZs{7De5T`0m(QaeuQVq355m7(qhH+MoBWBcFt$G!^ceHC z9Gtg`h%fAGqK}Rx54)|r+S zv242$`7BBcxPEIl$&y}b<~a$rw(j39KlS&@`-5C*GUpy`oGPFjO@|TJh^im&;ncLg zNm*tP89L$EXx&q(13%tfmL%or&aenqEE{KI^2F zxgDlnw|*1v5%NiP^wg{X$$Y9-U)F4fZg`f#s7ja4gzi=^`YT#YindbJXmn$=>kopc zPmXyF2Q+yeTONcb0rtmDBsk3m~< znj(`&pXu1^R+@8d5Hlzurp`>gZ%6Jne=N-TSVczQb&BWg(Hx7)Nw$;(KflTEIz((C zav5wXQx8S?m06D@V?AD&pVm&sudJ0AS()dq-@d7Hn?Gh+3ulvYcP)R)%JfQLh&}B) zYIsb|GJ?gNF58OP3)Y9Pc5nl`KaMps zo;3qe#S*sBoTmQDexv^R_w=cLPt!hi!%u11a(vnE+H>l*Wger~Dyp^0{FX_^yDTMg ztB3(IlUpxU_^-!?%RQ^nF0aQ_$+vhfcqZSO$<^!e>@8AJyNDLYE;Gl5R^)6r)HmOz zWXTg16DG~f-Dp>{cr0n}erl|47-HcdK`r@Y*+Wpu3o} zUA`a9&MbCtYa8^mgdskOhKbr~NH9oFPKC&y`KCO4YVY7dkeo~;+*iflF7HSjzJ?Yx zDGVyA%~i;A4)CcW{v6Gah_>xqew&!5yv6!0xDc=n2#h(cw`RiQ1W1uoc)n8+8X0rnpM85S zsVeo$c2(XJjVaQ@o2UAmY=ZJ#1uh1Ds=C(^k*$%2 zjMO>}2?#n+AR_LrfR-*e(`SzxlTAs(6UtHKmTwl{_qXGIb$Y`<1aoafNzh$;KA|hm z!r!lM@cO4y6S)}KO}@2a!zf*jO()$_LBPd@h(0D?m|RHq!NHoEyhtep6(WbqMSZy*`S+e0o zpA^MeA-s{QOXK6kxn}?R155D3*_l-w*upr>er~GC6UXis&8^?N^=EA?ZEv^t8z#SZ zf{nGVE~hO+E(k`yV7sIXX*7@tm!H;RKOTwXOO;dVS7_1kY`q|%x7I!t_p9DlM?`EG zCW6?`WiT#AjmY3@i`5*LDAk*J7a3;a!c*&>X5GR!{-b9%hA3;5{2NKx73rD}bUnd* zZfNAWnZ~K=T;Y>N_;jc)JXDvR3V*^Nbg9IcbG5C?F*3SCGJtD5-LalSn2&?9GGx<+f_-r@KsH0uxUlKN`==CJwS)r_5!?mIJ*n5zM zW^Jz=jA$9n@<%l zy%~PZS!J1?>Nn{y3;WQa3N)ZqgIsU(LBi9fro2JJ{KgDLR1?*kKN4W&yK≤6*>> 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%0Am-SR8$68cWi$%TVCk!BNGoD88IhgDY3X%(Vc! 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%0DClb!=>mC$Ffg{9+oAEEx{ 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 bbLRy5?7MPo)W3%>fIr<^2AUt$?IZsWCUftV diff --git a/config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasets.yaml b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasets.yaml new file mode 100644 index 000000000..912e593aa --- /dev/null +++ b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasets.yaml @@ -0,0 +1,107 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: datasets.arcadia.kubeagi.k8s.com.cn +spec: + group: arcadia.kubeagi.k8s.com.cn + names: + kind: Dataset + listKind: DatasetList + plural: datasets + singular: dataset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.displayName + name: display-name + type: string + - jsonPath: .spec.contentType + name: type + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Dataset is the Schema for the datasets API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: DatasetSpec defines the desired state of Dataset + properties: + bestCase: + description: bestCase defines the best case to use this dataset + type: string + contentType: + description: ContentType defines dataset + type: string + creator: + description: Creator defines dataset creator(AUTO-FILLED by webhook) + type: string + displayName: + description: DisplayName defines dataset display name + type: string + required: + - contentType + - displayName + type: object + status: + description: DatasetStatus defines the observed state of Dataset + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastSuccessfulTime: + description: LastSuccessfulTime is repository Last Successful + Update Time + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown + type: string + type: + description: Type of this condition. At most one of each condition + type may apply to a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasources.yaml b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasources.yaml index 0f237d92e..590497c12 100644 --- a/config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasources.yaml +++ b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_datasources.yaml @@ -94,6 +94,8 @@ spec: object: type: string type: object + required: + - displayName type: object status: description: DatasourceStatus defines the observed state of Datasource diff --git a/config/crd/bases/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml new file mode 100644 index 000000000..b2c83eb0d --- /dev/null +++ b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml @@ -0,0 +1,234 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: versioneddatasets.arcadia.kubeagi.k8s.com.cn +spec: + group: arcadia.kubeagi.k8s.com.cn + names: + kind: VersionedDataset + listKind: VersionedDatasetList + plural: versioneddatasets + singular: versioneddataset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.dataset.name + name: dataset + type: string + - jsonPath: .spec.version + name: version + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: VersionedDataset is the Schema for the versioneddatasets API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: VersionedDatasetSpec defines the desired state of VersionedDataset + properties: + dataset: + description: Dataset which this `VersionedDataset` belongs to + properties: + apiGroup: + description: APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in + the core API group. For any other third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: Namespace is the namespace of resource being referenced + type: string + required: + - kind + - name + type: object + files: + description: Files included in this `VersionedDataset` + items: + properties: + from: + description: From defines the datasource which provides this + `File` + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: Namespace is the namespace of resource being + referenced + type: string + required: + - kind + - name + type: object + path: + description: Path defines the detail path to get this `File` + type: string + required: + - from + - path + type: object + type: array + version: + description: Version + type: string + required: + - dataset + - version + type: object + status: + description: VersionedDatasetStatus defines the observed state of VersionedDataset + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastSuccessfulTime: + description: LastSuccessfulTime is repository Last Successful + Update Time + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown + type: string + type: + description: Type of this condition. At most one of each condition + type may apply to a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + filesStatus: + description: FilesStatus contains the status to all files in VersionedDatasetSpec + items: + properties: + processCondition: + description: ProcessCondition records the status of data processing + properties: + lastSuccessfulTime: + description: LastSuccessfulTime is repository Last Successful + Update Time + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition + from one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown + type: string + type: + description: Type of this condition. At most one of each + condition type may apply to a resource at any point in + time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + uploadCondition: + description: UploadCondition records the status of file upload + properties: + lastSuccessfulTime: + description: LastSuccessfulTime is repository Last Successful + Update Time + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition + from one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown + type: string + type: + description: Type of this condition. At most one of each + condition type may apply to a resource at any point in + time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 1e3c33e33..df49548ff 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,6 +7,8 @@ resources: - bases/arcadia.kubeagi.k8s.com.cn_prompts.yaml - bases/arcadia.kubeagi.k8s.com.cn_datasources.yaml - bases/arcadia.kubeagi.k8s.com.cn_embedders.yaml +- bases/arcadia.kubeagi.k8s.com.cn_datasets.yaml +- bases/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -17,6 +19,8 @@ patchesStrategicMerge: #- patches/webhook_in_prompts.yaml #- patches/webhook_in_datasources.yaml #- patches/webhook_in_embedders.yaml +#- patches/webhook_in_datasets.yaml +#- patches/webhook_in_versioneddatasets.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -26,6 +30,8 @@ patchesStrategicMerge: #- patches/cainjection_in_prompts.yaml #- patches/cainjection_in_datasources.yaml #- patches/cainjection_in_embedders.yaml +#- patches/cainjection_in_datasets.yaml +#- patches/cainjection_in_versioneddatasets.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_datasets.yaml b/config/crd/patches/cainjection_in_datasets.yaml new file mode 100644 index 000000000..064ae9c50 --- /dev/null +++ b/config/crd/patches/cainjection_in_datasets.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: datasets.arcadia.kubeagi.k8s.com.cn diff --git a/config/crd/patches/cainjection_in_versioneddatasets.yaml b/config/crd/patches/cainjection_in_versioneddatasets.yaml new file mode 100644 index 000000000..18ac8d467 --- /dev/null +++ b/config/crd/patches/cainjection_in_versioneddatasets.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: versioneddatasets.arcadia.kubeagi.k8s.com.cn diff --git a/config/crd/patches/webhook_in_datasets.yaml b/config/crd/patches/webhook_in_datasets.yaml new file mode 100644 index 000000000..e591e915d --- /dev/null +++ b/config/crd/patches/webhook_in_datasets.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: datasets.arcadia.kubeagi.k8s.com.cn +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/crd/patches/webhook_in_versioneddatasets.yaml b/config/crd/patches/webhook_in_versioneddatasets.yaml new file mode 100644 index 000000000..22ed62c5a --- /dev/null +++ b/config/crd/patches/webhook_in_versioneddatasets.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: versioneddatasets.arcadia.kubeagi.k8s.com.cn +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/dataset_editor_role.yaml b/config/rbac/dataset_editor_role.yaml new file mode 100644 index 000000000..a0d73bbf1 --- /dev/null +++ b/config/rbac/dataset_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit datasets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dataset-editor-role +rules: +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - datasets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - datasets/status + verbs: + - get diff --git a/config/rbac/dataset_viewer_role.yaml b/config/rbac/dataset_viewer_role.yaml new file mode 100644 index 000000000..deda05156 --- /dev/null +++ b/config/rbac/dataset_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view datasets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dataset-viewer-role +rules: +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - datasets + verbs: + - get + - list + - watch +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - datasets/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index c7527a02c..dd42ed899 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -13,6 +13,32 @@ rules: - get - list - watch +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - datasets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - datasets/finalizers + verbs: + - update +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - datasets/status + verbs: + - get + - patch + - update - apiGroups: - arcadia.kubeagi.k8s.com.cn resources: @@ -143,3 +169,29 @@ rules: - get - patch - update +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - versioneddatasets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - versioneddatasets/finalizers + verbs: + - update +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - versioneddatasets/status + verbs: + - get + - patch + - update diff --git a/config/rbac/versioneddataset_editor_role.yaml b/config/rbac/versioneddataset_editor_role.yaml new file mode 100644 index 000000000..fec80806a --- /dev/null +++ b/config/rbac/versioneddataset_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit versioneddatasets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: versioneddataset-editor-role +rules: +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - versioneddatasets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - versioneddatasets/status + verbs: + - get diff --git a/config/rbac/versioneddataset_viewer_role.yaml b/config/rbac/versioneddataset_viewer_role.yaml new file mode 100644 index 000000000..4ef9f2647 --- /dev/null +++ b/config/rbac/versioneddataset_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view versioneddatasets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: versioneddataset-viewer-role +rules: +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - versioneddatasets + verbs: + - get + - list + - watch +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - versioneddatasets/status + verbs: + - get diff --git a/config/samples/arcadia_v1alpha1_dataset.yaml b/config/samples/arcadia_v1alpha1_dataset.yaml new file mode 100644 index 000000000..6fa860886 --- /dev/null +++ b/config/samples/arcadia_v1alpha1_dataset.yaml @@ -0,0 +1,6 @@ +apiVersion: arcadia.kubeagi.k8s.com.cn/v1alpha1 +kind: Dataset +metadata: + name: dataset-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/arcadia_v1alpha1_versioneddataset.yaml b/config/samples/arcadia_v1alpha1_versioneddataset.yaml new file mode 100644 index 000000000..1324c257f --- /dev/null +++ b/config/samples/arcadia_v1alpha1_versioneddataset.yaml @@ -0,0 +1,6 @@ +apiVersion: arcadia.kubeagi.k8s.com.cn/v1alpha1 +kind: VersionedDataset +metadata: + name: versioneddataset-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 6163e6797..eb170c0d6 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -5,4 +5,6 @@ resources: - arcadia_v1alpha1_prompt.yaml - arcadia_v1alpha1_datasource.yaml - arcadia_v1alpha1_embedders.yaml +- arcadia_v1alpha1_dataset.yaml +- arcadia_v1alpha1_versioneddataset.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/controllers/dataset_controller.go b/controllers/dataset_controller.go new file mode 100644 index 000000000..e477eb459 --- /dev/null +++ b/controllers/dataset_controller.go @@ -0,0 +1,62 @@ +/* +Copyright 2023 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + arcadiav1alpha1 "github.com/kubeagi/arcadia/api/v1alpha1" +) + +// DatasetReconciler reconciles a Dataset object +type DatasetReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=arcadia.kubeagi.k8s.com.cn,resources=datasets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=arcadia.kubeagi.k8s.com.cn,resources=datasets/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=arcadia.kubeagi.k8s.com.cn,resources=datasets/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Dataset object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile +func (r *DatasetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DatasetReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&arcadiav1alpha1.Dataset{}). + Complete(r) +} diff --git a/controllers/versioneddataset_controller.go b/controllers/versioneddataset_controller.go new file mode 100644 index 000000000..ffeebc2d8 --- /dev/null +++ b/controllers/versioneddataset_controller.go @@ -0,0 +1,62 @@ +/* +Copyright 2023 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + arcadiav1alpha1 "github.com/kubeagi/arcadia/api/v1alpha1" +) + +// VersionedDatasetReconciler reconciles a VersionedDataset object +type VersionedDatasetReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=arcadia.kubeagi.k8s.com.cn,resources=versioneddatasets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=arcadia.kubeagi.k8s.com.cn,resources=versioneddatasets/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=arcadia.kubeagi.k8s.com.cn,resources=versioneddatasets/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the VersionedDataset object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile +func (r *VersionedDatasetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *VersionedDatasetReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&arcadiav1alpha1.VersionedDataset{}). + Complete(r) +} diff --git a/hack/install-operator-sdk.sh b/hack/install-operator-sdk.sh index e7abb7b8b..84e3d18b9 100755 --- a/hack/install-operator-sdk.sh +++ b/hack/install-operator-sdk.sh @@ -5,7 +5,9 @@ ARCH=$(go env GOARCH) OS=$(go env GOOS) URL="https://github.com/operator-framework/operator-sdk/releases/download/${OPERATOR_SDK_VERSION}/operator-sdk_${OS}_${ARCH}" -operator_sdk_version=$(operator-sdk version) +if command -v operator-sdk &> /dev/null; then + operator_sdk_version=$(operator-sdk version) +fi if echo $operator_sdk_version | grep -q "$OPERATOR_SDK_VERSION"; then echo "operator-sdk version ${OPERATOR_SDK_VERSION} found, exiting..." @@ -13,6 +15,7 @@ if echo $operator_sdk_version | grep -q "$OPERATOR_SDK_VERSION"; then fi echo "Installing operator-sdk version ${OPERATOR_SDK_VERSION} to /usr/local/bin/operator-sdk" +echo "$URL" curl -L $URL > operator-sdk chmod +x operator-sdk sudo mv operator-sdk /usr/local/bin diff --git a/main.go b/main.go index ce83331e5..bb152878e 100644 --- a/main.go +++ b/main.go @@ -159,6 +159,20 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Embedder") os.Exit(1) } + if err = (&controllers.DatasetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Dataset") + os.Exit(1) + } + if err = (&controllers.VersionedDatasetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "VersionedDataset") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { From 11ff7f1bc35664120a7b2d14c9279207432797c0 Mon Sep 17 00:00:00 2001 From: bjwswang Date: Wed, 1 Nov 2023 07:32:51 +0000 Subject: [PATCH 07/12] refactor: definition of versioned dataset Signed-off-by: bjwswang --- api/v1alpha1/condition.go | 27 ++++- api/v1alpha1/versioneddataset_types.go | 23 ++--- api/v1alpha1/zz_generated.deepcopy.go | 45 +++------ ....kubeagi.k8s.com.cn_versioneddatasets.yaml | 98 +++---------------- 4 files changed, 58 insertions(+), 135 deletions(-) diff --git a/api/v1alpha1/condition.go b/api/v1alpha1/condition.go index da93fedc0..a4665ff4f 100644 --- a/api/v1alpha1/condition.go +++ b/api/v1alpha1/condition.go @@ -42,7 +42,6 @@ type ConditionReason string // Some common Condition reasons. const ( - ReasonPublished ConditionReason = "Published" ReasonAvailable ConditionReason = "Available" ReasonUnavailable ConditionReason = "Unavailable" ReasonCreating ConditionReason = "Creating" @@ -52,6 +51,32 @@ const ( ReasonReconcilePaused ConditionReason = "ReconcilePaused" ) +// Some Data related Condition Types +const ( + // Dataset have 3 phases: load -> process -> publish + // TypeLoaded resources are believed to be loaded + TypeLoaded ConditionType = "Loaded" + // TypeProcessed resources are believed to be processed + TypeProcessed ConditionType = "Processed" + // TypePublished resources are believed to be published + TypePublished ConditionType = "Published" +) + +// Some Dataset related Condition reasons +const ( + // Load data + ReasonDataLoading ConditionReason = "DataLoading" + ReasonDataLoadError ConditionReason = "DataLoadError" + ReasonDataLoadSuccess ConditionReason = "DataLoadSuccess" + // Process data + ReasonDataProcessing ConditionReason = "DataProcessing" + ReasonDataProcessError ConditionReason = "DataProcessError" + ReasonDataProcessSuccess ConditionReason = "DataProcessSuccess" + // Publish dataset + ReasonDatasetUnpublished ConditionReason = "DatasetUnpublished" + ReasonDatasetPublished ConditionReason = "DatasetPublished" +) + // A Condition that may apply to a resource. type Condition struct { // Type of this condition. At most one of each condition type may apply to diff --git a/api/v1alpha1/versioneddataset_types.go b/api/v1alpha1/versioneddataset_types.go index d669e0de1..834233b5a 100644 --- a/api/v1alpha1/versioneddataset_types.go +++ b/api/v1alpha1/versioneddataset_types.go @@ -23,18 +23,11 @@ import ( // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. -type File struct { +type FileGroup struct { // From defines the datasource which provides this `File` - From *TypedObjectReference `json:"from"` - // Path defines the detail path to get this `File` - Path string `json:"path"` -} - -type FileStatus struct { - // UploadCondition records the status of file upload - UploadCondition Condition `json:"uploadCondition,omitempty"` - // ProcessCondition records the status of data processing - ProcessCondition Condition `json:"processCondition,omitempty"` + Datasource *TypedObjectReference `json:"datasource"` + // Paths defines the detail paths to get objects from above datasource + Paths []string `json:"paths"` } // VersionedDatasetSpec defines the desired state of VersionedDataset @@ -45,17 +38,15 @@ type VersionedDatasetSpec struct { // Version Version string `json:"version"` - // Files included in this `VersionedDataset` - Files []File `json:"files,omitempty"` + // FileGroups included in this `VersionedDataset` + // Grouped by Datasource + FileGroups []FileGroup `json:"fileGroups,omitempty"` } // VersionedDatasetStatus defines the observed state of VersionedDataset type VersionedDatasetStatus struct { // ConditionedStatus is the current status ConditionedStatus `json:",inline"` - - // FilesStatus contains the status to all files in VersionedDatasetSpec - FilesStatus []FileStatus `json:"filesStatus,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8572c54f1..d849be646 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -371,38 +371,26 @@ func (in *Endpoint) DeepCopy() *Endpoint { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *File) DeepCopyInto(out *File) { +func (in *FileGroup) DeepCopyInto(out *FileGroup) { *out = *in - if in.From != nil { - in, out := &in.From, &out.From + if in.Datasource != nil { + in, out := &in.Datasource, &out.Datasource *out = new(TypedObjectReference) (*in).DeepCopyInto(*out) } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new File. -func (in *File) DeepCopy() *File { - if in == nil { - return nil + if in.Paths != nil { + in, out := &in.Paths, &out.Paths + *out = make([]string, len(*in)) + copy(*out, *in) } - out := new(File) - in.DeepCopyInto(out) - return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileStatus) DeepCopyInto(out *FileStatus) { - *out = *in - in.UploadCondition.DeepCopyInto(&out.UploadCondition) - in.ProcessCondition.DeepCopyInto(&out.ProcessCondition) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileStatus. -func (in *FileStatus) DeepCopy() *FileStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileGroup. +func (in *FileGroup) DeepCopy() *FileGroup { if in == nil { return nil } - out := new(FileStatus) + out := new(FileGroup) in.DeepCopyInto(out) return out } @@ -798,9 +786,9 @@ func (in *VersionedDatasetSpec) DeepCopyInto(out *VersionedDatasetSpec) { *out = new(TypedObjectReference) (*in).DeepCopyInto(*out) } - if in.Files != nil { - in, out := &in.Files, &out.Files - *out = make([]File, len(*in)) + if in.FileGroups != nil { + in, out := &in.FileGroups, &out.FileGroups + *out = make([]FileGroup, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -821,13 +809,6 @@ func (in *VersionedDatasetSpec) DeepCopy() *VersionedDatasetSpec { func (in *VersionedDatasetStatus) DeepCopyInto(out *VersionedDatasetStatus) { *out = *in in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus) - if in.FilesStatus != nil { - in, out := &in.FilesStatus, &out.FilesStatus - *out = make([]FileStatus, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VersionedDatasetStatus. diff --git a/config/crd/bases/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml index b2c83eb0d..9f06a7ea6 100644 --- a/config/crd/bases/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml +++ b/config/crd/bases/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml @@ -64,11 +64,12 @@ spec: - kind - name type: object - files: - description: Files included in this `VersionedDataset` + fileGroups: + description: FileGroups included in this `VersionedDataset` Grouped + by Datasource items: properties: - from: + datasource: description: From defines the datasource which provides this `File` properties: @@ -92,12 +93,15 @@ spec: - kind - name type: object - path: - description: Path defines the detail path to get this `File` - type: string + paths: + description: Paths defines the detail paths to get objects from + above datasource + items: + type: string + type: array required: - - from - - path + - datasource + - paths type: object type: array version: @@ -148,84 +152,6 @@ spec: - type type: object type: array - filesStatus: - description: FilesStatus contains the status to all files in VersionedDatasetSpec - items: - properties: - processCondition: - description: ProcessCondition records the status of data processing - properties: - lastSuccessfulTime: - description: LastSuccessfulTime is repository Last Successful - Update Time - format: date-time - type: string - lastTransitionTime: - description: LastTransitionTime is the last time this condition - transitioned from one status to another. - format: date-time - type: string - message: - description: A Message containing details about this condition's - last transition from one status to another, if any. - type: string - reason: - description: A Reason for this condition's last transition - from one status to another. - type: string - status: - description: Status of this condition; is it currently True, - False, or Unknown - type: string - type: - description: Type of this condition. At most one of each - condition type may apply to a resource at any point in - time. - type: string - required: - - lastTransitionTime - - reason - - status - - type - type: object - uploadCondition: - description: UploadCondition records the status of file upload - properties: - lastSuccessfulTime: - description: LastSuccessfulTime is repository Last Successful - Update Time - format: date-time - type: string - lastTransitionTime: - description: LastTransitionTime is the last time this condition - transitioned from one status to another. - format: date-time - type: string - message: - description: A Message containing details about this condition's - last transition from one status to another, if any. - type: string - reason: - description: A Reason for this condition's last transition - from one status to another. - type: string - status: - description: Status of this condition; is it currently True, - False, or Unknown - type: string - type: - description: Type of this condition. At most one of each - condition type may apply to a resource at any point in - time. - type: string - required: - - lastTransitionTime - - reason - - status - - type - type: object - type: object - type: array type: object type: object served: true From 31bafe16e5206538710b848e6a8ba43420080487 Mon Sep 17 00:00:00 2001 From: Abirdcfly Date: Wed, 1 Nov 2023 17:21:12 +0800 Subject: [PATCH 08/12] chore: bump helm/chart-testing-action version Signed-off-by: Abirdcfly --- .github/workflows/lint_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint_test.yaml b/.github/workflows/lint_test.yaml index 3a41a9dfb..d50710368 100644 --- a/.github/workflows/lint_test.yaml +++ b/.github/workflows/lint_test.yaml @@ -22,7 +22,7 @@ jobs: check-latest: true - name: Set up chart-testing - uses: helm/chart-testing-action@v2.4.0 + uses: helm/chart-testing-action@v2.6.0 - name: Run chart-testing (list-changed) id: list-changed From 957057f7de7bf1286f0332e8c4de472599dcd351 Mon Sep 17 00:00:00 2001 From: Lanture1064 Date: Tue, 31 Oct 2023 16:07:25 +0800 Subject: [PATCH 09/12] feat: add fastchat API server & controller helm chart Signed-off-by: Lanture1064 <34346740+Lanture1064@users.noreply.github.com> --- charts/arcadia/Chart.yaml | 2 + charts/arcadia/charts/fastchat/.helmignore | 23 +++++++ charts/arcadia/charts/fastchat/Chart.yaml | 33 ++++++++++ .../charts/fastchat/templates/deployment.yaml | 34 +++++++++++ .../charts/fastchat/templates/ingress.yaml | 61 +++++++++++++++++++ .../charts/fastchat/templates/service.yaml | 19 ++++++ charts/arcadia/charts/fastchat/values.yaml | 58 ++++++++++++++++++ llms/Dockerfile | 15 ----- llms/Dockerfile.server | 15 +++++ llms/docker-compose.yml | 4 +- 10 files changed, 247 insertions(+), 17 deletions(-) create mode 100644 charts/arcadia/charts/fastchat/.helmignore create mode 100644 charts/arcadia/charts/fastchat/Chart.yaml create mode 100644 charts/arcadia/charts/fastchat/templates/deployment.yaml create mode 100644 charts/arcadia/charts/fastchat/templates/ingress.yaml create mode 100644 charts/arcadia/charts/fastchat/templates/service.yaml create mode 100644 charts/arcadia/charts/fastchat/values.yaml delete mode 100644 llms/Dockerfile create mode 100644 llms/Dockerfile.server diff --git a/charts/arcadia/Chart.yaml b/charts/arcadia/Chart.yaml index a8a902b00..654f92ede 100644 --- a/charts/arcadia/Chart.yaml +++ b/charts/arcadia/Chart.yaml @@ -19,3 +19,5 @@ maintainers: dependencies: - name: minio version: 5.0.10 + - name: fastchat + version: 0.1.0 diff --git a/charts/arcadia/charts/fastchat/.helmignore b/charts/arcadia/charts/fastchat/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/charts/arcadia/charts/fastchat/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/arcadia/charts/fastchat/Chart.yaml b/charts/arcadia/charts/fastchat/Chart.yaml new file mode 100644 index 000000000..c093eb171 --- /dev/null +++ b/charts/arcadia/charts/fastchat/Chart.yaml @@ -0,0 +1,33 @@ +apiVersion: v2 +name: fastchat +description: A Helm chart for fastchat server + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.0.1 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.2.32" + +sources: + - https://github.com/kubeagi/arcadia +keywords: + - kubeagi + - LLMOps +maintainers: + - name: lanture1064 + url: https://github.com/lanture1064 \ No newline at end of file diff --git a/charts/arcadia/charts/fastchat/templates/deployment.yaml b/charts/arcadia/charts/fastchat/templates/deployment.yaml new file mode 100644 index 000000000..8a2f1af71 --- /dev/null +++ b/charts/arcadia/charts/fastchat/templates/deployment.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-fastchat + namespace: {{ .Release.Namespace }} + labels: + control-plane: {{ .Release.Name }}-fastchat +spec: + selector: + matchLabels: + control-plane: {{ .Release.Name }}-fastchat + replicas: 1 + template: + metadata: + labels: + control-plane: {{ .Release.Name }}-fastchat + spec: + containers: + - name: {{ .Values.container.name.controller }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["/bin/bash","-c","python3 -m fastchat.serve.controller --host 0.0.0.0 --port 21001"] + ports: + - name: controller + containerPort: {{ .Values.service.controller.port }} + protocol: TCP + - name: {{ .Values.container.name.apiServer }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["/bin/bash","-c","python3 -m fastchat.serve.openai_api_server --controller-address http://localhost:21001 --host 0.0.0.0 --port 8000"] + ports: + - name: api + containerPort: {{ .Values.service.apiServer.port }} + protocol: TCP diff --git a/charts/arcadia/charts/fastchat/templates/ingress.yaml b/charts/arcadia/charts/fastchat/templates/ingress.yaml new file mode 100644 index 000000000..6b0431311 --- /dev/null +++ b/charts/arcadia/charts/fastchat/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := .Release.Name -}} +{{- $svcPort := .Values.service.apiServer.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }}-fastchat + labels: + control-plane: {{ .Release.Name }}-fastchat + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ .port }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ .port }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/arcadia/charts/fastchat/templates/service.yaml b/charts/arcadia/charts/fastchat/templates/service.yaml new file mode 100644 index 000000000..c9361985a --- /dev/null +++ b/charts/arcadia/charts/fastchat/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + labels: + control-plane: {{ .Release.Name }}-fastchat +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.apiServer.port }} + targetPort: {{ .Values.service.apiServer.port }} + protocol: TCP + name: api + - port: {{ .Values.service.controller.port }} + targetPort: {{ .Values.service.controller.port }} + protocol: TCP + name: controller + selector: + control-plane: {{ .Release.Name }}-fastchat \ No newline at end of file diff --git a/charts/arcadia/charts/fastchat/values.yaml b/charts/arcadia/charts/fastchat/values.yaml new file mode 100644 index 000000000..33e3cbc02 --- /dev/null +++ b/charts/arcadia/charts/fastchat/values.yaml @@ -0,0 +1,58 @@ +# Default values for fastchat. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +image: + repository: kubebb/arcadia-fastchat + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "v1" + +container: + name: + controller: fastchat-controller + apiServer: fastchat-api-server + +service: + type: ClusterIP + controller: + port: 21001 + apiServer: + port: 8000 + +volumes: + # Replace with real path if you want to use local huggingface models + huggingface: + +ingress: + enabled: false + className: portal-ingress + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: api.fastchat.arcadia.com + paths: + - path: / + port: 8000 + pathType: ImplementationSpecific + - host: controller.fastchat.arcadia.com + paths: + - path: / + port: 21001 + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi \ No newline at end of file diff --git a/llms/Dockerfile b/llms/Dockerfile deleted file mode 100644 index fa623c2f1..000000000 --- a/llms/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# FROM python:3.12.0b4-alpine3.18 -# FROM ubuntu:22.04 -FROM nvidia/cuda:11.7.1-runtime-ubuntu22.04 - -RUN sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list -RUN sed -i 's/security.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list - -RUN apt update -y && apt upgrade -y && apt install -y python3 curl -RUN apt install python3-pip -y -# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories -# RUN apk add curl py3-pip py3-setuptools -# RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py -# RUN python3 get-pip.py -RUN pip3 install fschat -i https://pypi.mirrors.ustc.edu.cn/simple/ - diff --git a/llms/Dockerfile.server b/llms/Dockerfile.server new file mode 100644 index 000000000..7f29f6168 --- /dev/null +++ b/llms/Dockerfile.server @@ -0,0 +1,15 @@ +FROM python:3.9-slim + +ENV TZ=Asia/Shanghai + +RUN sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list +RUN sed -i 's/security.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list + +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update \ + && apt-get install -y tzdata \ + && ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && dpkg-reconfigure --frontend noninteractive tzdata + +RUN apt-get update -y && apt-get install -y python3.9-distutils curl python3-pip +RUN pip3 install fschat -i https://pypi.mirrors.ustc.edu.cn/simple/ \ No newline at end of file diff --git a/llms/docker-compose.yml b/llms/docker-compose.yml index 57b5d3198..11c25996b 100644 --- a/llms/docker-compose.yml +++ b/llms/docker-compose.yml @@ -4,7 +4,7 @@ services: fastchat-controller: build: context: . - dockerfile: Dockerfile + dockerfile: Dockerfile.server image: kubebb/arcadia-llms:latest ports: - "21001:21001" @@ -12,7 +12,7 @@ services: fastchat-api-server: build: context: . - dockerfile: Dockerfile + dockerfile: Dockerfile.server image: kubebb/arcadia-llms:latest ports: - "8000:8000" From d064751fcdd9f8817be7b0adb9d8600730080e17 Mon Sep 17 00:00:00 2001 From: Abirdcfly Date: Mon, 30 Oct 2023 13:57:29 +0800 Subject: [PATCH 10/12] feat: init default controller config Signed-off-by: Abirdcfly --- charts/arcadia/Chart.lock | 7 +- charts/arcadia/Chart.yaml | 4 +- .../arcadia.kubeagi.k8s.com.cn_datasets.yaml | 107 ++++++++ ....kubeagi.k8s.com.cn_versioneddatasets.yaml | 234 ++++++++++++++++++ charts/arcadia/templates/config.yaml | 14 ++ charts/arcadia/templates/deployment.yaml | 13 + charts/arcadia/templates/rbac.yaml | 61 ++++- config/rbac/role.yaml | 8 + controllers/datasource_controller.go | 27 +- go.mod | 2 +- graphql-server/go-server/graph/resolver.go | 2 +- main.go | 7 - pkg/config/config.go | 110 ++++++++ pkg/config/config_type.go | 27 ++ pkg/datasource/datasource.go | 19 +- 15 files changed, 617 insertions(+), 25 deletions(-) create mode 100644 charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_datasets.yaml create mode 100644 charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml create mode 100644 charts/arcadia/templates/config.yaml create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_type.go diff --git a/charts/arcadia/Chart.lock b/charts/arcadia/Chart.lock index b2d84c11c..9be5c3769 100644 --- a/charts/arcadia/Chart.lock +++ b/charts/arcadia/Chart.lock @@ -2,5 +2,8 @@ dependencies: - name: minio repository: "" version: 5.0.10 -digest: sha256:789ce227756398b7c76fd611551cc373f5fb87a1bfab09563539786d01db0cb3 -generated: "2023-10-29T01:33:42.169253+08:00" +- name: fastchat + repository: "" + version: 0.0.1 +digest: sha256:f90e95bc7facd1376eb110f2cce9735533dedf08316f83a25a4a6a4d38e6d024 +generated: "2023-11-02T09:07:41.041119+08:00" diff --git a/charts/arcadia/Chart.yaml b/charts/arcadia/Chart.yaml index 654f92ede..0f6876676 100644 --- a/charts/arcadia/Chart.yaml +++ b/charts/arcadia/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: arcadia description: A Helm chart(KubeBB Component) for KubeAGI Arcadia type: application -version: 0.1.11 +version: 0.1.12 appVersion: "0.0.1" keywords: @@ -20,4 +20,4 @@ dependencies: - name: minio version: 5.0.10 - name: fastchat - version: 0.1.0 + version: 0.0.1 diff --git a/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_datasets.yaml b/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_datasets.yaml new file mode 100644 index 000000000..912e593aa --- /dev/null +++ b/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_datasets.yaml @@ -0,0 +1,107 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: datasets.arcadia.kubeagi.k8s.com.cn +spec: + group: arcadia.kubeagi.k8s.com.cn + names: + kind: Dataset + listKind: DatasetList + plural: datasets + singular: dataset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.displayName + name: display-name + type: string + - jsonPath: .spec.contentType + name: type + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Dataset is the Schema for the datasets API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: DatasetSpec defines the desired state of Dataset + properties: + bestCase: + description: bestCase defines the best case to use this dataset + type: string + contentType: + description: ContentType defines dataset + type: string + creator: + description: Creator defines dataset creator(AUTO-FILLED by webhook) + type: string + displayName: + description: DisplayName defines dataset display name + type: string + required: + - contentType + - displayName + type: object + status: + description: DatasetStatus defines the observed state of Dataset + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastSuccessfulTime: + description: LastSuccessfulTime is repository Last Successful + Update Time + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown + type: string + type: + description: Type of this condition. At most one of each condition + type may apply to a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml b/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml new file mode 100644 index 000000000..b2c83eb0d --- /dev/null +++ b/charts/arcadia/crds/arcadia.kubeagi.k8s.com.cn_versioneddatasets.yaml @@ -0,0 +1,234 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: versioneddatasets.arcadia.kubeagi.k8s.com.cn +spec: + group: arcadia.kubeagi.k8s.com.cn + names: + kind: VersionedDataset + listKind: VersionedDatasetList + plural: versioneddatasets + singular: versioneddataset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.dataset.name + name: dataset + type: string + - jsonPath: .spec.version + name: version + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: VersionedDataset is the Schema for the versioneddatasets API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: VersionedDatasetSpec defines the desired state of VersionedDataset + properties: + dataset: + description: Dataset which this `VersionedDataset` belongs to + properties: + apiGroup: + description: APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in + the core API group. For any other third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: Namespace is the namespace of resource being referenced + type: string + required: + - kind + - name + type: object + files: + description: Files included in this `VersionedDataset` + items: + properties: + from: + description: From defines the datasource which provides this + `File` + properties: + apiGroup: + description: APIGroup is the group for the resource being + referenced. If APIGroup is not specified, the specified + Kind must be in the core API group. For any other third-party + types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: Namespace is the namespace of resource being + referenced + type: string + required: + - kind + - name + type: object + path: + description: Path defines the detail path to get this `File` + type: string + required: + - from + - path + type: object + type: array + version: + description: Version + type: string + required: + - dataset + - version + type: object + status: + description: VersionedDatasetStatus defines the observed state of VersionedDataset + properties: + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastSuccessfulTime: + description: LastSuccessfulTime is repository Last Successful + Update Time + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown + type: string + type: + description: Type of this condition. At most one of each condition + type may apply to a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + filesStatus: + description: FilesStatus contains the status to all files in VersionedDatasetSpec + items: + properties: + processCondition: + description: ProcessCondition records the status of data processing + properties: + lastSuccessfulTime: + description: LastSuccessfulTime is repository Last Successful + Update Time + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition + from one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown + type: string + type: + description: Type of this condition. At most one of each + condition type may apply to a resource at any point in + time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + uploadCondition: + description: UploadCondition records the status of file upload + properties: + lastSuccessfulTime: + description: LastSuccessfulTime is repository Last Successful + Update Time + format: date-time + type: string + lastTransitionTime: + description: LastTransitionTime is the last time this condition + transitioned from one status to another. + format: date-time + type: string + message: + description: A Message containing details about this condition's + last transition from one status to another, if any. + type: string + reason: + description: A Reason for this condition's last transition + from one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown + type: string + type: + description: Type of this condition. At most one of each + condition type may apply to a resource at any point in + time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/arcadia/templates/config.yaml b/charts/arcadia/templates/config.yaml new file mode 100644 index 000000000..c9685f817 --- /dev/null +++ b/charts/arcadia/templates/config.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +data: + config: | + systemDatasource: + apiGroup: arcadia.kubeagi.k8s.com.cn/v1alpha1 + kind: Datasource + name: '{{ .Release.Name }}-minio' + namespace: '{{ .Release.Namespace }}' +kind: ConfigMap +metadata: + labels: + control-plane: {{ .Release.Name }}-arcadia + name: {{ .Release.Name }}-config + namespace: {{ .Release.Namespace }} diff --git a/charts/arcadia/templates/deployment.yaml b/charts/arcadia/templates/deployment.yaml index cdfc12ce4..29a8f755c 100644 --- a/charts/arcadia/templates/deployment.yaml +++ b/charts/arcadia/templates/deployment.yaml @@ -27,6 +27,19 @@ spec: containers: - command: - /manager + env: + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: DEFAULT_CONFIG + value: {{ .Release.Name }}-config image: {{ .Values.deployment.image }} imagePullPolicy: {{ .Values.deployment.imagePullPolicy }} name: manager diff --git a/charts/arcadia/templates/rbac.yaml b/charts/arcadia/templates/rbac.yaml index 25e76545f..8cf3b69a3 100644 --- a/charts/arcadia/templates/rbac.yaml +++ b/charts/arcadia/templates/rbac.yaml @@ -22,6 +22,14 @@ kind: ClusterRole metadata: name: {{ .Release.Name }} rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch - apiGroups: - "" resources: @@ -30,6 +38,32 @@ rules: - get - list - watch +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - datasets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - datasets/finalizers + verbs: + - update +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - datasets/status + verbs: + - get + - patch + - update - apiGroups: - arcadia.kubeagi.k8s.com.cn resources: @@ -160,4 +194,29 @@ rules: - get - patch - update - +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - versioneddatasets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - versioneddatasets/finalizers + verbs: + - update +- apiGroups: + - arcadia.kubeagi.k8s.com.cn + resources: + - versioneddatasets/status + verbs: + - get + - patch + - update diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index dd42ed899..16536a455 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -5,6 +5,14 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch - apiGroups: - "" resources: diff --git a/controllers/datasource_controller.go b/controllers/datasource_controller.go index 50b1c61f0..e98908dcd 100644 --- a/controllers/datasource_controller.go +++ b/controllers/datasource_controller.go @@ -35,6 +35,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" arcadiav1alpha1 "github.com/kubeagi/arcadia/api/v1alpha1" + "github.com/kubeagi/arcadia/pkg/config" "github.com/kubeagi/arcadia/pkg/datasource" "github.com/kubeagi/arcadia/pkg/utils" ) @@ -49,6 +50,7 @@ type DatasourceReconciler struct { //+kubebuilder:rbac:groups=arcadia.kubeagi.k8s.com.cn,resources=datasources/status,verbs=get;update;patch //+kubebuilder:rbac:groups=arcadia.kubeagi.k8s.com.cn,resources=datasources/finalizers,verbs=update //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch +//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -152,17 +154,26 @@ func (r *DatasourceReconciler) Checkdatasource(ctx context.Context, logger logr. var info any switch instance.Spec.Type() { case arcadiav1alpha1.DatasourceTypeLocal: - // FIXME: implement local datasource check when system datasource defined by https://github.com/kubeagi/arcadia/issues/156 - // 1. read system datasource endpoint - // 2. check against pre-denfined rules for local datasource rules - return r.UpdateStatus(ctx, instance, nil) + system, err := config.GetSystemDatasource(ctx, r.Client) + if err != nil { + return r.UpdateStatus(ctx, instance, err) + } + endpoint := system.Spec.Enpoint.DeepCopy() + if endpoint != nil && endpoint.AuthSecret != nil { + endpoint.AuthSecret.WithNameSpace(system.Namespace) + } + ds, err = datasource.NewLocal(ctx, r.Client, endpoint) + if err != nil { + return r.UpdateStatus(ctx, instance, err) + } + info = &arcadiav1alpha1.OSS{Bucket: instance.Namespace} case arcadiav1alpha1.DatasourceTypeOSS: - endpoiont := instance.Spec.Enpoint.DeepCopy() + endpoint := instance.Spec.Enpoint.DeepCopy() // set auth secret's namespace to the datasource's namespace - if endpoiont.AuthSecret != nil { - endpoiont.AuthSecret.WithNameSpace(instance.Namespace) + if endpoint.AuthSecret != nil { + endpoint.AuthSecret.WithNameSpace(instance.Namespace) } - ds, err = datasource.NewOSS(ctx, r.Client, endpoiont) + ds, err = datasource.NewOSS(ctx, r.Client, endpoint) if err != nil { return r.UpdateStatus(ctx, instance, err) } diff --git a/go.mod b/go.mod index e719b676d..5c179fb59 100644 --- a/go.mod +++ b/go.mod @@ -130,7 +130,7 @@ require ( k8s.io/apiextensions-apiserver v0.24.2 // indirect k8s.io/component-base v0.24.2 // indirect k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect - k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect + k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/graphql-server/go-server/graph/resolver.go b/graphql-server/go-server/graph/resolver.go index 9f9867920..06d0ca1f2 100644 --- a/graphql-server/go-server/graph/resolver.go +++ b/graphql-server/go-server/graph/resolver.go @@ -4,5 +4,5 @@ package graph // // It serves as dependency injection for your app, add any dependencies you require here. -type Resolver struct{ +type Resolver struct { } diff --git a/main.go b/main.go index bb152878e..5655a6bfa 100644 --- a/main.go +++ b/main.go @@ -198,10 +198,3 @@ func main() { os.Exit(1) } } - -// TODO: Defines the configuration for the Arcadia controller and implement the config parser -// Config defines the configuration for the Arcadia controller -type Config struct { - // SystemDatasource specifies the built-in datasource for Arcadia to host data files and model files - SystemDatasource arcadiav1alpha1.TypedObjectReference -} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 000000000..9da09a7fd --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,110 @@ +/* +Copyright 2023 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "context" + "fmt" + "os" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/utils/env" + "sigs.k8s.io/controller-runtime/pkg/client" + + arcadiav1alpha1 "github.com/kubeagi/arcadia/api/v1alpha1" +) + +const ( + EnvConfigKey = "DEFAULT_CONFIG" + EnvConfigDefaultValue = "arcadia-config" + EnvNamespaceKey = "POD_NAMESPACE" + + InClusterNamespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" +) + +var ( + ErrNoConfigEnv = fmt.Errorf("env:%s is not found", EnvConfigKey) + ErrNoConfig = fmt.Errorf("config in configmap is empty") + ErrNoNamespaceEnv = fmt.Errorf("not in cluster and env:%s is not found", EnvNamespaceKey) +) + +func GetSystemDatasource(ctx context.Context, c client.Client) (*arcadiav1alpha1.Datasource, error) { + config, err := GetConfig(ctx, c) + if err != nil { + return nil, err + } + name := config.SystemDatasource.Name + var namespace string + if config.SystemDatasource.Namespace != nil { + namespace = *config.SystemDatasource.Namespace + } else { + namespace, err = GetSelfNamespace() + if err != nil { + return nil, err + } + } + source := &arcadiav1alpha1.Datasource{} + if err = c.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, source); err != nil { + return nil, err + } + return source, err +} + +func GetConfig(ctx context.Context, c client.Client) (config *Config, err error) { + cmName := env.GetString(EnvConfigKey, EnvConfigDefaultValue) + if cmName == "" { + return nil, ErrNoConfigEnv + } + cmNamespace, err := GetSelfNamespace() + if err != nil { + return nil, err + } + cm := &corev1.ConfigMap{} + if err = c.Get(ctx, client.ObjectKey{Name: cmName, Namespace: cmNamespace}, cm); err != nil { + return nil, err + } + value, ok := cm.Data["config"] + if !ok || len(value) == 0 { + return nil, ErrNoConfig + } + if err = yaml.Unmarshal([]byte(value), &config); err != nil { + return nil, err + } + return config, nil +} + +func GetSelfNamespace() (string, error) { + // Check whether the namespace file exists. + // If not, we are not running in cluster so can't guess the namespace. + if _, err := os.Stat(InClusterNamespacePath); os.IsNotExist(err) { + operatorNamespace := os.Getenv(EnvNamespaceKey) + if operatorNamespace == "" { + return "", ErrNoNamespaceEnv + } + return operatorNamespace, nil + } else if err != nil { + return "", fmt.Errorf("error checking namespace file: %w", err) + } + + // Load the namespace file and return its content + namespace, err := os.ReadFile(InClusterNamespacePath) + if err != nil { + return "", fmt.Errorf("error reading namespace file: %w", err) + } + return string(namespace), nil +} diff --git a/pkg/config/config_type.go b/pkg/config/config_type.go new file mode 100644 index 000000000..bc62710c9 --- /dev/null +++ b/pkg/config/config_type.go @@ -0,0 +1,27 @@ +/* +Copyright 2023 KubeAGI. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + arcadiav1alpha1 "github.com/kubeagi/arcadia/api/v1alpha1" +) + +// Config defines the configuration for the Arcadia controller +type Config struct { + // SystemDatasource specifies the built-in datasource for Arcadia to host data files and model files + SystemDatasource arcadiav1alpha1.TypedObjectReference `json:"systemDatasource,omitempty"` +} diff --git a/pkg/datasource/datasource.go b/pkg/datasource/datasource.go index d1097b857..b61f8002c 100644 --- a/pkg/datasource/datasource.go +++ b/pkg/datasource/datasource.go @@ -31,6 +31,7 @@ import ( var ( ErrUnknowDatasourceType = errors.New("unknow datasource type") + ErrOSSNoSuchBucket = errors.New("NoSuchBucket") ) type Datasource interface { @@ -67,8 +68,17 @@ func NewLocal(ctx context.Context, c client.Client, endpoint *v1alpha1.Endpoint) } // Check `Local` with `OSS` -func (local *Local) Check(ctx context.Context, options any) error { - return local.oss.Check(ctx, options) +func (local *Local) Check(ctx context.Context, options any) (err error) { + err = local.oss.Check(ctx, options) + if err != nil && errors.Is(err, ErrOSSNoSuchBucket) { + ossInfo, ok := options.(*v1alpha1.OSS) + if !ok { + return errors.New("invalid check info for OSS") + } + defautlMakeBucketOptions := minio.MakeBucketOptions{} + err = local.oss.MakeBucket(ctx, ossInfo.Bucket, defautlMakeBucketOptions) + } + return err } var _ Datasource = (*OSS)(nil) @@ -118,10 +128,13 @@ func (oss *OSS) Check(ctx context.Context, info any) error { } if ossInfo.Bucket != "" { - _, err := oss.Client.BucketExists(ctx, ossInfo.Bucket) + exist, err := oss.Client.BucketExists(ctx, ossInfo.Bucket) if err != nil { return err } + if !exist { + return ErrOSSNoSuchBucket + } if ossInfo.Object != "" { _, err := oss.Client.StatObject(ctx, ossInfo.Bucket, ossInfo.Object, minio.StatObjectOptions{}) From 0d7c818645b7d279bcbdb946749892ed4c4af2de Mon Sep 17 00:00:00 2001 From: bjwswang Date: Thu, 2 Nov 2023 06:02:39 +0000 Subject: [PATCH 11/12] chore: change default image and service name for fastchat Signed-off-by: bjwswang --- charts/arcadia/Chart.lock | 6 +++--- charts/arcadia/Chart.yaml | 4 ++-- charts/arcadia/charts/fastchat/Chart.yaml | 2 +- charts/arcadia/charts/fastchat/templates/service.yaml | 2 +- charts/arcadia/charts/fastchat/values.yaml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/charts/arcadia/Chart.lock b/charts/arcadia/Chart.lock index 9be5c3769..9d2fb1df0 100644 --- a/charts/arcadia/Chart.lock +++ b/charts/arcadia/Chart.lock @@ -4,6 +4,6 @@ dependencies: version: 5.0.10 - name: fastchat repository: "" - version: 0.0.1 -digest: sha256:f90e95bc7facd1376eb110f2cce9735533dedf08316f83a25a4a6a4d38e6d024 -generated: "2023-11-02T09:07:41.041119+08:00" + version: 0.0.2 +digest: sha256:54d12a585a3decd50f82eaffae0224f188f7d715565bd751ffda3435678c2619 +generated: "2023-11-02T06:00:49.531248052Z" diff --git a/charts/arcadia/Chart.yaml b/charts/arcadia/Chart.yaml index 0f6876676..f20a737f4 100644 --- a/charts/arcadia/Chart.yaml +++ b/charts/arcadia/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: arcadia description: A Helm chart(KubeBB Component) for KubeAGI Arcadia type: application -version: 0.1.12 +version: 0.1.13 appVersion: "0.0.1" keywords: @@ -20,4 +20,4 @@ dependencies: - name: minio version: 5.0.10 - name: fastchat - version: 0.0.1 + version: 0.0.2 diff --git a/charts/arcadia/charts/fastchat/Chart.yaml b/charts/arcadia/charts/fastchat/Chart.yaml index c093eb171..c3d2bff8e 100644 --- a/charts/arcadia/charts/fastchat/Chart.yaml +++ b/charts/arcadia/charts/fastchat/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.0.1 +version: 0.0.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/arcadia/charts/fastchat/templates/service.yaml b/charts/arcadia/charts/fastchat/templates/service.yaml index c9361985a..5c18a37b9 100644 --- a/charts/arcadia/charts/fastchat/templates/service.yaml +++ b/charts/arcadia/charts/fastchat/templates/service.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Service metadata: - name: {{ .Release.Name }} + name: {{ .Release.Name }}-fastchat labels: control-plane: {{ .Release.Name }}-fastchat spec: diff --git a/charts/arcadia/charts/fastchat/values.yaml b/charts/arcadia/charts/fastchat/values.yaml index 33e3cbc02..551f67e0b 100644 --- a/charts/arcadia/charts/fastchat/values.yaml +++ b/charts/arcadia/charts/fastchat/values.yaml @@ -5,7 +5,7 @@ image: repository: kubebb/arcadia-fastchat pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "v1" + tag: "v0.0.1" container: name: From 8de1e45e431bebb86b4ec3b1679f411d62267329 Mon Sep 17 00:00:00 2001 From: bjwswang Date: Thu, 2 Nov 2023 07:14:21 +0000 Subject: [PATCH 12/12] chore: expose ingress settings for fastchat in arcadia Signed-off-by: bjwswang --- charts/arcadia/Chart.yaml | 4 +++- charts/arcadia/values.yaml | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/charts/arcadia/Chart.yaml b/charts/arcadia/Chart.yaml index f20a737f4..2f37000d7 100644 --- a/charts/arcadia/Chart.yaml +++ b/charts/arcadia/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: arcadia description: A Helm chart(KubeBB Component) for KubeAGI Arcadia type: application -version: 0.1.13 +version: 0.1.14 appVersion: "0.0.1" keywords: @@ -15,6 +15,8 @@ sources: maintainers: - name: bjwswang url: https://github.com/bjwswang + - name: lanture1064 + url: https://github.com/lanture1064 dependencies: - name: minio diff --git a/charts/arcadia/values.yaml b/charts/arcadia/values.yaml index faad4722e..0f61243b4 100644 --- a/charts/arcadia/values.yaml +++ b/charts/arcadia/values.yaml @@ -16,6 +16,24 @@ deployment: cpu: 10m memory: 64Mi +# @section fastchat is used as fastchat configurations for arcadia +fastchat: + ingress: + enabled: false + className: portal-ingress + hosts: + - host: fastchat-api.fastchat.arcadia.com + paths: + - path: / + port: 8000 + pathType: ImplementationSpecific + - host: fastchat-controller.fastchat.arcadia.com + paths: + - path: / + port: 21001 + pathType: ImplementationSpecific + + # @section oss is used as default Object-Storage-Service for arcadia which provides the capability to # - host user-uploaded data files as local datasource # - host user-uploaded models