Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/crds/core.kcp.io_logicalclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ spec:
- Initializing
- Ready
- Unavailable
- Inactive
- Terminating
- Deleting
type: string
Expand Down
1 change: 1 addition & 0 deletions config/crds/tenancy.kcp.io_workspaces.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ spec:
- Initializing
- Ready
- Unavailable
- Inactive
- Terminating
- Deleting
type: string
Expand Down
2 changes: 1 addition & 1 deletion config/root-phase0/apiexport-tenancy.kcp.io.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ spec:
crd: {}
- group: tenancy.kcp.io
name: workspaces
schema: v260428-d075f2d48.workspaces.tenancy.kcp.io
schema: v260522-2bf05df66.workspaces.tenancy.kcp.io
storage:
crd: {}
- group: tenancy.kcp.io
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: apis.kcp.io/v1alpha1
kind: APIResourceSchema
metadata:
name: v260428-d075f2d48.logicalclusters.core.kcp.io
name: v260522-2bf05df66.logicalclusters.core.kcp.io
spec:
group: core.kcp.io
names:
Expand Down Expand Up @@ -230,6 +230,7 @@ spec:
- Initializing
- Ready
- Unavailable
- Inactive
- Terminating
- Deleting
type: string
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: apis.kcp.io/v1alpha1
kind: APIResourceSchema
metadata:
name: v260428-d075f2d48.workspaces.tenancy.kcp.io
name: v260522-2bf05df66.workspaces.tenancy.kcp.io
spec:
group: tenancy.kcp.io
names:
Expand Down Expand Up @@ -295,6 +295,7 @@ spec:
- Initializing
- Ready
- Unavailable
- Inactive
- Terminating
- Deleting
type: string
Expand Down
5 changes: 5 additions & 0 deletions pkg/authorization/workspace_content_authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ func (a *workspaceContentAuthorizer) Authorize(ctx context.Context, attr authori
switch logicalCluster.Status.Phase {
case corev1alpha1.LogicalClusterPhaseInitializing,
corev1alpha1.LogicalClusterPhaseReady,
// Inactive: WithBlockInactiveLogicalClusters denies content access at the HTTP filter
// layer while permitting /openapi and the LogicalCluster resource itself, so the cluster
// can be reactivated. The authorizer must not also deny based on phase, otherwise the
// LC GET/UPDATE used to clear the inactive annotation is rejected.
corev1alpha1.LogicalClusterPhaseInactive,
// Terminating: registered terminator controllers are running and need to clean up content.
// Deleting: terminators are done; standard kube finalization (GC, namespace deletion,
// finalizer removal) still needs content access until the LogicalCluster object is gone.
Expand Down
69 changes: 69 additions & 0 deletions pkg/contextmanager/contextmanager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
Copyright 2026 The kcp Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package contextmanager

import (
"context"
"errors"
"fmt"
)

var errShutdown = errors.New("context manager shut down")

// Manager tracks contexts derived from the root context.
type Manager[K fmt.Stringer] struct {
rc *rootCtx
}

// New creates a new context manager.
func New[K fmt.Stringer](root context.Context) *Manager[K] {
return &Manager[K]{rc: newRootCtx(root)}
}

// Context returns a new context that is derived from parent.
// The context will be cancelled if either the manager's root context or the respective key context is cancelled.
func (m *Manager[K]) Context(parent context.Context, key K) (context.Context, context.CancelFunc) {
keyCtx, _ := m.rc.context(key.String())

ctx, cancel := context.WithCancelCause(parent)
stop := context.AfterFunc(keyCtx, func() {
cancel(context.Cause(keyCtx))
})

cleanup := func() {
stop()
cancel(nil)
}

return ctx, cleanup
}

// Cancel cancels the context for the given key with reason.
// If no context exists for the key a context will be created and cancelled.
func (m *Manager[K]) Cancel(key K, reason error) {
m.rc.cancel(key.String(), reason)
}

// Delete removes the entry for the given key, cancelling its context with reason.
func (m *Manager[K]) Delete(key K, reason error) {
m.rc.delete(key.String(), reason)
}

// Shutdown cancels the root context, which propagates to all contexts.
func (m *Manager[K]) Shutdown() {
m.rc.cancelAll(errShutdown)
}
18 changes: 18 additions & 0 deletions pkg/contextmanager/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
Copyright 2026 The kcp Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package contextmanager simulates multiple-parent contexts with sticky cancellation.
package contextmanager
110 changes: 110 additions & 0 deletions pkg/contextmanager/rootCtx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
Copyright 2026 The kcp Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package contextmanager

import (
"context"
"sync"

"golang.org/x/sync/singleflight"
)

// rootCtx abstracts the synchronisation of getting the shared keyed contexts.
type rootCtx struct {
root context.Context //nolint:containedctx
rootCancel context.CancelCauseFunc
// entries is the hot path quick lookup
entries sync.Map
// group is a singleflight group to synchronize slow path context
// creation
group singleflight.Group
// The mutex is to synchronize the slow path and context
// cancellation
lock sync.Mutex
}

type entry struct {
ctx context.Context //nolint:containedctx
cancel context.CancelCauseFunc
}

func newRootCtx(ctx context.Context) *rootCtx {
rc := new(rootCtx)
rc.root, rc.rootCancel = context.WithCancelCause(ctx)
return rc
}

func (rc *rootCtx) context(key string) (context.Context, context.CancelCauseFunc) {
stored, loaded := rc.entries.Load(key)
if loaded {
return stored.(*entry).ctx, stored.(*entry).cancel
}

// ignoring the error as the .Do only returns the error from the
// passed function
built, _, _ := rc.group.Do(key, func() (any, error) {
// locking to synchronize with the cancelFor
rc.lock.Lock()
defer rc.lock.Unlock()

// Double check the stored entries
stored, loaded := rc.entries.Load(key)
if loaded {
return stored, nil
}

// Create and store the entry
ctx, cancel := context.WithCancelCause(rc.root)
e := &entry{ctx: ctx, cancel: cancel}
rc.entries.Store(key, e)
return e, nil
})

return built.(*entry).ctx, built.(*entry).cancel
}

func (rc *rootCtx) cancel(key string, reason error) {
rc.lock.Lock()
defer rc.lock.Unlock()

if stored, loaded := rc.entries.Load(key); loaded {
stored.(*entry).cancel(reason)
return
}

// store a pre-cancelled context so following calls don't get
// a fresh context.
ctx, cancel := context.WithCancelCause(rc.root)
cancel(reason)
e := &entry{ctx: ctx, cancel: cancel}
rc.entries.Store(key, e)
}

func (rc *rootCtx) delete(key string, reason error) {
rc.lock.Lock()
defer rc.lock.Unlock()

stored, loaded := rc.entries.LoadAndDelete(key)
if !loaded {
return
}
stored.(*entry).cancel(reason)
}

func (rc *rootCtx) cancelAll(reason error) {
rc.rootCancel(reason)
}
Loading