Remove kube-aggregator from OSS (#103659)
* feat: remove kube-aggregator for OSS and provide injection points with runner iface * upgrade authlib to support expiresIn * new FT * new FT again * update go.mod * get rid of the slice implementation * reconcile conflicts * gracefully handle enterprise not being linked situation with kubeAggregator FT true * allow dataplane agg and kube agg to both be added to delegate chain * make update-workspace * address feedback * revert go.mod changes * go.mod updates * elaborate on why and how of skipping the Ready channel handling * after rebase and make run
This commit is contained in:
@@ -79,7 +79,7 @@ require (
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20250508220812-83b6de6b0a35 // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a // @grafana/identity-access-team
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5 // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics
|
||||
@@ -193,7 +193,7 @@ require (
|
||||
gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group
|
||||
gopkg.in/yaml.v2 v2.4.0 // @grafana/alerting-backend
|
||||
gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend
|
||||
k8s.io/api v0.32.3 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/api v0.32.3 // indirect; @grafana/grafana-app-platform-squad
|
||||
k8s.io/apimachinery v0.32.3 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/apiserver v0.32.3 // @grafana/grafana-app-platform-squad
|
||||
k8s.io/client-go v0.32.3 // @grafana/grafana-app-platform-squad
|
||||
@@ -452,12 +452,10 @@ require (
|
||||
github.com/mithrandie/go-file/v2 v2.1.0 // indirect
|
||||
github.com/mithrandie/go-text v1.6.0 // indirect
|
||||
github.com/mithrandie/ternary v1.1.1 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/natefinch/wrap v0.2.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/oapi-codegen/runtime v1.0.0 // indirect
|
||||
|
||||
@@ -1571,8 +1571,8 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/alerting v0.0.0-20250508220812-83b6de6b0a35 h1:/FvrKg5ZtJ09oWOt91lIPpeWtWGcNqXv/9UOQaRDyQE=
|
||||
github.com/grafana/alerting v0.0.0-20250508220812-83b6de6b0a35/go.mod h1:pMfhRxL2LZ3Pm8iy7VcVsb9CLYuBtjFYbf1oxgx7yFA=
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a h1:irEH0Abl6mKbkPx/xtmB5Wai4ipzEB6hGPNsLya/p9Y=
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5 h1:qu6+b9tY6E/CgKrLh8srjtvKOMCxdwk46jRqURjJ61s=
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA=
|
||||
@@ -2029,8 +2029,6 @@ github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QR
|
||||
github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/mocktools/go-smtp-mock/v2 v2.3.1 h1:wq75NDSsOy5oHo/gEQQT0fRRaYKRqr1IdkjhIPXxagM=
|
||||
github.com/mocktools/go-smtp-mock/v2 v2.3.1/go.mod h1:h9AOf/IXLSU2m/1u4zsjtOM/WddPwdOUBz56dV9f81M=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -2053,7 +2051,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=
|
||||
github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk=
|
||||
|
||||
+3
-14
@@ -1123,8 +1123,6 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/alerting v0.0.0-20250129195454-3e5b80036b7a/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
|
||||
github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e/go.mod h1:HfvjmU3UqCIpoy9Z2wgKGrZ4A5vz+yQlP9ZXvCfEkiA=
|
||||
github.com/grafana/alerting v0.0.0-20250403153742-418bc7118d05 h1:hMzOzI/S0nkZt0nUqpfAa4Rdb+YL8z8oG3pl4Jb31h8=
|
||||
@@ -1378,6 +1376,8 @@ github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f h1:2+myh5ml7lgEU/5
|
||||
github.com/mithrandie/readline-csvq v1.3.0 h1:VTJEOGouJ8j27jJCD4kBBbNTxM0OdBvE1aY1tMhlqE8=
|
||||
github.com/mithrandie/readline-csvq v1.3.0/go.mod h1:FKyYqDgf/G4SNov7SMFXRWO6LQLXIOeTog/NB97FZl0=
|
||||
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI=
|
||||
@@ -1397,6 +1397,7 @@ github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:
|
||||
github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ=
|
||||
github.com/natessilva/dag v0.0.0-20180124060714-7194b8dcc5c4 h1:dnMxwus89s86tI8rcGVp2HwZzlz7c5o92VOy7dSckBQ=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
@@ -1923,8 +1924,6 @@ golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ug
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
@@ -1969,8 +1968,6 @@ golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
|
||||
@@ -2004,22 +2001,16 @@ golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
|
||||
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
@@ -2043,8 +2034,6 @@ golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
|
||||
|
||||
@@ -520,6 +520,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
kubernetesAggregator?: boolean;
|
||||
/**
|
||||
* Enable CAP token based authentication in grafana's embedded kube-aggregator
|
||||
*/
|
||||
kubernetesAggregatorCapTokenAuth?: boolean;
|
||||
/**
|
||||
* Enable new expression parser
|
||||
*/
|
||||
expressionParser?: boolean;
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a // @grafana/identity-access-team
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5 // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team
|
||||
github.com/stretchr/testify v1.10.0
|
||||
k8s.io/apimachinery v0.32.3
|
||||
|
||||
@@ -32,8 +32,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a h1:irEH0Abl6mKbkPx/xtmB5Wai4ipzEB6hGPNsLya/p9Y=
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5 h1:qu6+b9tY6E/CgKrLh8srjtvKOMCxdwk46jRqURjJ61s=
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
|
||||
@@ -46,7 +46,7 @@ require (
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
|
||||
@@ -81,8 +81,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a h1:irEH0Abl6mKbkPx/xtmB5Wai4ipzEB6hGPNsLya/p9Y=
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5 h1:qu6+b9tY6E/CgKrLh8srjtvKOMCxdwk46jRqURjJ61s=
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.35.1 h1:taVpl+RoixTYl0JBJGhH+fPVmwA9wvdwdzJTZsv9buM=
|
||||
|
||||
@@ -32,9 +32,18 @@ import (
|
||||
_ "golang.org/x/time/rate"
|
||||
_ "xorm.io/builder"
|
||||
|
||||
_ "github.com/grafana/authlib/authn"
|
||||
_ "github.com/grafana/authlib/authz"
|
||||
_ "github.com/grafana/authlib/cache"
|
||||
_ "github.com/grafana/authlib/grpcutils"
|
||||
_ "github.com/grafana/authlib/types"
|
||||
_ "github.com/grafana/dskit/backoff"
|
||||
_ "github.com/grafana/dskit/flagext"
|
||||
_ "github.com/grafana/e2e"
|
||||
_ "github.com/grafana/gofpdf"
|
||||
_ "github.com/grafana/gomemcache/memcache"
|
||||
|
||||
_ "k8s.io/kube-aggregator/pkg/apiserver/scheme"
|
||||
_ "k8s.io/kube-aggregator/pkg/generated/openapi"
|
||||
_ "k8s.io/kube-aggregator/pkg/registry/apiservice/rest"
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/anonymous"
|
||||
"github.com/grafana/grafana/pkg/services/anonymous/anonimpl"
|
||||
"github.com/grafana/grafana/pkg/services/anonymous/validator"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/aggregatorrunner"
|
||||
builder "github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/standalone"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
@@ -121,6 +122,7 @@ var wireExtsBasicSet = wire.NewSet(
|
||||
wire.Struct(new(unified.Options), "*"),
|
||||
unified.ProvideUnifiedStorageClient,
|
||||
builder.ProvideDefaultBuildHandlerChainFuncFromBuilders,
|
||||
aggregatorrunner.ProvideNoopAggregatorConfigurator,
|
||||
)
|
||||
|
||||
var wireExtsSet = wire.NewSet(
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
# aggregator
|
||||
|
||||
This is a package that is intended to power the aggregation of microservices within Grafana. The concept
|
||||
as well as implementation is largely borrowed from [kube-aggregator](https://github.com/kubernetes/kube-aggregator).
|
||||
|
||||
## Why aggregate services?
|
||||
|
||||
Grafana's future architecture will entail the same API Server design as that of Kubernetes API Servers. API Servers
|
||||
provide a standard way of stitching together API Groups through discovery and shared routing patterns that allows
|
||||
them to aggregate to a parent API Server in a seamless manner. Since we desire to break Grafana monolith up into
|
||||
more functionally divided microservices, aggregation does the job of still being able to provide these services
|
||||
under a single address. Other benefits of aggregation include free health checks and being able to independently
|
||||
roll out features for each service without downtime.
|
||||
|
||||
To read more about the concept, see
|
||||
[here](https://kubernetes.io/docs/tasks/extend-kubernetes/setup-extension-api-server/).
|
||||
|
||||
Note that this aggregation will be a totally internal detail to Grafana. External fully functional API Servers that
|
||||
may themselves act as parent API Servers to Grafana will never be made aware of internal Grafana API Servers.
|
||||
Thus, any `APIService` objects corresponding to Grafana's API groups will take the address of
|
||||
Grafana's main API Server (the one that bundles grafana-aggregator).
|
||||
|
||||
Also, note that the single binary OSS offering of Grafana doesn't make use of the aggregator component at all, instead
|
||||
opting for local installation of all the Grafana API groups.
|
||||
|
||||
### kube-aggregator versus grafana-aggregator
|
||||
|
||||
The `grafana-aggregator` component will work similarly to how `kube-aggregator` works for `kube-apiserver`, the major
|
||||
difference being that it doesn't require core V1 APIs such as `Service`. Early on, we decided to not have core V1
|
||||
APIs in the root Grafana API Server. In order to still be able to implement aggregation, we do the following in this Go
|
||||
package:
|
||||
|
||||
1. We do not start the core shared informer factories as well as any default controllers that utilize them.
|
||||
This is achieved using `DisabledPostStartHooks` facility under the GenericAPIServer's RecommendedConfig.
|
||||
2. We provide an `externalname` Kind API implementation under `service.grafana.app` group which works functionally
|
||||
equivalent to the idea with the same name under `core/v1/Service`.
|
||||
3. Lastly, we swap the default available condition controller with the custom one written by us. This one is based on
|
||||
our `externalname` (`service.grafana.app`) implementation. We register separate `PostStartHooks`
|
||||
using `AddPostStartHookOrDie` on the GenericAPIServer to start the corresponding custom controller as well as
|
||||
requisite informer factories for our own `externalname` Kind.
|
||||
4. For now, we bundle apiextensions-apiserver under our aggregator component. This is slightly different from K8s
|
||||
where kube-apiserver is called the top-level component and controlplane, aggregator and apiextensions-apiserver
|
||||
live under that instead.
|
||||
|
||||
### Gotchas (Pay Attention)
|
||||
|
||||
1. `grafana-aggregator` uses file storage under `data/grafana-apiserver` (`apiregistration.k8s.io`,
|
||||
`service.grafana.app`). Thus, any restarts will still have any prior configured aggregation in effect.
|
||||
2. During local development, ensure you start the aggregated service after launching the aggregator. This is
|
||||
so you have TLS and kubeconfig available for use with example aggregated api servers.
|
||||
3. Ensure you have `grafanaAPIServerWithExperimentalAPIs = false` in your custom.ini. Otherwise, the example
|
||||
service the following guide uses for the aggregation test is bundled as a `Local` `APIService` and will cause
|
||||
configuration overwrites on startup.
|
||||
|
||||
## Testing aggregation locally
|
||||
|
||||
1. Generate the PKI using `openssl` (for development purposes, we will use the CN of `system:masters`):
|
||||
```shell
|
||||
./hack/make-aggregator-pki.sh
|
||||
```
|
||||
2. Configure the aggregator:
|
||||
```ini
|
||||
[feature_toggles]
|
||||
grafanaAPIServerEnsureKubectlAccess = true
|
||||
; disable the experimental APIs flag to disable bundling of the example service locally
|
||||
grafanaAPIServerWithExperimentalAPIs = false
|
||||
kubernetesAggregator = true
|
||||
|
||||
[grafana-apiserver]
|
||||
proxy_client_cert_file = ./data/grafana-aggregator/client.crt
|
||||
proxy_client_key_file = ./data/grafana-aggregator/client.key
|
||||
```
|
||||
3. Start the server
|
||||
```shell
|
||||
make run
|
||||
```
|
||||
4. In another tab, apply the manifests:
|
||||
```shell
|
||||
export KUBECONFIG=$PWD/data/grafana-apiserver/grafana.kubeconfig
|
||||
kubectl apply -f ./pkg/services/apiserver/aggregator/examples/manual-test/
|
||||
# SAMPLE OUTPUT
|
||||
# apiservice.apiregistration.k8s.io/v0alpha1.example.grafana.app created
|
||||
# externalname.service.grafana.app/example-apiserver created
|
||||
|
||||
kubectl get apiservice
|
||||
# SAMPLE OUTPUT
|
||||
# NAME SERVICE AVAILABLE AGE
|
||||
# v0alpha1.example.grafana.app grafana/example-apiserver False (FailedDiscoveryCheck) 29m
|
||||
```
|
||||
5. In another tab, start the example microservice that will be aggregated by the parent apiserver:
|
||||
```shell
|
||||
go run ./pkg/cmd/grafana apiserver \
|
||||
--runtime-config=example.grafana.app/v0alpha1=true \
|
||||
--secure-port 7443 \
|
||||
--tls-cert-file $PWD/data/grafana-aggregator/server.crt \
|
||||
--tls-private-key-file $PWD/data/grafana-aggregator/server.key \
|
||||
--requestheader-client-ca-file=$PWD/data/grafana-aggregator/ca.crt \
|
||||
--requestheader-extra-headers-prefix=X-Remote-Extra- \
|
||||
--requestheader-group-headers=X-Remote-Group \
|
||||
--requestheader-username-headers=X-Remote-User \
|
||||
-v 10
|
||||
```
|
||||
6. After 10 seconds, check `APIService` again. It should report as available.
|
||||
```shell
|
||||
export KUBECONFIG=$PWD/data/grafana-apiserver/grafana.kubeconfig
|
||||
kubectl get apiservice
|
||||
# SAMPLE OUTPUT
|
||||
# NAME SERVICE AVAILABLE AGE
|
||||
# v0alpha1.example.grafana.app grafana/example-apiserver True 30m
|
||||
```
|
||||
7. For tear down of the above test:
|
||||
```shell
|
||||
kubectl delete -f ./pkg/services/apiserver/aggregator/examples/
|
||||
```
|
||||
|
||||
## Testing auto-registration of remote services locally
|
||||
|
||||
A sample aggregation config for remote services is provided under [conf](../../../../conf/aggregation/apiservices.yaml). Provided, you have the following setup in your custom.ini, the apiserver will
|
||||
register your remotely running services on startup.
|
||||
|
||||
```ini
|
||||
; in custom.ini
|
||||
; the bundle is only used when not in dev mode
|
||||
apiservice_ca_bundle_file = ./data/grafana-aggregator/ca.crt
|
||||
|
||||
remote_services_file = ./pkg/services/apiserver/aggregator/examples/autoregister/apiservices.yaml
|
||||
```
|
||||
@@ -1,509 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/aggregator.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/server.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/pkg/controlplane/apiserver/apiextensions.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
|
||||
package aggregator
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"gopkg.in/yaml.v3"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/component-base/metrics"
|
||||
"k8s.io/component-base/metrics/legacyregistry"
|
||||
"k8s.io/klog/v2"
|
||||
v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
||||
v1helper "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper"
|
||||
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
|
||||
aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
|
||||
apiregistrationclientset "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
|
||||
apiregistrationclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1"
|
||||
apiregistrationInformers "k8s.io/kube-aggregator/pkg/client/informers/externalversions/apiregistration/v1"
|
||||
"k8s.io/kube-aggregator/pkg/controllers"
|
||||
"k8s.io/kube-aggregator/pkg/controllers/autoregister"
|
||||
|
||||
servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1"
|
||||
servicev0alpha1applyconfiguration "github.com/grafana/grafana/pkg/generated/applyconfiguration/service/v0alpha1"
|
||||
serviceclientset "github.com/grafana/grafana/pkg/generated/clientset/versioned"
|
||||
informersv0alpha1 "github.com/grafana/grafana/pkg/generated/informers/externalversions"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/service"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||
)
|
||||
|
||||
// making sure we only register metrics once into legacy registry
|
||||
var registerIntoLegacyRegistryOnce sync.Once
|
||||
|
||||
//nolint:unused
|
||||
func _readCABundlePEM(path string, devMode bool) ([]byte, error) {
|
||||
if devMode {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// We can ignore the gosec G304 warning on this one because `path` comes
|
||||
// from Grafana configuration (commandOptions.AggregatorOptions.APIServiceCABundleFile)
|
||||
//nolint:gosec
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
klog.Errorf("error closing remote services file: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
func ReadRemoteServices(path string) ([]RemoteService, error) {
|
||||
// We can ignore the gosec G304 warning on this one because `path` comes
|
||||
// from Grafana configuration (commandOptions.AggregatorOptions.RemoteServicesFile)
|
||||
//nolint:gosec
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
klog.Errorf("error closing remote services file: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
rawRemoteServices, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remoteServices := make([]RemoteService, 0)
|
||||
if err := yaml.Unmarshal(rawRemoteServices, &remoteServices); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return remoteServices, nil
|
||||
}
|
||||
|
||||
func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig genericapiserver.RecommendedConfig, externalNamesNamespace string) (*Config, error) {
|
||||
// Create a fake clientset and informers for the k8s v1 API group.
|
||||
// These are not used in grafana's aggregator because v1 APIs are not available.
|
||||
fakev1Informers := informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 10*time.Minute)
|
||||
|
||||
serviceClient, err := serviceclientset.NewForConfig(sharedConfig.LoopbackClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sharedInformerFactory := informersv0alpha1.NewSharedInformerFactory(
|
||||
serviceClient,
|
||||
5*time.Minute, // this is effectively used as a refresh interval right now. Might want to do something nicer later on.
|
||||
)
|
||||
serviceResolver := NewExternalNameResolver(sharedInformerFactory.Service().V0alpha1().ExternalNames().Lister())
|
||||
|
||||
aggregatorConfig := &aggregatorapiserver.Config{
|
||||
GenericConfig: &genericapiserver.RecommendedConfig{
|
||||
Config: sharedConfig.Config,
|
||||
SharedInformerFactory: fakev1Informers,
|
||||
ClientConfig: sharedConfig.LoopbackClientConfig,
|
||||
},
|
||||
ExtraConfig: aggregatorapiserver.ExtraConfig{
|
||||
DisableRemoteAvailableConditionController: true,
|
||||
// NOTE: while ProxyTransport can be skipped in the configuration, it allows honoring
|
||||
// DISABLE_HTTP2, HTTPS_PROXY and NO_PROXY env vars as needed
|
||||
ProxyTransport: createProxyTransport(),
|
||||
ServiceResolver: serviceResolver,
|
||||
},
|
||||
}
|
||||
|
||||
if commandOptions.KubeAggregatorOptions.LegacyClientCertAuth {
|
||||
// NOTE: the availability controller below is a bit different and uses the cert/key pair regardless
|
||||
// of the legacy bool, this is because we are still using that for discovery requests
|
||||
aggregatorConfig.ExtraConfig.ProxyClientCertFile = commandOptions.KubeAggregatorOptions.ProxyClientCertFile
|
||||
aggregatorConfig.ExtraConfig.ProxyClientKeyFile = commandOptions.KubeAggregatorOptions.ProxyClientKeyFile
|
||||
}
|
||||
|
||||
customExtraConfig := &CustomExtraConfig{
|
||||
DiscoveryOnlyProxyClientCertFile: commandOptions.KubeAggregatorOptions.ProxyClientCertFile,
|
||||
DiscoveryOnlyProxyClientKeyFile: commandOptions.KubeAggregatorOptions.ProxyClientKeyFile,
|
||||
}
|
||||
|
||||
if err := commandOptions.KubeAggregatorOptions.ApplyTo(aggregatorConfig, commandOptions.RecommendedOptions.Etcd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceAPIBuilder := service.NewServiceAPIBuilder()
|
||||
if err := serviceAPIBuilder.InstallSchema(aggregatorscheme.Scheme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
APIVersionPriorities[serviceAPIBuilder.GetGroupVersion()] = Priority{Group: 15000, Version: int32(1)}
|
||||
|
||||
// Exit early, if no remote services file is configured
|
||||
if commandOptions.KubeAggregatorOptions.RemoteServicesFile == "" {
|
||||
return NewConfig(aggregatorConfig, customExtraConfig, sharedInformerFactory, []builder.APIGroupBuilder{serviceAPIBuilder}, nil), nil
|
||||
}
|
||||
|
||||
remoteServices, err := ReadRemoteServices(commandOptions.KubeAggregatorOptions.RemoteServicesFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remoteServicesConfig := &RemoteServicesConfig{
|
||||
// TODO: in practice, we should only use the insecure flag when commandOptions.ExtraOptions.DevMode == true
|
||||
// But given the bug in K8s, we are forced to set it to true until the below PR is merged and available
|
||||
// https://github.com/kubernetes/kubernetes/pull/123808
|
||||
InsecureSkipTLSVerify: true,
|
||||
ExternalNamesNamespace: externalNamesNamespace,
|
||||
// TODO: CABundle can't be set when insecure is true
|
||||
// CABundle: caBundlePEM,
|
||||
Services: remoteServices,
|
||||
serviceClientSet: serviceClient,
|
||||
}
|
||||
|
||||
return NewConfig(aggregatorConfig, customExtraConfig, sharedInformerFactory, []builder.APIGroupBuilder{serviceAPIBuilder}, remoteServicesConfig), nil
|
||||
}
|
||||
|
||||
// CreateAggregatorServer creates an aggregated server to layer into the existing apiserver
|
||||
// TODO: passing options temporarily as that allows us to pass in cert/key for client into AvailableController but skip it in the aggregator lib
|
||||
func CreateAggregatorServer(config *Config, delegateAPIServer genericapiserver.DelegationTarget, reg prometheus.Registerer) (*aggregatorapiserver.APIAggregator, error) {
|
||||
aggregatorConfig := config.KubeAggregatorConfig
|
||||
sharedInformerFactory := config.Informers
|
||||
remoteServicesConfig := config.RemoteServicesConfig
|
||||
externalNamesInformer := sharedInformerFactory.Service().V0alpha1().ExternalNames()
|
||||
completedConfig := aggregatorConfig.Complete()
|
||||
|
||||
aggregatorServer, err := completedConfig.NewWithDelegate(delegateAPIServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create controllers for auto-registration
|
||||
apiRegistrationClient, err := apiregistrationclient.NewForConfig(completedConfig.GenericConfig.LoopbackClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
autoRegistrationController := autoregister.NewAutoRegisterController(aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(), apiRegistrationClient)
|
||||
|
||||
apiServices := apiServicesToRegister(delegateAPIServer, autoRegistrationController)
|
||||
|
||||
// Imbue all builtin group-priorities onto the aggregated discovery
|
||||
if completedConfig.GenericConfig.AggregatedDiscoveryGroupManager != nil {
|
||||
for gv, entry := range APIVersionPriorities {
|
||||
completedConfig.GenericConfig.AggregatedDiscoveryGroupManager.SetGroupVersionPriority(metav1.GroupVersion(gv), int(entry.Group), int(entry.Version))
|
||||
}
|
||||
}
|
||||
|
||||
err = aggregatorServer.GenericAPIServer.AddPostStartHook("grafana-apiserver-autoregistration", func(context genericapiserver.PostStartHookContext) error {
|
||||
go autoRegistrationController.Run(5, context.Done())
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if remoteServicesConfig != nil {
|
||||
addRemoteAPIServicesToRegister(remoteServicesConfig, autoRegistrationController)
|
||||
externalNames := getRemoteExternalNamesToRegister(remoteServicesConfig)
|
||||
err = aggregatorServer.GenericAPIServer.AddPostStartHook("grafana-apiserver-remote-autoregistration", func(ctx genericapiserver.PostStartHookContext) error {
|
||||
controllers.WaitForCacheSync("grafana-apiserver-remote-autoregistration", ctx.Done(), externalNamesInformer.Informer().HasSynced)
|
||||
namespacedClient := remoteServicesConfig.serviceClientSet.ServiceV0alpha1().ExternalNames(remoteServicesConfig.ExternalNamesNamespace)
|
||||
for _, externalName := range externalNames {
|
||||
_, err := namespacedClient.Apply(ctx, externalName, metav1.ApplyOptions{
|
||||
FieldManager: "grafana-aggregator",
|
||||
Force: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = aggregatorServer.GenericAPIServer.AddBootSequenceHealthChecks(
|
||||
makeAPIServiceAvailableHealthCheck(
|
||||
"autoregister-completion",
|
||||
apiServices,
|
||||
aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiregistrationClient, err := apiregistrationclientset.NewForConfig(completedConfig.GenericConfig.LoopbackClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxyCurrentCertKeyContentFunc := func() ([]byte, []byte) {
|
||||
return nil, nil
|
||||
}
|
||||
if len(config.CustomExtraConfig.DiscoveryOnlyProxyClientCertFile) > 0 && len(config.CustomExtraConfig.DiscoveryOnlyProxyClientKeyFile) > 0 {
|
||||
aggregatorProxyCerts, err := dynamiccertificates.NewDynamicServingContentFromFiles("aggregator-proxy-cert", config.CustomExtraConfig.DiscoveryOnlyProxyClientCertFile, config.CustomExtraConfig.DiscoveryOnlyProxyClientKeyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxyCurrentCertKeyContentFunc = func() ([]byte, []byte) {
|
||||
return aggregatorProxyCerts.CurrentCertKeyContent()
|
||||
}
|
||||
}
|
||||
|
||||
registry := legacyregistry.DefaultGatherer.(metrics.KubeRegistry)
|
||||
availibilityMetrics := newAvailabilityMetrics()
|
||||
// create shared (remote and local) availability metrics
|
||||
// TODO: decouple from legacyregistry
|
||||
registerIntoLegacyRegistryOnce.Do(func() { err = availibilityMetrics.Register(registry.Register, registry.CustomRegister) })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
availableController, err := NewAvailableConditionController(
|
||||
aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(),
|
||||
externalNamesInformer,
|
||||
apiregistrationClient.ApiregistrationV1(),
|
||||
nil,
|
||||
proxyCurrentCertKeyContentFunc,
|
||||
completedConfig.ExtraConfig.ServiceResolver,
|
||||
availibilityMetrics,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aggregatorServer.GenericAPIServer.AddPostStartHookOrDie("apiservice-status-override-available-controller", func(context genericapiserver.PostStartHookContext) error {
|
||||
// if we end up blocking for long periods of time, we may need to increase workers.
|
||||
go availableController.Run(5, context.Done())
|
||||
return nil
|
||||
})
|
||||
|
||||
aggregatorServer.GenericAPIServer.AddPostStartHookOrDie("start-grafana-aggregator-informers", func(context genericapiserver.PostStartHookContext) error {
|
||||
sharedInformerFactory.Start(context.Done())
|
||||
aggregatorServer.APIRegistrationInformers.Start(context.Done())
|
||||
return nil
|
||||
})
|
||||
|
||||
serviceAPIGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(servicev0alpha1.GROUP, aggregatorscheme.Scheme, metav1.ParameterCodec, aggregatorscheme.Codecs)
|
||||
for _, b := range config.Builders {
|
||||
err := b.UpdateAPIGroupInfo(
|
||||
&serviceAPIGroupInfo,
|
||||
builder.APIGroupOptions{
|
||||
Scheme: aggregatorscheme.Scheme,
|
||||
OptsGetter: aggregatorConfig.GenericConfig.RESTOptionsGetter,
|
||||
DualWriteBuilder: nil, // no dual writer
|
||||
MetricsRegister: reg,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := aggregatorServer.GenericAPIServer.InstallAPIGroup(&serviceAPIGroupInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return aggregatorServer, nil
|
||||
}
|
||||
|
||||
func makeAPIService(gv schema.GroupVersion) *v1.APIService {
|
||||
apiServicePriority, ok := APIVersionPriorities[gv]
|
||||
if !ok {
|
||||
// if we aren't found, then we shouldn't register ourselves because it could result in a CRD group version
|
||||
// being permanently stuck in the APIServices list.
|
||||
klog.Infof("Skipping APIService creation for %v", gv)
|
||||
return nil
|
||||
}
|
||||
return &v1.APIService{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: gv.Version + "." + gv.Group},
|
||||
Spec: v1.APIServiceSpec{
|
||||
Group: gv.Group,
|
||||
Version: gv.Version,
|
||||
GroupPriorityMinimum: apiServicePriority.Group,
|
||||
VersionPriority: apiServicePriority.Version,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// makeAPIServiceAvailableHealthCheck returns a healthz check that returns healthy
|
||||
// once all of the specified services have been observed to be available at least once.
|
||||
func makeAPIServiceAvailableHealthCheck(name string, apiServices []*v1.APIService, apiServiceInformer apiregistrationInformers.APIServiceInformer) healthz.HealthChecker {
|
||||
// Track the auto-registered API services that have not been observed to be available yet
|
||||
pendingServiceNamesLock := &sync.RWMutex{}
|
||||
pendingServiceNames := sets.NewString()
|
||||
for _, service := range apiServices {
|
||||
pendingServiceNames.Insert(service.Name)
|
||||
}
|
||||
|
||||
// When an APIService in the list is seen as available, remove it from the pending list
|
||||
handleAPIServiceChange := func(service *v1.APIService) {
|
||||
pendingServiceNamesLock.Lock()
|
||||
defer pendingServiceNamesLock.Unlock()
|
||||
if !pendingServiceNames.Has(service.Name) {
|
||||
return
|
||||
}
|
||||
if v1helper.IsAPIServiceConditionTrue(service, v1.Available) {
|
||||
pendingServiceNames.Delete(service.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch add/update events for APIServices
|
||||
_, err := apiServiceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) { handleAPIServiceChange(obj.(*v1.APIService)) },
|
||||
UpdateFunc: func(old, new interface{}) { handleAPIServiceChange(new.(*v1.APIService)) },
|
||||
})
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to watch APIServices for health check: %v", err)
|
||||
}
|
||||
|
||||
// Don't return healthy until the pending list is empty
|
||||
return healthz.NamedCheck(name, func(r *http.Request) error {
|
||||
pendingServiceNamesLock.RLock()
|
||||
defer pendingServiceNamesLock.RUnlock()
|
||||
if pendingServiceNames.Len() > 0 {
|
||||
klog.Error("APIServices not yet available", "services", pendingServiceNames.List())
|
||||
return fmt.Errorf("missing APIService: %v", pendingServiceNames.List())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Priority defines group Priority that is used in discovery. This controls
|
||||
// group position in the kubectl output.
|
||||
type Priority struct {
|
||||
// Group indicates the order of the Group relative to other groups.
|
||||
Group int32
|
||||
// Version indicates the relative order of the Version inside of its group.
|
||||
Version int32
|
||||
}
|
||||
|
||||
// APIVersionPriorities are the proper way to resolve this letting the aggregator know the desired group and version-within-group order of the underlying servers
|
||||
// is to refactor the genericapiserver.DelegationTarget to include a list of priorities based on which APIs were installed.
|
||||
// This requires the APIGroupInfo struct to evolve and include the concept of priorities and to avoid mistakes, the core storage map there needs to be updated.
|
||||
// That ripples out every bit as far as you'd expect, so for 1.7 we'll include the list here instead of being built up during storage.
|
||||
var APIVersionPriorities = map[schema.GroupVersion]Priority{
|
||||
{Group: "", Version: "v1"}: {Group: 18000, Version: 1},
|
||||
// to my knowledge, nothing below here collides
|
||||
{Group: "admissionregistration.k8s.io", Version: "v1"}: {Group: 16700, Version: 15},
|
||||
{Group: "admissionregistration.k8s.io", Version: "v1beta1"}: {Group: 16700, Version: 12},
|
||||
{Group: "admissionregistration.k8s.io", Version: "v1alpha1"}: {Group: 16700, Version: 9},
|
||||
// Append a new group to the end of the list if unsure.
|
||||
// You can use min(existing group)-100 as the initial value for a group.
|
||||
// Version can be set to 9 (to have space around) for a new group.
|
||||
}
|
||||
|
||||
func addRemoteAPIServicesToRegister(config *RemoteServicesConfig, registration autoregister.AutoAPIServiceRegistration) {
|
||||
for i, service := range config.Services {
|
||||
port := service.Port
|
||||
apiService := &v1.APIService{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: service.Version + "." + service.Group},
|
||||
Spec: v1.APIServiceSpec{
|
||||
Group: service.Group,
|
||||
Version: service.Version,
|
||||
InsecureSkipTLSVerify: config.InsecureSkipTLSVerify,
|
||||
CABundle: config.CABundle,
|
||||
// TODO: Group priority minimum of 1000 more than for local services, figure out a better story
|
||||
// when we have multiple versions, potentially running in heterogeneous ways (local and remote)
|
||||
GroupPriorityMinimum: 16000,
|
||||
VersionPriority: 1 + int32(i),
|
||||
Service: &v1.ServiceReference{
|
||||
Name: service.Version + "." + service.Group,
|
||||
Namespace: config.ExternalNamesNamespace,
|
||||
Port: &port,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
registration.AddAPIServiceToSyncOnStart(apiService)
|
||||
}
|
||||
}
|
||||
|
||||
func getRemoteExternalNamesToRegister(config *RemoteServicesConfig) []*servicev0alpha1applyconfiguration.ExternalNameApplyConfiguration {
|
||||
externalNames := make([]*servicev0alpha1applyconfiguration.ExternalNameApplyConfiguration, 0)
|
||||
|
||||
for _, service := range config.Services {
|
||||
host := service.Host
|
||||
name := service.Version + "." + service.Group
|
||||
externalName := &servicev0alpha1applyconfiguration.ExternalNameApplyConfiguration{}
|
||||
externalName.WithAPIVersion(servicev0alpha1.SchemeGroupVersion.String())
|
||||
externalName.WithKind("ExternalName")
|
||||
externalName.WithName(name)
|
||||
externalName.WithSpec(&servicev0alpha1applyconfiguration.ExternalNameSpecApplyConfiguration{
|
||||
Host: &host,
|
||||
})
|
||||
externalNames = append(externalNames, externalName)
|
||||
}
|
||||
|
||||
return externalNames
|
||||
}
|
||||
|
||||
func apiServicesToRegister(delegateAPIServer genericapiserver.DelegationTarget, registration autoregister.AutoAPIServiceRegistration) []*v1.APIService {
|
||||
apiServices := []*v1.APIService{}
|
||||
|
||||
for _, curr := range delegateAPIServer.ListedPaths() {
|
||||
if curr == "/api/v1" {
|
||||
apiService := makeAPIService(schema.GroupVersion{Group: "", Version: "v1"})
|
||||
registration.AddAPIServiceToSyncOnStart(apiService)
|
||||
apiServices = append(apiServices, apiService)
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(curr, "/apis/") {
|
||||
continue
|
||||
}
|
||||
// this comes back in a list that looks like /apis/rbac.authorization.k8s.io/v1alpha1
|
||||
tokens := strings.Split(curr, "/")
|
||||
if len(tokens) != 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
apiService := makeAPIService(schema.GroupVersion{Group: tokens[2], Version: tokens[3]})
|
||||
if apiService == nil {
|
||||
continue
|
||||
}
|
||||
registration.AddAPIServiceToSyncOnStart(apiService)
|
||||
apiServices = append(apiServices, apiService)
|
||||
}
|
||||
|
||||
return apiServices
|
||||
}
|
||||
|
||||
// NOTE: below function imported from https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/server.go#L197
|
||||
// createProxyTransport creates the dialer infrastructure to connect to the api servers.
|
||||
func createProxyTransport() *http.Transport {
|
||||
// NOTE: We don't set proxyDialerFn but the below SetTransportDefaults will
|
||||
// See https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/util/net/http.go#L109
|
||||
var proxyDialerFn utilnet.DialFunc
|
||||
// Proxying to services is IP-based... don't expect to be able to verify the hostname
|
||||
proxyTLSClientConfig := &tls.Config{InsecureSkipVerify: true}
|
||||
proxyTransport := utilnet.SetTransportDefaults(&http.Transport{
|
||||
DialContext: proxyDialerFn,
|
||||
TLSClientConfig: proxyTLSClientConfig,
|
||||
})
|
||||
return proxyTransport
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package aggregator_test
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
clientrest "k8s.io/client-go/rest"
|
||||
utilversion "k8s.io/component-base/version"
|
||||
"k8s.io/kube-aggregator/pkg/apiserver"
|
||||
aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
|
||||
aggregatoropenapi "k8s.io/kube-aggregator/pkg/generated/openapi"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
)
|
||||
|
||||
// TestAggregatorPostStartHooks tests that the kube-aggregator server has the expected default post start hooks enabled.
|
||||
func TestAggregatorPostStartHooks(t *testing.T) {
|
||||
cfg := apiserver.Config{
|
||||
GenericConfig: genericapiserver.NewRecommendedConfig(aggregatorscheme.Codecs),
|
||||
ExtraConfig: apiserver.ExtraConfig{},
|
||||
}
|
||||
|
||||
cfg.GenericConfig.ExternalAddress = "127.0.0.1:6443"
|
||||
cfg.GenericConfig.EffectiveVersion = utilversion.DefaultBuildEffectiveVersion()
|
||||
cfg.GenericConfig.LoopbackClientConfig = &clientrest.Config{}
|
||||
cfg.GenericConfig.MergedResourceConfig = apiserver.DefaultAPIResourceConfigSource()
|
||||
|
||||
// Add OpenAPI config, which depends on builders
|
||||
namer := openapinamer.NewDefinitionNamer(aggregatorscheme.Scheme)
|
||||
cfg.GenericConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(aggregatoropenapi.GetOpenAPIDefinitions, namer)
|
||||
cfg.GenericConfig.OpenAPIV3Config.Info.Title = "Kubernetes"
|
||||
cfg.GenericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(aggregatoropenapi.GetOpenAPIDefinitions, namer)
|
||||
cfg.GenericConfig.OpenAPIConfig.Info.Title = "Kubernetes"
|
||||
cfg.GenericConfig.SkipOpenAPIInstallation = true
|
||||
cfg.GenericConfig.SharedInformerFactory = informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 10*time.Minute)
|
||||
|
||||
// override the RESTOptionsGetter to use the in memory storage options
|
||||
restOptionsGetter, err := apistore.NewRESTOptionsGetterMemory(*storagebackend.NewDefaultConfig("memory", nil))
|
||||
require.NoError(t, err)
|
||||
cfg.GenericConfig.RESTOptionsGetter = restOptionsGetter
|
||||
|
||||
complete := cfg.Complete()
|
||||
|
||||
server, err := complete.NewWithDelegate(genericapiserver.NewEmptyDelegate())
|
||||
require.NoError(t, err)
|
||||
|
||||
actual := make([]string, 0, len(server.GenericAPIServer.PostStartHooks()))
|
||||
for k := range server.GenericAPIServer.PostStartHooks() {
|
||||
actual = append(actual, k)
|
||||
}
|
||||
sort.Strings(actual)
|
||||
expected := []string{
|
||||
"apiservice-discovery-controller",
|
||||
"generic-apiserver-start-informers",
|
||||
"max-in-flight-filter",
|
||||
"storage-object-count-tracker-hook",
|
||||
"start-kube-aggregator-informers",
|
||||
"apiservice-status-local-available-controller",
|
||||
"apiservice-status-remote-available-controller",
|
||||
"apiservice-registration-controller",
|
||||
}
|
||||
sort.Strings(expected)
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
@@ -1,490 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kube-aggregator/blob/master/pkg/controllers/status/available_controller.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
|
||||
package aggregator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/transport"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/klog/v2"
|
||||
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
||||
apiregistrationv1apihelper "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper"
|
||||
apiregistrationclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1"
|
||||
informers "k8s.io/kube-aggregator/pkg/client/informers/externalversions/apiregistration/v1"
|
||||
listers "k8s.io/kube-aggregator/pkg/client/listers/apiregistration/v1"
|
||||
"k8s.io/kube-aggregator/pkg/controllers"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/service/v0alpha1"
|
||||
informersservicev0alpha1 "github.com/grafana/grafana/pkg/generated/informers/externalversions/service/v0alpha1"
|
||||
listersservicev0alpha1 "github.com/grafana/grafana/pkg/generated/listers/service/v0alpha1"
|
||||
)
|
||||
|
||||
type certKeyFunc func() ([]byte, []byte)
|
||||
|
||||
// ServiceResolver knows how to convert a service reference into an actual location.
|
||||
type ServiceResolver interface {
|
||||
ResolveEndpoint(namespace, name string, port int32) (*url.URL, error)
|
||||
}
|
||||
|
||||
// AvailableConditionController handles checking the availability of registered API services.
|
||||
type AvailableConditionController struct {
|
||||
apiServiceClient apiregistrationclient.APIServicesGetter
|
||||
|
||||
apiServiceLister listers.APIServiceLister
|
||||
apiServiceSynced cache.InformerSynced
|
||||
|
||||
// externalNameLister is used to get the IP to create the transport for
|
||||
externalNameLister listersservicev0alpha1.ExternalNameLister
|
||||
servicesSynced cache.InformerSynced
|
||||
|
||||
// proxyTransportDial specifies the dial function for creating unencrypted TCP connections.
|
||||
proxyTransportDial *transport.DialHolder
|
||||
proxyCurrentCertKeyContent certKeyFunc
|
||||
serviceResolver ServiceResolver
|
||||
|
||||
// To allow injection for testing.
|
||||
syncFn func(key string) error
|
||||
|
||||
queue workqueue.TypedRateLimitingInterface[string]
|
||||
// map from service-namespace -> service-name -> apiservice names
|
||||
cache map[string]map[string][]string
|
||||
// this lock protects operations on the above cache
|
||||
cacheLock sync.RWMutex
|
||||
metrics *Metrics
|
||||
}
|
||||
|
||||
// NewAvailableConditionController returns a new AvailableConditionController.
|
||||
func NewAvailableConditionController(
|
||||
apiServiceInformer informers.APIServiceInformer,
|
||||
externalNameInformer informersservicev0alpha1.ExternalNameInformer,
|
||||
apiServiceClient apiregistrationclient.APIServicesGetter,
|
||||
proxyTransportDial *transport.DialHolder,
|
||||
proxyCurrentCertKeyContent certKeyFunc,
|
||||
serviceResolver ServiceResolver,
|
||||
metrics *Metrics,
|
||||
) (*AvailableConditionController, error) {
|
||||
c := &AvailableConditionController{
|
||||
apiServiceClient: apiServiceClient,
|
||||
apiServiceLister: apiServiceInformer.Lister(),
|
||||
externalNameLister: externalNameInformer.Lister(),
|
||||
serviceResolver: serviceResolver,
|
||||
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
|
||||
// We want a fairly tight requeue time. The controller listens to the API, but because it relies on the routability of the
|
||||
// service network, it is possible for an external, non-watchable factor to affect availability. This keeps
|
||||
// the maximum disruption time to a minimum, but it does prevent hot loops.
|
||||
workqueue.NewTypedItemExponentialFailureRateLimiter[string](5*time.Millisecond, 30*time.Second),
|
||||
workqueue.TypedRateLimitingQueueConfig[string]{Name: "AvailableConditionController"},
|
||||
),
|
||||
proxyTransportDial: proxyTransportDial,
|
||||
proxyCurrentCertKeyContent: proxyCurrentCertKeyContent,
|
||||
metrics: metrics,
|
||||
}
|
||||
|
||||
// resync on this one because it is low cardinality and rechecking the actual discovery
|
||||
// allows us to detect health in a more timely fashion when network connectivity to
|
||||
// nodes is snipped, but the network still attempts to route there. See
|
||||
// https://github.com/openshift/origin/issues/17159#issuecomment-341798063
|
||||
apiServiceHandler, _ := apiServiceInformer.Informer().AddEventHandlerWithResyncPeriod(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: c.addAPIService,
|
||||
UpdateFunc: c.updateAPIService,
|
||||
DeleteFunc: c.deleteAPIService,
|
||||
},
|
||||
30*time.Second)
|
||||
c.apiServiceSynced = apiServiceHandler.HasSynced
|
||||
|
||||
serviceHandler, _ := externalNameInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: c.addService,
|
||||
UpdateFunc: c.updateService,
|
||||
DeleteFunc: c.deleteService,
|
||||
})
|
||||
c.servicesSynced = serviceHandler.HasSynced
|
||||
|
||||
c.syncFn = c.sync
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *AvailableConditionController) sync(key string) error {
|
||||
originalAPIService, err := c.apiServiceLister.Get(key)
|
||||
if apierrors.IsNotFound(err) {
|
||||
if originalAPIService.Spec.Service != nil {
|
||||
// Only reset state, if the service was a remote service
|
||||
c.metrics.ForgetAPIService(key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if a particular transport was specified, use that otherwise build one
|
||||
// construct an http client that will ignore TLS verification (if someone owns the network and messes with your status
|
||||
// that's not so bad) and sets a very short timeout. This is a best effort GET that provides no additional information
|
||||
transportConfig := &transport.Config{
|
||||
TLS: transport.TLSConfig{
|
||||
Insecure: true,
|
||||
},
|
||||
DialHolder: c.proxyTransportDial,
|
||||
}
|
||||
|
||||
if c.proxyCurrentCertKeyContent != nil {
|
||||
proxyClientCert, proxyClientKey := c.proxyCurrentCertKeyContent()
|
||||
|
||||
transportConfig.TLS.CertData = proxyClientCert
|
||||
transportConfig.TLS.KeyData = proxyClientKey
|
||||
}
|
||||
restTransport, err := transport.New(transportConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discoveryClient := &http.Client{
|
||||
Transport: restTransport,
|
||||
// the request should happen quickly.
|
||||
Timeout: 5 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
apiService := originalAPIService.DeepCopy()
|
||||
|
||||
availableCondition := apiregistrationv1.APIServiceCondition{
|
||||
Type: apiregistrationv1.Available,
|
||||
Status: apiregistrationv1.ConditionTrue,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
}
|
||||
|
||||
// local API services are always considered available
|
||||
if apiService.Spec.Service == nil {
|
||||
apiregistrationv1apihelper.SetAPIServiceCondition(apiService, apiregistrationv1apihelper.NewLocalAvailableAPIServiceCondition())
|
||||
_, err := c.updateAPIServiceStatus(originalAPIService, apiService)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.externalNameLister.ExternalNames(apiService.Spec.Service.Namespace).Get(apiService.Spec.Service.Name)
|
||||
if apierrors.IsNotFound(err) {
|
||||
availableCondition.Status = apiregistrationv1.ConditionFalse
|
||||
availableCondition.Reason = "ServiceNotFound"
|
||||
availableCondition.Message = fmt.Sprintf("service/%s in %q is not present", apiService.Spec.Service.Name, apiService.Spec.Service.Namespace)
|
||||
apiregistrationv1apihelper.SetAPIServiceCondition(apiService, availableCondition)
|
||||
_, err := c.updateAPIServiceStatus(originalAPIService, apiService)
|
||||
return err
|
||||
} else if err != nil {
|
||||
availableCondition.Status = apiregistrationv1.ConditionUnknown
|
||||
availableCondition.Reason = "ServiceAccessError"
|
||||
availableCondition.Message = fmt.Sprintf("service/%s in %q cannot be checked due to: %v", apiService.Spec.Service.Name, apiService.Spec.Service.Namespace, err)
|
||||
apiregistrationv1apihelper.SetAPIServiceCondition(apiService, availableCondition)
|
||||
_, err := c.updateAPIServiceStatus(originalAPIService, apiService)
|
||||
return err
|
||||
}
|
||||
|
||||
// actually try to hit the discovery endpoint when it isn't local and when we're routing as a service.
|
||||
if apiService.Spec.Service != nil && c.serviceResolver != nil {
|
||||
attempts := 5
|
||||
results := make(chan error, attempts)
|
||||
for i := 0; i < attempts; i++ {
|
||||
go func() {
|
||||
// stagger these requests to reduce pressure on aggregated services
|
||||
waitDuration := time.Second * time.Duration(int32(i))
|
||||
time.Sleep(waitDuration)
|
||||
|
||||
discoveryURL, err := c.serviceResolver.ResolveEndpoint(apiService.Spec.Service.Namespace, apiService.Spec.Service.Name, *apiService.Spec.Service.Port)
|
||||
if err != nil {
|
||||
results <- err
|
||||
return
|
||||
}
|
||||
// render legacyAPIService health check path when it is delegated to a service
|
||||
if apiService.Name == "v1." {
|
||||
discoveryURL.Path = "/api/" + apiService.Spec.Version
|
||||
} else {
|
||||
discoveryURL.Path = "/apis/" + apiService.Spec.Group + "/" + apiService.Spec.Version
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
// be sure to check a URL that the aggregated API server is required to serve
|
||||
newReq, err := http.NewRequest("GET", discoveryURL.String(), nil)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
// setting the system-masters identity ensures that we will always have access rights
|
||||
uid := ""
|
||||
var extra map[string][]string
|
||||
transport.SetAuthProxyHeaders(newReq, "system:kube-aggregator", uid, []string{"system:masters"}, extra)
|
||||
resp, err := discoveryClient.Do(newReq)
|
||||
if resp != nil {
|
||||
_ = resp.Body.Close()
|
||||
// we should always been in the 200s or 300s
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
errCh <- fmt.Errorf("bad status from %v: %d", discoveryURL, resp.StatusCode)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err = <-errCh:
|
||||
if err != nil {
|
||||
results <- fmt.Errorf("failing or missing response from %v: %v", discoveryURL, err)
|
||||
return
|
||||
}
|
||||
|
||||
// we had trouble with slow dial and DNS responses causing us to wait too long.
|
||||
// we added this as insurance
|
||||
case <-time.After(6 * time.Second):
|
||||
results <- fmt.Errorf("timed out waiting for %v", discoveryURL)
|
||||
return
|
||||
}
|
||||
|
||||
results <- nil
|
||||
}()
|
||||
}
|
||||
|
||||
var lastError error
|
||||
for i := 0; i < attempts; i++ {
|
||||
lastError = <-results
|
||||
// if we had at least one success, we are successful overall and we can return now
|
||||
if lastError == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lastError != nil {
|
||||
availableCondition.Status = apiregistrationv1.ConditionFalse
|
||||
availableCondition.Reason = "FailedDiscoveryCheck"
|
||||
availableCondition.Message = lastError.Error()
|
||||
apiregistrationv1apihelper.SetAPIServiceCondition(apiService, availableCondition)
|
||||
_, updateErr := c.updateAPIServiceStatus(originalAPIService, apiService)
|
||||
if updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
// force a requeue to make it very obvious that this will be retried at some point in the future
|
||||
// along with other requeues done via service change, endpoint change, and resync
|
||||
return lastError
|
||||
}
|
||||
}
|
||||
|
||||
availableCondition.Reason = "Passed"
|
||||
availableCondition.Message = "all checks passed"
|
||||
apiregistrationv1apihelper.SetAPIServiceCondition(apiService, availableCondition)
|
||||
_, err = c.updateAPIServiceStatus(originalAPIService, apiService)
|
||||
return err
|
||||
}
|
||||
|
||||
// updateAPIServiceStatus only issues an update if a change is detected. We have a tight resync loop to quickly detect dead
|
||||
// apiservices. Doing that means we don't want to quickly issue no-op updates.
|
||||
func (c *AvailableConditionController) updateAPIServiceStatus(originalAPIService, newAPIService *apiregistrationv1.APIService) (*apiregistrationv1.APIService, error) {
|
||||
// update this metric on every sync operation to reflect the actual state
|
||||
if newAPIService.Spec.Service != nil {
|
||||
// Only expose the metric for remote services, trusts the type on the new object
|
||||
c.metrics.SetUnavailableGauge(newAPIService)
|
||||
}
|
||||
|
||||
if equality.Semantic.DeepEqual(originalAPIService.Status, newAPIService.Status) {
|
||||
return newAPIService, nil
|
||||
}
|
||||
|
||||
orig := apiregistrationv1apihelper.GetAPIServiceConditionByType(originalAPIService, apiregistrationv1.Available)
|
||||
now := apiregistrationv1apihelper.GetAPIServiceConditionByType(newAPIService, apiregistrationv1.Available)
|
||||
unknown := apiregistrationv1.APIServiceCondition{
|
||||
Type: apiregistrationv1.Available,
|
||||
Status: apiregistrationv1.ConditionUnknown,
|
||||
}
|
||||
if orig == nil {
|
||||
orig = &unknown
|
||||
}
|
||||
if now == nil {
|
||||
now = &unknown
|
||||
}
|
||||
if *orig != *now {
|
||||
klog.V(2).InfoS("changing APIService availability", "name", newAPIService.Name, "oldStatus", orig.Status, "newStatus", now.Status, "message", now.Message, "reason", now.Reason)
|
||||
}
|
||||
|
||||
newAPIService, err := c.apiServiceClient.APIServices().UpdateStatus(context.TODO(), newAPIService, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if newAPIService.Spec.Service != nil {
|
||||
// Only expose the metric for remote services, trusts the type on the new object
|
||||
c.metrics.SetUnavailableCounter(originalAPIService, newAPIService)
|
||||
}
|
||||
return newAPIService, nil
|
||||
}
|
||||
|
||||
// Run starts the AvailableConditionController loop which manages the availability condition of API services.
|
||||
func (c *AvailableConditionController) Run(workers int, stopCh <-chan struct{}) {
|
||||
defer utilruntime.HandleCrash()
|
||||
defer c.queue.ShutDown()
|
||||
|
||||
klog.Info("Starting AvailableConditionController")
|
||||
defer klog.Info("Shutting down AvailableConditionController")
|
||||
|
||||
// This waits not just for the informers to sync, but for our handlers
|
||||
// to be called; since the handlers are three different ways of
|
||||
// enqueueing the same thing, waiting for this permits the queue to
|
||||
// maximally de-duplicate the entries.
|
||||
if !controllers.WaitForCacheSync("AvailableConditionCOverrideController", stopCh, c.apiServiceSynced, c.servicesSynced) {
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < workers; i++ {
|
||||
go wait.Until(c.runWorker, time.Second, stopCh)
|
||||
}
|
||||
|
||||
<-stopCh
|
||||
}
|
||||
|
||||
func (c *AvailableConditionController) runWorker() {
|
||||
for c.processNextWorkItem() {
|
||||
}
|
||||
}
|
||||
|
||||
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
|
||||
func (c *AvailableConditionController) processNextWorkItem() bool {
|
||||
key, quit := c.queue.Get()
|
||||
if quit {
|
||||
return false
|
||||
}
|
||||
defer c.queue.Done(key)
|
||||
|
||||
err := c.syncFn(key)
|
||||
if err == nil {
|
||||
c.queue.Forget(key)
|
||||
return true
|
||||
}
|
||||
|
||||
utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err))
|
||||
c.queue.AddRateLimited(key)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *AvailableConditionController) addAPIService(obj interface{}) {
|
||||
castObj := obj.(*apiregistrationv1.APIService)
|
||||
klog.V(4).Infof("Adding %s", castObj.Name)
|
||||
if castObj.Spec.Service != nil {
|
||||
c.rebuildAPIServiceCache()
|
||||
}
|
||||
c.queue.Add(castObj.Name)
|
||||
}
|
||||
|
||||
func (c *AvailableConditionController) updateAPIService(oldObj, newObj interface{}) {
|
||||
castObj := newObj.(*apiregistrationv1.APIService)
|
||||
oldCastObj := oldObj.(*apiregistrationv1.APIService)
|
||||
klog.V(4).Infof("Updating %s", oldCastObj.Name)
|
||||
if !reflect.DeepEqual(castObj.Spec.Service, oldCastObj.Spec.Service) {
|
||||
c.rebuildAPIServiceCache()
|
||||
}
|
||||
c.queue.Add(oldCastObj.Name)
|
||||
}
|
||||
|
||||
func (c *AvailableConditionController) deleteAPIService(obj interface{}) {
|
||||
castObj, ok := obj.(*apiregistrationv1.APIService)
|
||||
if !ok {
|
||||
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
||||
if !ok {
|
||||
klog.Errorf("Couldn't get object from tombstone %#v", obj)
|
||||
return
|
||||
}
|
||||
castObj, ok = tombstone.Obj.(*apiregistrationv1.APIService)
|
||||
if !ok {
|
||||
klog.Errorf("Tombstone contained object that is not expected %#v", obj)
|
||||
return
|
||||
}
|
||||
}
|
||||
klog.V(4).Infof("Deleting %q", castObj.Name)
|
||||
if castObj.Spec.Service != nil {
|
||||
c.rebuildAPIServiceCache()
|
||||
}
|
||||
c.queue.Add(castObj.Name)
|
||||
}
|
||||
|
||||
func (c *AvailableConditionController) getAPIServicesFor(obj runtime.Object) []string {
|
||||
metadata, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
return nil
|
||||
}
|
||||
c.cacheLock.RLock()
|
||||
defer c.cacheLock.RUnlock()
|
||||
return c.cache[metadata.GetNamespace()][metadata.GetName()]
|
||||
}
|
||||
|
||||
// if the service/endpoint handler wins the race against the cache rebuilding, it may queue a no-longer-relevant apiservice
|
||||
// (which will get processed an extra time - this doesn't matter),
|
||||
// and miss a newly relevant apiservice (which will get queued by the apiservice handler)
|
||||
func (c *AvailableConditionController) rebuildAPIServiceCache() {
|
||||
apiServiceList, _ := c.apiServiceLister.List(labels.Everything())
|
||||
newCache := map[string]map[string][]string{}
|
||||
for _, apiService := range apiServiceList {
|
||||
if apiService.Spec.Service == nil {
|
||||
continue
|
||||
}
|
||||
if newCache[apiService.Spec.Service.Namespace] == nil {
|
||||
newCache[apiService.Spec.Service.Namespace] = map[string][]string{}
|
||||
}
|
||||
newCache[apiService.Spec.Service.Namespace][apiService.Spec.Service.Name] = append(newCache[apiService.Spec.Service.Namespace][apiService.Spec.Service.Name], apiService.Name)
|
||||
}
|
||||
|
||||
c.cacheLock.Lock()
|
||||
defer c.cacheLock.Unlock()
|
||||
c.cache = newCache
|
||||
}
|
||||
|
||||
// TODO, think of a way to avoid checking on every service manipulation
|
||||
|
||||
func (c *AvailableConditionController) addService(obj interface{}) {
|
||||
for _, apiService := range c.getAPIServicesFor(obj.(*v0alpha1.ExternalName)) {
|
||||
c.queue.Add(apiService)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AvailableConditionController) updateService(obj, _ interface{}) {
|
||||
for _, apiService := range c.getAPIServicesFor(obj.(*v0alpha1.ExternalName)) {
|
||||
c.queue.Add(apiService)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AvailableConditionController) deleteService(obj interface{}) {
|
||||
castObj, ok := obj.(*v0alpha1.ExternalName)
|
||||
if !ok {
|
||||
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
||||
if !ok {
|
||||
klog.Errorf("Couldn't get object from tombstone %#v", obj)
|
||||
return
|
||||
}
|
||||
castObj, ok = tombstone.Obj.(*v0alpha1.ExternalName)
|
||||
if !ok {
|
||||
klog.Errorf("Tombstone contained object that is not expected %#v", obj)
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, apiService := range c.getAPIServicesFor(castObj) {
|
||||
c.queue.Add(apiService)
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package aggregator
|
||||
|
||||
import (
|
||||
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
|
||||
aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
|
||||
aggregatoropenapi "k8s.io/kube-aggregator/pkg/generated/openapi"
|
||||
"k8s.io/kube-openapi/pkg/common"
|
||||
|
||||
serviceclientset "github.com/grafana/grafana/pkg/generated/clientset/versioned"
|
||||
informersv0alpha1 "github.com/grafana/grafana/pkg/generated/informers/externalversions"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
)
|
||||
|
||||
type RemoteService struct {
|
||||
Group string `yaml:"group"`
|
||||
Version string `yaml:"version"`
|
||||
Host string `yaml:"host"`
|
||||
Port int32 `yaml:"port"`
|
||||
}
|
||||
|
||||
type RemoteServicesConfig struct {
|
||||
ExternalNamesNamespace string
|
||||
InsecureSkipTLSVerify bool
|
||||
CABundle []byte
|
||||
Services []RemoteService
|
||||
serviceClientSet *serviceclientset.Clientset
|
||||
}
|
||||
|
||||
type CustomExtraConfig struct {
|
||||
DiscoveryOnlyProxyClientCertFile string
|
||||
DiscoveryOnlyProxyClientKeyFile string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
KubeAggregatorConfig *aggregatorapiserver.Config
|
||||
CustomExtraConfig *CustomExtraConfig // this is temporary and will be removed once we have moved across newer auth rollout in cloud
|
||||
Informers informersv0alpha1.SharedInformerFactory
|
||||
RemoteServicesConfig *RemoteServicesConfig
|
||||
// Builders contain prerequisite api groups for aggregator to function correctly e.g. ExternalName
|
||||
// Since the main APIServer delegate supports storage implementations that intend to be multi-tenant
|
||||
// Aggregator builders that we don't intend to use multi-tenant storage are kept in aggregator's
|
||||
// Delegate, one which is configured explicitly to use file storage only
|
||||
Builders []builder.APIGroupBuilder
|
||||
}
|
||||
|
||||
// remoteServices may be nil when not using aggregation
|
||||
func NewConfig(aggregator *aggregatorapiserver.Config, customExtraConfig *CustomExtraConfig, informers informersv0alpha1.SharedInformerFactory, builders []builder.APIGroupBuilder, remoteServices *RemoteServicesConfig) *Config {
|
||||
getMergedOpenAPIDefinitions := func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||
aggregatorAPIs := aggregatoropenapi.GetOpenAPIDefinitions(ref)
|
||||
builderAPIs := builder.GetOpenAPIDefinitions(builders)(ref)
|
||||
|
||||
for k, v := range builderAPIs {
|
||||
aggregatorAPIs[k] = v
|
||||
}
|
||||
|
||||
return aggregatorAPIs
|
||||
}
|
||||
|
||||
// Add OpenAPI config, which depends on builders
|
||||
namer := openapinamer.NewDefinitionNamer(aggregatorscheme.Scheme)
|
||||
aggregator.GenericConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(getMergedOpenAPIDefinitions, namer)
|
||||
aggregator.GenericConfig.OpenAPIV3Config.Info.Title = "Kubernetes"
|
||||
aggregator.GenericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(getMergedOpenAPIDefinitions, namer)
|
||||
aggregator.GenericConfig.OpenAPIConfig.Info.Title = "Kubernetes"
|
||||
|
||||
return &Config{
|
||||
aggregator,
|
||||
customExtraConfig,
|
||||
informers,
|
||||
remoteServices,
|
||||
builders,
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
# NOTE: dev-mode only and governed by presence of non-empty value for cfg["grafana-apiserver"]["remote_services_file"]
|
||||
# List of sample multi-tenant services to aggregate on startup
|
||||
- group: example.grafana.app
|
||||
version: v0alpha1
|
||||
host: localhost
|
||||
port: 7443
|
||||
- group: query.grafana.app
|
||||
version: v0alpha1
|
||||
host: localhost
|
||||
port: 7444
|
||||
- group: testdata.datasource.grafana.app
|
||||
version: v0alpha1
|
||||
host: localhost
|
||||
port: 7445
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
apiVersion: apiregistration.k8s.io/v1
|
||||
kind: APIService
|
||||
metadata:
|
||||
name: v0alpha1.example.grafana.app
|
||||
spec:
|
||||
version: v0alpha1
|
||||
insecureSkipTLSVerify: true
|
||||
group: example.grafana.app
|
||||
groupPriorityMinimum: 1000
|
||||
versionPriority: 15
|
||||
service:
|
||||
name: example-apiserver
|
||||
namespace: grafana
|
||||
port: 7443
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
apiVersion: service.grafana.app/v0alpha1
|
||||
kind: ExternalName
|
||||
metadata:
|
||||
name: example-apiserver
|
||||
namespace: grafana
|
||||
spec:
|
||||
host: localhost
|
||||
@@ -1,170 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/kube-aggregator/pkg/controllers/status/metrics/metrics.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
|
||||
package aggregator
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"k8s.io/component-base/metrics"
|
||||
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
||||
apiregistrationv1apihelper "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper"
|
||||
)
|
||||
|
||||
/*
|
||||
* By default, all the following metrics are defined as falling under
|
||||
* ALPHA stability level https://github.com/kubernetes/enhancements/blob/master/keps/sig-instrumentation/1209-metrics-stability/kubernetes-control-plane-metrics-stability.md#stability-classes)
|
||||
*
|
||||
* Promoting the stability level of the metric is a responsibility of the component owner, since it
|
||||
* involves explicitly acknowledging support for the metric across multiple releases, in accordance with
|
||||
* the metric stability policy.
|
||||
*/
|
||||
var (
|
||||
unavailableGaugeDesc = metrics.NewDesc(
|
||||
"st_aggregator_unavailable_apiservice",
|
||||
"Gauge of Grafana APIServices which are marked as unavailable broken down by APIService name.",
|
||||
[]string{"name"},
|
||||
nil,
|
||||
metrics.ALPHA,
|
||||
"",
|
||||
)
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
unavailableCounter *metrics.CounterVec
|
||||
|
||||
*availabilityCollector
|
||||
}
|
||||
|
||||
func newAvailabilityMetrics() *Metrics {
|
||||
return &Metrics{
|
||||
unavailableCounter: metrics.NewCounterVec(
|
||||
&metrics.CounterOpts{
|
||||
// These metrics are registered in the main kube-aggregator package as well, prefixing with single-tenant (ST) to avoid
|
||||
// "duplicate metrics collector registration attempted" in https://github.com/prometheus/client_golang
|
||||
// a more descriptive prefix is already added for apiserver metrics during scraping in cloud and didn't want
|
||||
// to double a word by using a word such as "grafana" here
|
||||
Name: "st_aggregator_unavailable_apiservice_total",
|
||||
Help: "Counter of Grafana APIServices which are marked as unavailable broken down by APIService name and reason.",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
},
|
||||
[]string{"name", "reason"},
|
||||
),
|
||||
availabilityCollector: newAvailabilityCollector(),
|
||||
}
|
||||
}
|
||||
|
||||
// Register registers apiservice availability metrics.
|
||||
func (m *Metrics) Register(
|
||||
registrationFunc func(metrics.Registerable) error,
|
||||
customRegistrationFunc func(metrics.StableCollector) error,
|
||||
) error {
|
||||
err := registrationFunc(m.unavailableCounter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = customRegistrationFunc(m.availabilityCollector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnavailableCounter returns a counter to track apiservices marked as unavailable.
|
||||
func (m *Metrics) UnavailableCounter(apiServiceName, reason string) metrics.CounterMetric {
|
||||
return m.unavailableCounter.WithLabelValues(apiServiceName, reason)
|
||||
}
|
||||
|
||||
type availabilityCollector struct {
|
||||
metrics.BaseStableCollector
|
||||
|
||||
mtx sync.RWMutex
|
||||
availabilities map[string]bool
|
||||
}
|
||||
|
||||
// SetUnavailableGauge set the metrics so that it reflect the current state based on availability of the given service
|
||||
func (m *Metrics) SetUnavailableGauge(newAPIService *apiregistrationv1.APIService) {
|
||||
if apiregistrationv1apihelper.IsAPIServiceConditionTrue(newAPIService, apiregistrationv1.Available) {
|
||||
m.SetAPIServiceAvailable(newAPIService.Name)
|
||||
return
|
||||
}
|
||||
|
||||
m.SetAPIServiceUnavailable(newAPIService.Name)
|
||||
}
|
||||
|
||||
// SetUnavailableCounter increases the metrics only if the given service is unavailable and its APIServiceCondition has changed
|
||||
func (m *Metrics) SetUnavailableCounter(originalAPIService, newAPIService *apiregistrationv1.APIService) {
|
||||
wasAvailable := apiregistrationv1apihelper.IsAPIServiceConditionTrue(originalAPIService, apiregistrationv1.Available)
|
||||
isAvailable := apiregistrationv1apihelper.IsAPIServiceConditionTrue(newAPIService, apiregistrationv1.Available)
|
||||
statusChanged := isAvailable != wasAvailable
|
||||
|
||||
if statusChanged && !isAvailable {
|
||||
reason := "UnknownReason"
|
||||
if newCondition := apiregistrationv1apihelper.GetAPIServiceConditionByType(newAPIService, apiregistrationv1.Available); newCondition != nil {
|
||||
reason = newCondition.Reason
|
||||
}
|
||||
m.UnavailableCounter(newAPIService.Name, reason).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
// Check if apiServiceStatusCollector implements necessary interface.
|
||||
var _ metrics.StableCollector = &availabilityCollector{}
|
||||
|
||||
func newAvailabilityCollector() *availabilityCollector {
|
||||
return &availabilityCollector{
|
||||
availabilities: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// DescribeWithStability implements the metrics.StableCollector interface.
|
||||
func (c *availabilityCollector) DescribeWithStability(ch chan<- *metrics.Desc) {
|
||||
ch <- unavailableGaugeDesc
|
||||
}
|
||||
|
||||
// CollectWithStability implements the metrics.StableCollector interface.
|
||||
func (c *availabilityCollector) CollectWithStability(ch chan<- metrics.Metric) {
|
||||
c.mtx.RLock()
|
||||
defer c.mtx.RUnlock()
|
||||
|
||||
for apiServiceName, isAvailable := range c.availabilities {
|
||||
gaugeValue := 1.0
|
||||
if isAvailable {
|
||||
gaugeValue = 0.0
|
||||
}
|
||||
ch <- metrics.NewLazyConstMetric(
|
||||
unavailableGaugeDesc,
|
||||
metrics.GaugeValue,
|
||||
gaugeValue,
|
||||
apiServiceName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAPIServiceAvailable sets the given apiservice availability gauge to available.
|
||||
func (c *availabilityCollector) SetAPIServiceAvailable(apiServiceKey string) {
|
||||
c.setAPIServiceAvailability(apiServiceKey, true)
|
||||
}
|
||||
|
||||
// SetAPIServiceUnavailable sets the given apiservice availability gauge to unavailable.
|
||||
func (c *availabilityCollector) SetAPIServiceUnavailable(apiServiceKey string) {
|
||||
c.setAPIServiceAvailability(apiServiceKey, false)
|
||||
}
|
||||
|
||||
func (c *availabilityCollector) setAPIServiceAvailability(apiServiceKey string, availability bool) {
|
||||
c.mtx.Lock()
|
||||
defer c.mtx.Unlock()
|
||||
|
||||
c.availabilities[apiServiceKey] = availability
|
||||
}
|
||||
|
||||
// ForgetAPIService removes the availability gauge of the given apiservice.
|
||||
func (c *availabilityCollector) ForgetAPIService(apiServiceKey string) {
|
||||
c.mtx.Lock()
|
||||
defer c.mtx.Unlock()
|
||||
|
||||
delete(c.availabilities, apiServiceKey)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package aggregator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"k8s.io/kube-aggregator/pkg/apiserver"
|
||||
|
||||
servicelistersv0alpha1 "github.com/grafana/grafana/pkg/generated/listers/service/v0alpha1"
|
||||
)
|
||||
|
||||
func NewExternalNameResolver(externalNames servicelistersv0alpha1.ExternalNameLister) apiserver.ServiceResolver {
|
||||
return &externalNameResolver{
|
||||
externalNames: externalNames,
|
||||
}
|
||||
}
|
||||
|
||||
type externalNameResolver struct {
|
||||
externalNames servicelistersv0alpha1.ExternalNameLister
|
||||
}
|
||||
|
||||
func (r *externalNameResolver) ResolveEndpoint(namespace, name string, port int32) (*url.URL, error) {
|
||||
extName, err := r.externalNames.ExternalNames(namespace).Get(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &url.URL{
|
||||
Scheme: "https",
|
||||
Host: net.JoinHostPort(extName.Spec.Host, fmt.Sprintf("%d", port)),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package aggregatorrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||
)
|
||||
|
||||
type NoopAggregatorConfigurator struct {
|
||||
}
|
||||
|
||||
func (n NoopAggregatorConfigurator) Configure(opts *options.Options, config *genericapiserver.RecommendedConfig, delegateAPIServer genericapiserver.DelegationTarget, scheme *runtime.Scheme, builders []builder.APIGroupBuilder) (*genericapiserver.GenericAPIServer, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n NoopAggregatorConfigurator) Run(ctx context.Context, transport *options.RoundTripperFunc, stoppedCh chan error) (*genericapiserver.GenericAPIServer, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func ProvideNoopAggregatorConfigurator() AggregatorRunner {
|
||||
return &NoopAggregatorConfigurator{}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package aggregatorrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||
)
|
||||
|
||||
// AggregatorRunner is an interface for running an aggregator inside the same generic apiserver delegate chain
|
||||
type AggregatorRunner interface {
|
||||
// Configure is called to configure the component and returns the delegate for further chaining.
|
||||
Configure(opts *options.Options,
|
||||
config *genericapiserver.RecommendedConfig,
|
||||
delegateAPIServer genericapiserver.DelegationTarget,
|
||||
scheme *runtime.Scheme,
|
||||
builders []builder.APIGroupBuilder) (*genericapiserver.GenericAPIServer, error)
|
||||
|
||||
// Run starts the complete apiserver chain, expects it executes any logic inside a goroutine and doesn't block. Returns the running server.
|
||||
Run(ctx context.Context, transport *options.RoundTripperFunc, stoppedCh chan error) (*genericapiserver.GenericAPIServer, error)
|
||||
}
|
||||
@@ -46,13 +46,6 @@ func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o
|
||||
o.RecommendedOptions.Authentication.RemoteKubeConfigFileOptional = true
|
||||
o.RecommendedOptions.Authorization.RemoteKubeConfigFileOptional = true
|
||||
|
||||
o.KubeAggregatorOptions.ProxyClientCertFile = apiserverCfg.Key("proxy_client_cert_file").MustString("")
|
||||
o.KubeAggregatorOptions.ProxyClientKeyFile = apiserverCfg.Key("proxy_client_key_file").MustString("")
|
||||
o.KubeAggregatorOptions.LegacyClientCertAuth = apiserverCfg.Key("legacy_client_cert_auth").MustBool(true)
|
||||
|
||||
o.KubeAggregatorOptions.APIServiceCABundleFile = apiserverCfg.Key("apiservice_ca_bundle_file").MustString("")
|
||||
o.KubeAggregatorOptions.RemoteServicesFile = apiserverCfg.Key("remote_services_file").MustString("")
|
||||
|
||||
o.RecommendedOptions.Admission = nil
|
||||
o.RecommendedOptions.CoreAPI = nil
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package options
|
||||
import (
|
||||
"maps"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/endpoints/openapi"
|
||||
@@ -12,6 +11,8 @@ import (
|
||||
"k8s.io/apiserver/pkg/server/resourceconfig"
|
||||
"k8s.io/kube-openapi/pkg/common"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/grafana/grafana/pkg/aggregator/apis/aggregation/v0alpha1"
|
||||
aggregatorapiserver "github.com/grafana/grafana/pkg/aggregator/apiserver"
|
||||
aggregatorscheme "github.com/grafana/grafana/pkg/aggregator/apiserver/scheme"
|
||||
@@ -88,7 +89,5 @@ func (o *GrafanaAggregatorOptions) ApplyTo(aggregatorConfig *aggregatorapiserver
|
||||
}
|
||||
genericConfig.MergedResourceConfig = mergedResourceConfig
|
||||
|
||||
genericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/spf13/pflag"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/options"
|
||||
"k8s.io/apiserver/pkg/server/resourceconfig"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
apiregistrationv1beta1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
|
||||
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
|
||||
aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"
|
||||
|
||||
servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
)
|
||||
|
||||
// KubeAggregatorOptions contains the state for the aggregator apiserver
|
||||
type KubeAggregatorOptions struct {
|
||||
AlternateDNS []string
|
||||
ProxyClientCertFile string
|
||||
ProxyClientKeyFile string
|
||||
LegacyClientCertAuth bool
|
||||
RemoteServicesFile string
|
||||
APIServiceCABundleFile string
|
||||
}
|
||||
|
||||
func NewAggregatorServerOptions() *KubeAggregatorOptions {
|
||||
return &KubeAggregatorOptions{}
|
||||
}
|
||||
|
||||
func (o *KubeAggregatorOptions) AddFlags(fs *pflag.FlagSet) {
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// the following two config variables are slated to be faded out in cloud deployments after which
|
||||
// their scope is restricted to local development and non Grafana Cloud use-cases only
|
||||
// leaving them unspecified leads to graceful behavior in grafana-aggregator
|
||||
// and would work for configurations where the aggregated servers and aggregator are auth-less and trusting
|
||||
// of each other
|
||||
fs.StringVar(&o.ProxyClientCertFile, "proxy-client-cert-file", o.ProxyClientCertFile,
|
||||
"path to proxy client cert file")
|
||||
|
||||
fs.StringVar(&o.ProxyClientKeyFile, "proxy-client-key-file", o.ProxyClientKeyFile,
|
||||
"path to proxy client key file")
|
||||
|
||||
fs.BoolVar(&o.LegacyClientCertAuth, "legacy-client-cert-auth", true,
|
||||
"whether to use legacy client cert auth")
|
||||
}
|
||||
|
||||
func (o *KubeAggregatorOptions) Validate() []error {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: do we need to validate anything here?
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *KubeAggregatorOptions) ApplyTo(aggregatorConfig *aggregatorapiserver.Config, etcdOpts *options.EtcdOptions) error {
|
||||
genericConfig := aggregatorConfig.GenericConfig
|
||||
|
||||
genericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
|
||||
genericConfig.RESTOptionsGetter = nil
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StorageVersionAPI) &&
|
||||
utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) {
|
||||
// Add StorageVersionPrecondition handler to aggregator-apiserver.
|
||||
// The handler will block write requests to built-in resources until the
|
||||
// target resources' storage versions are up-to-date.
|
||||
genericConfig.BuildHandlerChainFunc = genericapiserver.BuildHandlerChainWithStorageVersionPrecondition
|
||||
}
|
||||
|
||||
// copy the etcd options so we don't mutate originals.
|
||||
// we assume that the etcd options have been completed already. avoid messing with anything outside
|
||||
// of changes to StorageConfig as that may lead to unexpected behavior when the options are applied.
|
||||
etcdOptions := *etcdOpts
|
||||
etcdOptions.StorageConfig.Codec = aggregatorscheme.Codecs.LegacyCodec(v1.SchemeGroupVersion,
|
||||
apiregistrationv1beta1.SchemeGroupVersion,
|
||||
servicev0alpha1.SchemeGroupVersion)
|
||||
etcdOptions.StorageConfig.EncodeVersioner = runtime.NewMultiGroupVersioner(v1.SchemeGroupVersion,
|
||||
schema.GroupKind{Group: apiregistrationv1beta1.GroupName},
|
||||
schema.GroupKind{Group: servicev0alpha1.GROUP})
|
||||
etcdOptions.SkipHealthEndpoints = true // avoid double wiring of health checks
|
||||
if err := etcdOptions.ApplyTo(&genericConfig.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
// override the RESTOptionsGetter to use the in memory storage options
|
||||
restOptionsGetter, err := apistore.NewRESTOptionsGetterMemory(etcdOptions.StorageConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
aggregatorConfig.GenericConfig.RESTOptionsGetter = restOptionsGetter
|
||||
|
||||
// prevent generic API server from installing the OpenAPI handler. Aggregator server has its own customized OpenAPI handler.
|
||||
genericConfig.SkipOpenAPIInstallation = true
|
||||
mergedResourceConfig, err := resourceconfig.MergeAPIResourceConfigs(aggregatorapiserver.DefaultAPIResourceConfigSource(), nil, aggregatorscheme.Scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
genericConfig.MergedResourceConfig = mergedResourceConfig
|
||||
|
||||
genericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
|
||||
|
||||
// These hooks use v1 informers, which are not available in the grafana aggregator.
|
||||
genericConfig.DisabledPostStartHooks = genericConfig.DisabledPostStartHooks.Insert("start-kube-aggregator-informers")
|
||||
genericConfig.DisabledPostStartHooks = genericConfig.DisabledPostStartHooks.Insert("apiservice-status-local-available-controller")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -3,11 +3,12 @@ package options
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
genericoptions "k8s.io/apiserver/pkg/server/options"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type OptionsProvider interface {
|
||||
@@ -21,7 +22,6 @@ const defaultEtcdPathPrefix = "/registry/grafana.app"
|
||||
type Options struct {
|
||||
RecommendedOptions *genericoptions.RecommendedOptions
|
||||
GrafanaAggregatorOptions *GrafanaAggregatorOptions
|
||||
KubeAggregatorOptions *KubeAggregatorOptions
|
||||
StorageOptions *StorageOptions
|
||||
ExtraOptions *ExtraOptions
|
||||
APIOptions []OptionsProvider
|
||||
@@ -31,7 +31,6 @@ func NewOptions(codec runtime.Codec) *Options {
|
||||
return &Options{
|
||||
RecommendedOptions: NewRecommendedOptions(codec),
|
||||
GrafanaAggregatorOptions: NewGrafanaAggregatorOptions(),
|
||||
KubeAggregatorOptions: NewAggregatorServerOptions(),
|
||||
StorageOptions: NewStorageOptions(),
|
||||
ExtraOptions: NewExtraOptions(),
|
||||
}
|
||||
@@ -40,7 +39,6 @@ func NewOptions(codec runtime.Codec) *Options {
|
||||
func (o *Options) AddFlags(fs *pflag.FlagSet) {
|
||||
o.RecommendedOptions.AddFlags(fs)
|
||||
o.GrafanaAggregatorOptions.AddFlags(fs)
|
||||
o.KubeAggregatorOptions.AddFlags(fs)
|
||||
o.StorageOptions.AddFlags(fs)
|
||||
o.ExtraOptions.AddFlags(fs)
|
||||
|
||||
@@ -62,10 +60,6 @@ func (o *Options) Validate() []error {
|
||||
return errs
|
||||
}
|
||||
|
||||
if errs := o.KubeAggregatorOptions.Validate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
if errs := o.RecommendedOptions.SecureServing.Validate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// NOTE: both dataplane aggregator and kubernetes aggregator (when enterprise is linked) have logic around
|
||||
// setting this RoundTripper as ready, however, kubernetes aggregator part is skipped naturally,
|
||||
// given it is invoked as part of the delegate chain headed by the dataplane aggregator, and not through
|
||||
// its own Run method.
|
||||
type RoundTripperFunc struct {
|
||||
Ready chan struct{}
|
||||
Fn func(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
func (f *RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if f.Fn == nil {
|
||||
<-f.Ready
|
||||
}
|
||||
return f.Fn(req)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
@@ -16,7 +15,6 @@ import (
|
||||
"k8s.io/apiserver/pkg/util/notfoundhandler"
|
||||
clientrest "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
|
||||
|
||||
"github.com/grafana/dskit/services"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
@@ -35,7 +33,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||
kubeaggregator "github.com/grafana/grafana/pkg/services/apiserver/aggregator"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/aggregatorrunner"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/auth/authenticator"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
@@ -49,6 +47,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -102,6 +101,7 @@ type service struct {
|
||||
restConfigProvider RestConfigProvider
|
||||
|
||||
buildHandlerChainFuncFromBuilders builder.BuildHandlerChainFuncFromBuilders
|
||||
aggregatorRunner aggregatorrunner.AggregatorRunner
|
||||
}
|
||||
|
||||
func ProvideService(
|
||||
@@ -122,6 +122,7 @@ func ProvideService(
|
||||
buildHandlerChainFuncFromBuilders builder.BuildHandlerChainFuncFromBuilders,
|
||||
eventualRestConfigProvider *eventualRestConfigProvider,
|
||||
reg prometheus.Registerer,
|
||||
aggregatorRunner aggregatorrunner.AggregatorRunner,
|
||||
) (*service, error) {
|
||||
scheme := builder.ProvideScheme()
|
||||
codecs := builder.ProvideCodecFactory(scheme)
|
||||
@@ -148,6 +149,7 @@ func ProvideService(
|
||||
unified: unified,
|
||||
restConfigProvider: restConfigProvider,
|
||||
buildHandlerChainFuncFromBuilders: buildHandlerChainFuncFromBuilders,
|
||||
aggregatorRunner: aggregatorRunner,
|
||||
}
|
||||
// This will be used when running as a dskit service
|
||||
service := services.NewBasicService(s.start, s.running, nil).WithName(modules.GrafanaAPIServer)
|
||||
@@ -234,7 +236,7 @@ func (s *service) start(ctx context.Context) error {
|
||||
groupVersions := make([]schema.GroupVersion, 0, len(builders))
|
||||
|
||||
// Install schemas
|
||||
for i, b := range builders {
|
||||
for _, b := range builders {
|
||||
gvs := builder.GetGroupVersions(b)
|
||||
groupVersions = append(groupVersions, gvs...)
|
||||
if len(gvs) == 0 {
|
||||
@@ -245,12 +247,7 @@ func (s *service) start(ctx context.Context) error {
|
||||
}
|
||||
pvs := s.scheme.PrioritizedVersionsForGroup(gvs[0].Group)
|
||||
|
||||
for j, gv := range pvs {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator) {
|
||||
// set the priority for the group+version
|
||||
kubeaggregator.APIVersionPriorities[gv] = kubeaggregator.Priority{Group: int32(15000 + i), Version: int32(len(pvs) - j)}
|
||||
}
|
||||
|
||||
for _, gv := range pvs {
|
||||
if a, ok := b.(builder.APIGroupAuthorizer); ok {
|
||||
auth := a.GetAuthorizer()
|
||||
if auth != nil {
|
||||
@@ -280,7 +277,7 @@ func (s *service) start(ctx context.Context) error {
|
||||
serverConfig.TracerProvider = s.tracing.GetTracerProvider()
|
||||
|
||||
// setup loopback transport for the aggregator server
|
||||
transport := &roundTripperFunc{ready: make(chan struct{})}
|
||||
transport := &grafanaapiserveroptions.RoundTripperFunc{Ready: make(chan struct{})}
|
||||
serverConfig.LoopbackClientConfig.Transport = transport
|
||||
serverConfig.LoopbackClientConfig.TLSClientConfig = clientrest.TLSClientConfig{}
|
||||
serverConfig.MaxRequestBodyBytes = MaxRequestBodyBytes
|
||||
@@ -341,27 +338,41 @@ func (s *service) start(ctx context.Context) error {
|
||||
s.options = o
|
||||
|
||||
delegate := server
|
||||
var aggregatorServer *aggregatorapiserver.APIAggregator
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator) {
|
||||
aggregatorServer, err = s.createKubeAggregator(serverConfig, server, s.metrics)
|
||||
|
||||
var runningServer *genericapiserver.GenericAPIServer
|
||||
isKubernetesAggregatorEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator)
|
||||
isDataplaneAggregatorEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagDataplaneAggregator)
|
||||
|
||||
if isKubernetesAggregatorEnabled {
|
||||
aggregatorServer, err := s.aggregatorRunner.Configure(s.options, serverConfig, delegate, s.scheme, builders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delegate = aggregatorServer.GenericAPIServer
|
||||
// we are running with KubernetesAggregator FT set to true but with enterprise unlinked, handle this gracefully
|
||||
if aggregatorServer != nil {
|
||||
if !isDataplaneAggregatorEnabled {
|
||||
runningServer, err = s.aggregatorRunner.Run(ctx, transport, s.stoppedCh)
|
||||
if err != nil {
|
||||
s.log.Error("aggregator runner failed to run", "error", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
delegate = aggregatorServer
|
||||
}
|
||||
} else {
|
||||
// even though the FT is set to true, enterprise isn't linked
|
||||
isKubernetesAggregatorEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
var runningServer *genericapiserver.GenericAPIServer
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagDataplaneAggregator) {
|
||||
if isDataplaneAggregatorEnabled {
|
||||
runningServer, err = s.startDataplaneAggregator(ctx, transport, serverConfig, delegate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator) {
|
||||
runningServer, err = s.startKubeAggregator(ctx, transport, aggregatorServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if !isDataplaneAggregatorEnabled && !isKubernetesAggregatorEnabled {
|
||||
runningServer, err = s.startCoreServer(ctx, transport, server)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -385,15 +396,15 @@ func (s *service) start(ctx context.Context) error {
|
||||
|
||||
func (s *service) startCoreServer(
|
||||
ctx context.Context,
|
||||
transport *roundTripperFunc,
|
||||
transport *grafanaapiserveroptions.RoundTripperFunc,
|
||||
server *genericapiserver.GenericAPIServer,
|
||||
) (*genericapiserver.GenericAPIServer, error) {
|
||||
// setup the loopback transport and signal that it's ready.
|
||||
// ignore the lint error because the response is passed directly to the client,
|
||||
// so the client will be responsible for closing the response body.
|
||||
// nolint:bodyclose
|
||||
transport.fn = grafanaresponsewriter.WrapHandler(server.Handler)
|
||||
close(transport.ready)
|
||||
transport.Fn = grafanaresponsewriter.WrapHandler(server.Handler)
|
||||
close(transport.Ready)
|
||||
|
||||
prepared := server.PrepareRun()
|
||||
go func() {
|
||||
@@ -405,7 +416,7 @@ func (s *service) startCoreServer(
|
||||
|
||||
func (s *service) startDataplaneAggregator(
|
||||
ctx context.Context,
|
||||
transport *roundTripperFunc,
|
||||
transport *grafanaapiserveroptions.RoundTripperFunc,
|
||||
serverConfig *genericapiserver.RecommendedConfig,
|
||||
delegate *genericapiserver.GenericAPIServer,
|
||||
) (*genericapiserver.GenericAPIServer, error) {
|
||||
@@ -436,8 +447,8 @@ func (s *service) startDataplaneAggregator(
|
||||
// ignore the lint error because the response is passed directly to the client,
|
||||
// so the client will be responsible for closing the response body.
|
||||
// nolint:bodyclose
|
||||
transport.fn = grafanaresponsewriter.WrapHandler(aggregatorServer.GenericAPIServer.Handler)
|
||||
close(transport.ready)
|
||||
transport.Fn = grafanaresponsewriter.WrapHandler(aggregatorServer.GenericAPIServer.Handler)
|
||||
close(transport.Ready)
|
||||
|
||||
prepared, err := aggregatorServer.PrepareRun()
|
||||
if err != nil {
|
||||
@@ -451,49 +462,10 @@ func (s *service) startDataplaneAggregator(
|
||||
return aggregatorServer.GenericAPIServer, nil
|
||||
}
|
||||
|
||||
func (s *service) createKubeAggregator(
|
||||
serverConfig *genericapiserver.RecommendedConfig,
|
||||
server *genericapiserver.GenericAPIServer,
|
||||
reg prometheus.Registerer,
|
||||
) (*aggregatorapiserver.APIAggregator, error) {
|
||||
namespaceMapper := request.GetNamespaceMapper(s.cfg)
|
||||
|
||||
aggregatorConfig, err := kubeaggregator.CreateAggregatorConfig(s.options, *serverConfig, namespaceMapper(1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kubeaggregator.CreateAggregatorServer(aggregatorConfig, server, reg)
|
||||
}
|
||||
|
||||
func (s *service) startKubeAggregator(
|
||||
ctx context.Context,
|
||||
transport *roundTripperFunc,
|
||||
aggregatorServer *aggregatorapiserver.APIAggregator,
|
||||
) (*genericapiserver.GenericAPIServer, error) {
|
||||
// setup the loopback transport for the aggregator server and signal that it's ready
|
||||
// ignore the lint error because the response is passed directly to the client,
|
||||
// so the client will be responsible for closing the response body.
|
||||
// nolint:bodyclose
|
||||
transport.fn = grafanaresponsewriter.WrapHandler(aggregatorServer.GenericAPIServer.Handler)
|
||||
close(transport.ready)
|
||||
|
||||
prepared, err := aggregatorServer.PrepareRun()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
s.stoppedCh <- prepared.Run(ctx)
|
||||
}()
|
||||
|
||||
return aggregatorServer.GenericAPIServer, nil
|
||||
}
|
||||
|
||||
func (s *service) GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Config {
|
||||
return &clientrest.Config{
|
||||
Transport: &roundTripperFunc{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
Transport: &grafanaapiserveroptions.RoundTripperFunc{
|
||||
Fn: func(req *http.Request) (*http.Response, error) {
|
||||
if err := s.AwaitRunning(req.Context()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -531,18 +503,6 @@ func ensureKubeConfig(restConfig *clientrest.Config, dir string) error {
|
||||
)
|
||||
}
|
||||
|
||||
type roundTripperFunc struct {
|
||||
ready chan struct{}
|
||||
fn func(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
func (f *roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if f.fn == nil {
|
||||
<-f.ready
|
||||
}
|
||||
return f.fn(req)
|
||||
}
|
||||
|
||||
type pluginContextProvider struct {
|
||||
pluginStore pluginstore.Store
|
||||
datasources datasource.ScopedPluginDatasourceProvider
|
||||
|
||||
@@ -875,6 +875,13 @@ var (
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
Name: "kubernetesAggregatorCapTokenAuth",
|
||||
Description: "Enable CAP token based authentication in grafana's embedded kube-aggregator",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
Name: "expressionParser",
|
||||
Description: "Enable new expression parser",
|
||||
|
||||
@@ -114,6 +114,7 @@ groupToNestedTableTransformation,GA,@grafana/dataviz-squad,false,false,true
|
||||
newPDFRendering,GA,@grafana/sharing-squad,false,false,false
|
||||
tlsMemcached,GA,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
kubernetesAggregatorCapTokenAuth,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
expressionParser,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
groupByVariable,experimental,@grafana/dashboards-squad,false,false,false
|
||||
scopeFilters,experimental,@grafana/dashboards-squad,false,false,false
|
||||
|
||||
|
@@ -467,6 +467,10 @@ const (
|
||||
// Enable grafana's embedded kube-aggregator
|
||||
FlagKubernetesAggregator = "kubernetesAggregator"
|
||||
|
||||
// FlagKubernetesAggregatorCapTokenAuth
|
||||
// Enable CAP token based authentication in grafana's embedded kube-aggregator
|
||||
FlagKubernetesAggregatorCapTokenAuth = "kubernetesAggregatorCapTokenAuth"
|
||||
|
||||
// FlagExpressionParser
|
||||
// Enable new expression parser
|
||||
FlagExpressionParser = "expressionParser"
|
||||
|
||||
@@ -1695,6 +1695,22 @@
|
||||
"requiresRestart": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesAggregatorCapTokenAuth",
|
||||
"resourceVersion": "1746996924159",
|
||||
"creationTimestamp": "2025-05-11T20:48:13Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-05-11 20:55:24.159856 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable CAP token based authentication in grafana's embedded kube-aggregator",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/grafana-app-platform-squad",
|
||||
"requiresRestart": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesClientDashboardsFolders",
|
||||
|
||||
@@ -120,7 +120,7 @@ require (
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect
|
||||
github.com/grafana/grafana-app-sdk v0.35.1 // indirect
|
||||
github.com/grafana/grafana-app-sdk/logging v0.35.1 // indirect
|
||||
|
||||
@@ -308,8 +308,7 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORR
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a h1:irEH0Abl6mKbkPx/xtmB5Wai4ipzEB6hGPNsLya/p9Y=
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5 h1:qu6+b9tY6E/CgKrLh8srjtvKOMCxdwk46jRqURjJ61s=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g=
|
||||
|
||||
@@ -6,15 +6,16 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
otgrpc "github.com/opentracing-contrib/go-grpc"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
"gocloud.dev/blob/fileblob"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
otgrpc "github.com/opentracing-contrib/go-grpc"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/dskit/flagext"
|
||||
"github.com/grafana/dskit/grpcclient"
|
||||
@@ -52,7 +53,7 @@ type clientMetrics struct {
|
||||
|
||||
// This adds a UnifiedStorage client into the wire dependency tree
|
||||
func ProvideUnifiedStorageClient(opts *Options, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics) (resource.ResourceClient, error) {
|
||||
// See: apiserver.ApplyGrafanaConfig(cfg, features, o)
|
||||
// See: apiserver.applyAPIServerConfig(cfg, features, o)
|
||||
apiserverCfg := opts.Cfg.SectionWithEnvOverrides("grafana-apiserver")
|
||||
client, err := newClient(options.StorageOptions{
|
||||
StorageType: options.StorageType(apiserverCfg.Key("storage_type").MustString(string(options.StorageTypeUnified))),
|
||||
|
||||
@@ -13,7 +13,7 @@ require (
|
||||
github.com/fullstorydev/grpchan v1.1.1
|
||||
github.com/go-jose/go-jose/v3 v3.0.4
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d
|
||||
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040
|
||||
github.com/grafana/grafana-app-sdk/logging v0.35.1
|
||||
|
||||
@@ -307,8 +307,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a h1:irEH0Abl6mKbkPx/xtmB5Wai4ipzEB6hGPNsLya/p9Y=
|
||||
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5 h1:qu6+b9tY6E/CgKrLh8srjtvKOMCxdwk46jRqURjJ61s=
|
||||
github.com/grafana/authlib v0.0.0-20250430134519-13c42d09f9f5/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM=
|
||||
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g=
|
||||
|
||||
Reference in New Issue
Block a user