Skip to content

Commit e5b96f4

Browse files
committed
feat: cluster context storage rewrite
1 parent 64f63cb commit e5b96f4

36 files changed

Lines changed: 2731 additions & 438 deletions

README.md

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -147,29 +147,14 @@ keyFunc := func(obj runtime.Object) (string, error) {
147147
}
148148
```
149149

150-
#### Admission: server-owned cluster label
151-
We stamp a server-owned cluster label on persisted objects and validate it on
152-
create/update to prevent cross-cluster writes and support watchcache keying.
150+
#### Admission: internal cluster identity
151+
We store cluster identity in managed fields and validate it on create/update
152+
to prevent cross-cluster writes and support watchcache keying.
153153

154154
```go
155-
lbls := accessor.GetLabels()
156-
if lbls == nil {
157-
lbls = map[string]string{}
158-
}
159-
key := m.Options.ClusterAnnotationKey
160-
if key == "" {
161-
key = mcv1.DefaultClusterAnnotation
162-
}
163-
lbls[key] = cid
164-
accessor.SetLabels(lbls)
165-
```
166-
Note: the `ClusterAnnotationKey` fallback to `DefaultClusterAnnotation` is
167-
temporary and not ideal. It exists to preserve compatibility while we migrate
168-
callers to an explicit label key; avoid relying on this fallback long term.
169-
170-
```go
171-
if cid := acc.GetLabels()[key]; cid != reqCID {
172-
return fmt.Errorf("cluster label %q=%q must match request cluster %q", key, cid, reqCID)
155+
mc.SetObjectClusterIdentity(obj, reqCID)
156+
if cid := mc.ObjectClusterIdentity(obj); cid != reqCID {
157+
return fmt.Errorf("cluster identity %q must match request cluster %q", cid, reqCID)
173158
}
174159
```
175160

cmd/apiserver/app/config.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
146146
})
147147
genericConfig.BuildHandlerChainFunc = func(h http.Handler, conf *server.Config) http.Handler {
148148
ex := mc.PathExtractor{PathPrefix: mcOpts.PathPrefix, ControlPlaneSegment: mcOpts.ControlPlaneSegment}
149-
base := server.DefaultBuildHandlerChain(h, conf)
149+
base := withVersionOverride(server.DefaultBuildHandlerChain(h, conf))
150150
dispatch := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151151
cid, _, _ := mc.FromContext(r.Context())
152152
if cid != "" && cid != mcOpts.DefaultCluster && crdRuntimeMgr != nil {
@@ -179,6 +179,8 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
179179
BaseLoopbackClientConfig: genericConfig.LoopbackClientConfig,
180180
PathPrefix: mcOpts.PathPrefix,
181181
ControlPlaneSegment: mcOpts.ControlPlaneSegment,
182+
EtcdPrefix: storageFactory.StorageConfig.Prefix,
183+
EtcdTransport: storageFactory.StorageConfig.Transport,
182184
Authentication: opts.Authentication,
183185
Authorization: opts.Authorization,
184186
EgressSelector: genericConfig.EgressSelector,
@@ -190,7 +192,9 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
190192
genericConfig.Authentication.Authenticator = mcauth.NewClusterAuthenticator(mcOpts.DefaultCluster, genericConfig.Authentication.Authenticator, authManager)
191193
}
192194
if genericConfig.Authorization.Authorizer != nil {
193-
clusterAuthorizer := mcauth.NewClusterAuthorizer(mcOpts.DefaultCluster, genericConfig.Authorization.Authorizer, genericConfig.RuleResolver, authManager)
195+
// Route root and tenant authorization through the same multicluster
196+
// manager-backed path to avoid root-only stale lister divergence.
197+
clusterAuthorizer := mcauth.NewClusterAuthorizer(mcOpts.DefaultCluster, nil, nil, authManager)
194198
genericConfig.Authorization.Authorizer = clusterAuthorizer
195199
genericConfig.RuleResolver = clusterAuthorizer
196200
}
@@ -314,6 +318,9 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
314318
if apiExtensions.GenericConfig.RESTOptionsGetter != nil {
315319
apiExtensions.GenericConfig.RESTOptionsGetter = decorateRESTOptionsGetter("apiextensions", apiExtensions.GenericConfig.RESTOptionsGetter, mcOpts)
316320
}
321+
if apiExtensions.ExtraConfig.CRDRESTOptionsGetter != nil {
322+
apiExtensions.ExtraConfig.CRDRESTOptionsGetter = decorateRESTOptionsGetter("apiextensions-crd", apiExtensions.ExtraConfig.CRDRESTOptionsGetter, mcOpts)
323+
}
317324
apiExtensionsClientPool := mc.NewAPIExtensionsClientPool(apiExtensions.GenericConfig.LoopbackClientConfig, mcOpts.PathPrefix, mcOpts.ControlPlaneSegment)
318325
apiExtensionsInformerPool := mc.NewAPIExtensionsInformerPoolFromClientPool(apiExtensionsClientPool, 0, genericConfig.DrainedNotify())
319326
crdRuntimeMgr = mcbootstrap.NewCRDRuntimeManager(mcbootstrap.CRDRuntimeManagerOptions{
@@ -322,6 +329,8 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
322329
PathPrefix: mcOpts.PathPrefix,
323330
ControlPlaneSegment: mcOpts.ControlPlaneSegment,
324331
DefaultCluster: mcOpts.DefaultCluster,
332+
EtcdPrefix: storageFactory.StorageConfig.Prefix,
333+
EtcdTransport: storageFactory.StorageConfig.Transport,
325334
})
326335
setCRDRequestResolvers(apiExtensions, crdRuntimeMgr.CRDGetterForRequest, crdRuntimeMgr.CRDListerForRequest)
327336
crdController := mcbootstrap.NewMulticlusterCRDController(crdRuntimeMgr, mcOpts.DefaultCluster)
@@ -364,7 +373,7 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
364373
// Ensure CRDs are also routed through the multicluster handler
365374
apiExtensions.GenericConfig.BuildHandlerChainFunc = func(h http.Handler, conf *server.Config) http.Handler {
366375
ex := mc.PathExtractor{PathPrefix: mcOpts.PathPrefix, ControlPlaneSegment: mcOpts.ControlPlaneSegment}
367-
base := server.DefaultBuildHandlerChain(h, conf)
376+
base := withVersionOverride(server.DefaultBuildHandlerChain(h, conf))
368377
dispatch := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
369378
cid, _, _ := mc.FromContext(r.Context())
370379
if cid != "" && cid != mcOpts.DefaultCluster {
@@ -381,11 +390,11 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
381390
mut := mca.NewMutating(mcOpts)
382391
val := mca.NewValidating(mcOpts)
383392
base := apiExtensions.GenericConfig.AdmissionControl
384-
chain := []admission.Interface{mut, mcNamespaceLifecycle, mcMutatingWebhook}
393+
chain := []admission.Interface{mut, mcNamespaceLifecycle}
385394
if base != nil {
386395
chain = append(chain, base)
387396
}
388-
chain = append(chain, mcValidatingWebhook, val)
397+
chain = append(chain, val)
389398
apiExtensions.GenericConfig.AdmissionControl = admission.NewChainHandler(chain...)
390399
}
391400
c.ApiExtensions = apiExtensions
@@ -400,7 +409,7 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
400409
// Ensure aggregator also receives multicluster routing
401410
aggregator.GenericConfig.BuildHandlerChainFunc = func(h http.Handler, conf *server.Config) http.Handler {
402411
ex := mc.PathExtractor{PathPrefix: mcOpts.PathPrefix, ControlPlaneSegment: mcOpts.ControlPlaneSegment}
403-
base := server.DefaultBuildHandlerChain(h, conf)
412+
base := withVersionOverride(server.DefaultBuildHandlerChain(h, conf))
404413
dispatch := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
405414
cid, _, _ := mc.FromContext(r.Context())
406415
if cid != "" && cid != mcOpts.DefaultCluster && crdRuntimeMgr != nil {
@@ -417,11 +426,11 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
417426
mut := mca.NewMutating(mcOpts)
418427
val := mca.NewValidating(mcOpts)
419428
base := aggregator.GenericConfig.AdmissionControl
420-
chain := []admission.Interface{mut, mcNamespaceLifecycle, mcMutatingWebhook}
429+
chain := []admission.Interface{mut, mcNamespaceLifecycle}
421430
if base != nil {
422431
chain = append(chain, base)
423432
}
424-
chain = append(chain, mcValidatingWebhook, val)
433+
chain = append(chain, val)
425434
aggregator.GenericConfig.AdmissionControl = admission.NewChainHandler(chain...)
426435
}
427436
c.Aggregator = aggregator

cmd/apiserver/app/server.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ import (
4949
logsapi "k8s.io/component-base/logs/api/v1"
5050
_ "k8s.io/component-base/metrics/prometheus/workqueue"
5151
"k8s.io/component-base/term"
52-
utilversion "k8s.io/component-base/version"
5352
"k8s.io/component-base/version/verflag"
5453
"k8s.io/component-base/zpages/flagz"
5554
"k8s.io/klog/v2"
@@ -148,7 +147,7 @@ cluster's shared state through which all other components interact.`,
148147
// Run runs the specified APIServer. This should never exit.
149148
func Run(ctx context.Context, opts options.CompletedOptions) error {
150149
// To help debugging, immediately log version
151-
klog.Infof("Version: %+v", utilversion.Get())
150+
klog.Infof("Version: %+v", normalizedServerVersion())
152151

153152
klog.InfoS("Golang settings", "GOGC", os.Getenv("GOGC"), "GOMAXPROCS", os.Getenv("GOMAXPROCS"), "GOTRACEBACK", os.Getenv("GOTRACEBACK"))
154153

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package app
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"strings"
7+
8+
apimachineryversion "k8s.io/apimachinery/pkg/version"
9+
utilversion "k8s.io/component-base/version"
10+
)
11+
12+
const gitVersionArchivePlaceholder = "v0.0.0-master+$Format:%H$"
13+
14+
func withVersionOverride(next http.Handler) http.Handler {
15+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16+
if isVersionPath(r.URL.Path) {
17+
info := normalizedServerVersion()
18+
w.Header().Set("Content-Type", "application/json")
19+
_ = json.NewEncoder(w).Encode(info)
20+
return
21+
}
22+
next.ServeHTTP(w, r)
23+
})
24+
}
25+
26+
func isVersionPath(path string) bool {
27+
if path == "/version" || path == "/version/" {
28+
return true
29+
}
30+
trimmed := strings.Trim(path, "/")
31+
parts := strings.Split(trimmed, "/")
32+
return len(parts) == 3 && parts[0] == "clusters" && parts[2] == "version"
33+
}
34+
35+
func normalizedServerVersion() apimachineryversion.Info {
36+
info := utilversion.Get()
37+
if info.GitVersion != gitVersionArchivePlaceholder {
38+
return info
39+
}
40+
41+
parts := strings.SplitN(utilversion.DefaultKubeBinaryVersion, ".", 2)
42+
if len(parts) != 2 {
43+
return info
44+
}
45+
46+
info.Major = parts[0]
47+
info.Minor = parts[1]
48+
info.GitVersion = "v" + utilversion.DefaultKubeBinaryVersion + ".0"
49+
return info
50+
}
51+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package app
2+
3+
import "testing"
4+
5+
func TestIsVersionPath(t *testing.T) {
6+
tests := []struct {
7+
path string
8+
want bool
9+
}{
10+
{path: "/version", want: true},
11+
{path: "/version/", want: true},
12+
{path: "/clusters/kpt-1/version", want: true},
13+
{path: "/clusters/kpt-1/version/", want: true},
14+
{path: "/apis", want: false},
15+
}
16+
17+
for _, tt := range tests {
18+
if got := isVersionPath(tt.path); got != tt.want {
19+
t.Fatalf("isVersionPath(%q)=%v, want %v", tt.path, got, tt.want)
20+
}
21+
}
22+
}
23+
24+
func TestNormalizedServerVersion(t *testing.T) {
25+
info := normalizedServerVersion()
26+
if info.GitVersion == gitVersionArchivePlaceholder {
27+
t.Fatalf("expected non-placeholder git version, got %q", info.GitVersion)
28+
}
29+
}
30+

0 commit comments

Comments
 (0)