diff --git a/pkg/server/config.go b/pkg/server/config.go index 0f43135db22..abe59a24b79 100644 --- a/pkg/server/config.go +++ b/pkg/server/config.go @@ -445,6 +445,7 @@ func NewConfig(opts kcpserveroptions.CompletedOptions) (*Config, error) { apiHandler = filters.WithWarningRecorder(apiHandler) apiHandler = kcpfilters.WithAuditEventClusterAnnotation(apiHandler) + apiHandler = kcpfilters.WithBlockInactiveLogicalClusters(apiHandler, c.KcpSharedInformerFactory.Core().V1alpha1().LogicalClusters()) // Add a mux before the chain, for other handlers with their own handler chain to hook in. For example, when // the virtual workspace server is running as part of kcp, it registers /services with the mux so it can handle diff --git a/pkg/server/filters/filters.go b/pkg/server/filters/filters.go index 0939645c0d2..0962e99def5 100644 --- a/pkg/server/filters/filters.go +++ b/pkg/server/filters/filters.go @@ -18,6 +18,7 @@ package filters import ( "context" + "errors" "fmt" "net/http" "net/url" @@ -35,6 +36,9 @@ import ( kaudit "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/request" + + corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" + informersv1alpha1 "github.com/kcp-dev/kcp/sdk/client/informers/externalversions/core/v1alpha1" ) type ( @@ -44,6 +48,10 @@ type ( const ( workspaceAnnotation = "tenancy.kcp.io/workspace" + // inactiveAnnotation is the annotation denoting a logical cluster should be + // deemed unreachable. + inactiveAnnotation = "internal.kcp.io/inactive" + // clusterKey is the context key for the request namespace. acceptHeaderContextKey acceptHeaderContextKeyType = iota ) @@ -78,6 +86,49 @@ func WithAuditEventClusterAnnotation(handler http.Handler) http.HandlerFunc { }) } +// WithBlockInactiveLogicalClusters ensures that any requests to logical +// clusters marked inactive are rejected. +func WithBlockInactiveLogicalClusters(handler http.Handler, kcpClusterClient informersv1alpha1.LogicalClusterClusterInformer) http.HandlerFunc { + allowedPathPrefixes := []string{ + "/openapi", + "/apis/core.kcp.io/v1alpha1/logicalclusters", + } + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, newURL, _, err := ClusterPathFromAndStrip(req) + if err != nil { + responsewriters.InternalError(w, req, err) + return + } + + isException := false + for _, prefix := range allowedPathPrefixes { + if strings.HasPrefix(newURL.String(), prefix) { + isException = true + } + } + + cluster := request.ClusterFrom(req.Context()) + if cluster != nil && !cluster.Name.Empty() && !isException { + logicalCluster, err := kcpClusterClient.Cluster(cluster.Name).Lister().Get(corev1alpha1.LogicalClusterName) + if err == nil { + if ann, ok := logicalCluster.ObjectMeta.Annotations[inactiveAnnotation]; ok && ann == "true" { + responsewriters.ErrorNegotiated( + apierrors.NewForbidden(corev1alpha1.Resource("logicalclusters"), cluster.Name.String(), errors.New("logical cluster is marked inactive")), + errorCodecs, schema.GroupVersion{}, w, req, + ) + return + } + } else if !apierrors.IsNotFound(err) { + responsewriters.InternalError(w, req, err) + return + } + } + + handler.ServeHTTP(w, req) + }) +} + // WithClusterScope reads a cluster name from the URL path and puts it into the context. // It also trims "/clusters/" prefix from the URL. func WithClusterScope(apiHandler http.Handler) http.HandlerFunc {