Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
f4ce3f1
chore: migrated to multi-cluster provider
vertex451 Aug 29, 2025
4999ab2
pulled main
vertex451 Sep 5, 2025
82ce421
addressed comments
vertex451 Sep 5, 2025
70e5a05
fixed kcp gateway connection
vertex451 Sep 19, 2025
c04cab2
removed http2 workaround
vertex451 Sep 19, 2025
b5a328d
linter
vertex451 Sep 19, 2025
903e39a
cleanup
vertex451 Sep 19, 2025
e84c1f7
removed tls
vertex451 Sep 19, 2025
9919cf0
iterate
vertex451 Sep 19, 2025
3f5976f
fix discovery check
vertex451 Sep 22, 2025
fea6b34
fixed virtual workspace
vertex451 Sep 22, 2025
b8f3ade
dynamic virtual workspace resolution
vertex451 Sep 23, 2025
1be4f83
made app exit on virtual-ws failure
vertex451 Sep 23, 2025
0c9ac2d
incapsulated clusterNameCtxKey
vertex451 Sep 23, 2025
1aa7ca0
added dedicated helper for ws path
vertex451 Sep 23, 2025
a5efb28
pulled main
vertex451 Sep 24, 2025
36eff27
centralized context keys
vertex451 Sep 24, 2025
907f251
removed redundant wrappers
vertex451 Sep 24, 2025
c7193e2
improved logging in ClusterPathResolver
vertex451 Sep 24, 2025
b366f2b
made clusterPathResolver as a KCPManager field
vertex451 Sep 24, 2025
98ab379
removed hardcoded fallback in clusterpath
vertex451 Sep 24, 2025
70b65c2
iterate
vertex451 Sep 24, 2025
ad6be7a
iterate
vertex451 Sep 24, 2025
e9b4c33
cleanup
vertex451 Sep 25, 2025
5c4741d
consistent error handling
vertex451 Sep 25, 2025
e3b5705
add validation of virtual ws fields
vertex451 Sep 25, 2025
7d6d82c
differentiate between shutdown reasons
vertex451 Sep 25, 2025
330ffe4
corrected virtual ws patter check
vertex451 Sep 25, 2025
282ef8b
exponential backoff
vertex451 Sep 25, 2025
5b62aa1
removed deprecated alias
vertex451 Sep 25, 2025
3ab0b6e
iterate
vertex451 Sep 25, 2025
3225d75
pulled main
vertex451 Sep 26, 2025
d01584a
improve reconciler coversage
vertex451 Sep 26, 2025
bcc07f3
coverage
vertex451 Sep 26, 2025
988e7b7
scalars test
vertex451 Sep 26, 2025
b808fd2
more tests
vertex451 Sep 26, 2025
1869a61
addressed scalar bug
vertex451 Sep 29, 2025
e8beffd
Merge branch 'main' of github.com:platform-mesh/kubernetes-graphql-ga…
vertex451 Sep 29, 2025
779f42c
iterate
vertex451 Sep 29, 2025
f6bab1c
replaced log.Error with log.Fatal
vertex451 Oct 2, 2025
97cc1c0
replaced context.Background with t.Context() in tests
vertex451 Oct 2, 2025
4494be2
decomposed isDiscoveryRequest logic
vertex451 Oct 2, 2025
409185a
removed redundant url parsing
vertex451 Oct 2, 2025
662bcef
normal shutdown in case of context cancelation
vertex451 Oct 2, 2025
e695399
eliminated false positive in worksapce detection
vertex451 Oct 2, 2025
73cc293
added type asser
vertex451 Oct 2, 2025
00c6e87
fix bug in ws
vertex451 Oct 2, 2025
ffa9ba8
Merge branch 'main' of github.com:platform-mesh/kubernetes-graphql-ga…
vertex451 Oct 6, 2025
f664fa9
addressed comments
vertex451 Oct 6, 2025
b1b9c7d
used er.Go() in gateway cmd
vertex451 Oct 6, 2025
9c8652a
pulled main
vertex451 Oct 20, 2025
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
8 changes: 4 additions & 4 deletions cmd/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,21 +107,21 @@ var listenCmd = &cobra.Command{
// Create the appropriate reconciler based on configuration
var reconcilerInstance reconciler.CustomReconciler
if appCfg.EnableKcp {
kcpReconciler, err := kcp.NewKCPReconciler(appCfg, reconcilerOpts, log)
kcpManager, err := kcp.NewKCPManager(appCfg, reconcilerOpts, log)
if err != nil {
log.Fatal().Err(err).Msg("unable to create KCP reconciler")
log.Fatal().Err(err).Msg("unable to create KCP manager")
}

// Start virtual workspace watching if path is configured
if appCfg.Listener.VirtualWorkspacesConfigPath != "" {
go func() {
if err := kcpReconciler.StartVirtualWorkspaceWatching(ctx, appCfg.Listener.VirtualWorkspacesConfigPath); err != nil {
if err := kcpManager.StartVirtualWorkspaceWatching(ctx, appCfg.Listener.VirtualWorkspacesConfigPath); err != nil {
log.Fatal().Err(err).Msg("failed to start virtual workspace watching")
}
}()
}

reconcilerInstance = kcpReconciler
reconcilerInstance = kcpManager
} else {
ioHandler, err := workspacefile.NewIOHandler(appCfg.OpenApiDefinitionsPath)
if err != nil {
Expand Down
45 changes: 43 additions & 2 deletions docs/virtual-workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,21 @@ virtualWorkspaces:
- name: example
url: https://192.168.1.118:6443/services/apiexport/root/configmaps-view
kubeconfig: PATH_TO_KCP_KUBECONFIG
- name: another-service
targetWorkspace: root:orgs:default # Explicit full workspace path
- name: production-service
url: https://your-kcp-server:6443/services/apiexport/root/your-export
kubeconfig: PATH_TO_KCP_KUBECONFIG
targetWorkspace: root:orgs:production # Different organization
- name: team-service
url: https://your-kcp-server:6443/services/apiexport/root/team-export
kubeconfig: PATH_TO_KCP_KUBECONFIG
targetWorkspace: root:orgs:team-a # Team-specific organization
- name: flexible-service
url: https://your-kcp-server:6443/services/apiexport/root/flexible-export
kubeconfig: PATH_TO_KCP_KUBECONFIG
# targetWorkspace not specified - will dynamically use default configuration
# If gateway-url-default-kcp-workspace="production", this will resolve to "root:orgs:production"
# If gateway-url-default-kcp-workspace="root:orgs:staging", this will use "root:orgs:staging" as-is
```

### Configuration Options
Expand All @@ -22,15 +34,44 @@ virtualWorkspaces:
- `name`: Unique identifier for the virtual workspace (used in URL paths)
- `url`: Full URL to the virtual workspace or API export
- `kubeconfig`: path to kcp kubeconfig
- `targetWorkspace`: Optional target workspace path (e.g., "root:orgs:default", "root:orgs:production", "root:orgs:team-a")
- If not specified, uses the `gateway-url-default-kcp-workspace` and `gateway-url-kcp-workspace-pattern` configuration values
- If the default workspace contains ":", it's used as-is (e.g., "root:orgs:production")
- If the default workspace is just an organization name, it's inserted into the workspace pattern
- Default pattern: "root:orgs:{org}" where {org} is replaced with the organization name
- Allows different virtual workspaces to target different organizations or workspace hierarchies dynamically

## Configuration Options

### Global Configuration

## Environment Variables
The following environment variables or configuration options control the default workspace resolution:

- `GATEWAY_URL_DEFAULT_KCP_WORKSPACE` (default: "default"): The default organization name
- `GATEWAY_URL_KCP_WORKSPACE_PATTERN` (default: "root:orgs:{org}"): The pattern for building workspace paths

### Environment Variables

Set the configuration path using:

```bash
export VIRTUAL_WORKSPACES_CONFIG_PATH="./bin/virtual-workspaces/config.yaml"
```

Set the default organization:

```bash
export GATEWAY_URL_DEFAULT_KCP_WORKSPACE="production"
```

Customize the workspace pattern (for different hierarchies):

```bash
export GATEWAY_URL_KCP_WORKSPACE_PATTERN="root:organizations:{org}"
# or for a flat structure:
export GATEWAY_URL_KCP_WORKSPACE_PATTERN="root:{org}"
```

## URL Pattern

Virtual workspaces are accessible through the gateway using the following URL pattern:
Expand Down
3 changes: 3 additions & 0 deletions gateway/manager/roundtripper/roundtripper.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ func isDiscoveryRequest(req *http.Request) bool {
if len(parts) >= 5 && parts[0] == "services" && parts[2] == "clusters" {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For such if else pattern you can also consider using a switch statement for better readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decomposed it into a separate function and add examples in comments for easier tracking.

// Handle virtual workspace prefixes first: /services/<service>/clusters/<workspace>/api
parts = parts[4:] // Remove /services/<service>/clusters/<workspace> prefix
} else if len(parts) >= 3 && parts[0] == "services" {
// Handle APIExport virtual workspace pattern: /services/<service>/api
parts = parts[2:] // Remove /services/<service> prefix
} else if len(parts) >= 3 && parts[0] == "clusters" {
// Handle KCP workspace prefixes: /clusters/<workspace>/api
parts = parts[2:] // Remove /clusters/<workspace> prefix
Expand Down
9 changes: 2 additions & 7 deletions gateway/manager/targetcluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/platform-mesh/golang-commons/logger"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/kcp"

"github.com/platform-mesh/kubernetes-graphql-gateway/common/auth"
appConfig "github.com/platform-mesh/kubernetes-graphql-gateway/common/config"
Expand Down Expand Up @@ -120,12 +119,8 @@ func (tc *TargetCluster) connect(appCfg appConfig.Config, metadata *ClusterMetad
})
}

// Create client - use KCP-aware client only for KCP mode, standard client otherwise
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multicluster-runtime uses standard client.WithWatch internally for each cluster it manages.
So no need to differentiate between KCP and non KCP modes here

if appCfg.EnableKcp {
tc.client, err = kcp.NewClusterAwareClientWithWatch(tc.restCfg, client.Options{})
} else {
tc.client, err = client.NewWithWatch(tc.restCfg, client.Options{})
}
// multicluster-runtime uses standard client.WithWatch internally for each cluster it manages.
tc.client, err = client.NewWithWatch(tc.restCfg, client.Options{})
if err != nil {
return fmt.Errorf("failed to create cluster client: %w", err)
}
Expand Down
13 changes: 8 additions & 5 deletions gateway/manager/targetcluster/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ import (
"github.com/graphql-go/graphql"
"github.com/graphql-go/handler"
"github.com/kcp-dev/logicalcluster/v3"
"sigs.k8s.io/controller-runtime/pkg/kontext"

"github.com/platform-mesh/golang-commons/logger"

appConfig "github.com/platform-mesh/kubernetes-graphql-gateway/common/config"
"github.com/platform-mesh/kubernetes-graphql-gateway/gateway/manager/roundtripper"
)

// LogicalClusterKey is the context key for storing logical cluster information
// Using logicalcluster.Name as the key type to be compatible with KCP ecosystem
type LogicalClusterKey = logicalcluster.Name

// GraphQLHandler wraps a GraphQL schema and HTTP handler
type GraphQLHandler struct {
Schema *graphql.Schema
Expand Down Expand Up @@ -57,14 +60,14 @@ func (s *GraphQLServer) CreateHandler(schema *graphql.Schema) *GraphQLHandler {
// SetContexts sets the required contexts for KCP and authentication
func SetContexts(r *http.Request, workspace, token string, enableKcp bool) *http.Request {
if enableKcp {
// For virtual workspaces, use the KCP workspace from the request context if available
// This allows the URL to specify the actual KCP workspace (e.g., root, root:orgs)
// while keeping the file mapping based on the virtual workspace name
// For virtual workspaces, the multicluster-runtime will handle cluster context
// We just need to store the workspace name in the context for potential future use
kcpWorkspaceName := workspace
if kcpWorkspace, ok := r.Context().Value(kcpWorkspaceKey).(string); ok && kcpWorkspace != "" {
kcpWorkspaceName = kcpWorkspace
}
r = r.WithContext(kontext.WithCluster(r.Context(), logicalcluster.Name(kcpWorkspaceName)))
// Store the logical cluster name in context using logicalcluster.Name as key
r = r.WithContext(context.WithValue(r.Context(), LogicalClusterKey("cluster"), logicalcluster.Name(kcpWorkspaceName)))
}
return r.WithContext(context.WithValue(r.Context(), roundtripper.TokenKey{}, token))
}
Expand Down
5 changes: 2 additions & 3 deletions gateway/manager/targetcluster/graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

"github.com/graphql-go/graphql"
"github.com/kcp-dev/logicalcluster/v3"
"sigs.k8s.io/controller-runtime/pkg/kontext"

"github.com/platform-mesh/golang-commons/logger/testlogger"
"github.com/platform-mesh/kubernetes-graphql-gateway/common"
Expand Down Expand Up @@ -197,8 +196,8 @@ func TestSetContexts(t *testing.T) {

// Check KCP context
if tt.expectKcp {
clusterFromCtx, _ := kontext.ClusterFrom(result.Context())
if clusterFromCtx != logicalcluster.Name(tt.workspace) {
clusterFromCtx, ok := result.Context().Value(targetcluster.LogicalClusterKey("cluster")).(logicalcluster.Name)
if !ok || clusterFromCtx != logicalcluster.Name(tt.workspace) {
t.Errorf("expected cluster %q in context, got %q", tt.workspace, clusterFromCtx)
}
}
Expand Down
65 changes: 36 additions & 29 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ module github.com/platform-mesh/kubernetes-graphql-gateway
go 1.24.3

replace (
github.com/google/cel-go => github.com/google/cel-go v0.26.1
// this PR introduces newer version of graphiQL that supports headers
// https://github.com/graphql-go/handler/pull/93
github.com/graphql-go/handler => github.com/vertex451/handler v0.0.0-20250124125145-ed328e3cf42a

// Pin versions to match multicluster-provider requirements
golang.org/x/net => golang.org/x/net v0.40.0
k8s.io/api => k8s.io/api v0.33.3
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.33.3
k8s.io/apimachinery => k8s.io/apimachinery v0.33.3
k8s.io/client-go => k8s.io/client-go v0.32.4
sigs.k8s.io/controller-runtime => github.com/kcp-dev/controller-runtime v0.19.0-kcp.1.0.20250129100209-5eaf4c7b6056
k8s.io/apiserver => k8s.io/apiserver v0.33.3
k8s.io/client-go => k8s.io/client-go v0.33.3
k8s.io/component-base => k8s.io/component-base v0.33.3
k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911
sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.21.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to pin it? is there any way to use the wanted version? Otherwise we still have the same dependency pain as before.

What forces us to use controller-runtime at all? I though we might be able to get rid of this dependency

Copy link
Member

@embik embik Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multicluster-runtime has a dependency on controller-runtime since it's an "addon", not a standalone project, but I agree with this being doable without replace directives. You probably just have to downgrade the k8s.io packages to be compatible with v0.21.0 of controller-runtime (currently I think controller-runtime gets upgraded because of k8s.io packages in go.mod being too new).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it - makes sense. Let's try and make it work without the replace then 💪

Copy link
Contributor Author

@vertex451 vertex451 Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed replace lines

)

require (
Expand All @@ -24,25 +30,27 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/kcp-dev/kcp/sdk v0.28.1
github.com/kcp-dev/logicalcluster/v3 v3.0.5
github.com/kcp-dev/multicluster-provider v0.0.0-20250827085327-2b5ca378b7b4
github.com/pkg/errors v0.9.1
github.com/platform-mesh/account-operator v0.1.24
github.com/platform-mesh/golang-commons v0.1.24
github.com/prometheus/client_golang v1.23.0
github.com/platform-mesh/account-operator v0.1.28
github.com/platform-mesh/golang-commons v0.1.29
github.com/prometheus/client_golang v1.23.2
github.com/rs/zerolog v1.34.0
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
golang.org/x/text v0.28.0
golang.org/x/text v0.29.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.33.3
k8s.io/apiextensions-apiserver v0.33.3
k8s.io/apimachinery v0.33.3
k8s.io/client-go v0.33.3
k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911
sigs.k8s.io/controller-runtime v0.22.0
sigs.k8s.io/controller-runtime v0.22.1
sigs.k8s.io/multicluster-runtime v0.21.0-alpha.9
)

require (
Expand All @@ -54,10 +62,10 @@ require (
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/getsentry/sentry-go v0.35.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
Expand All @@ -69,35 +77,34 @@ require (
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.26.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20250512171935-ebb573a40077 // indirect
github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20250728122101-adbf20db3e51 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/gomega v1.36.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
Expand All @@ -110,16 +117,16 @@ require (
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/oauth2 v0.31.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/time v0.11.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
Expand All @@ -129,8 +136,8 @@ require (
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/apiserver v0.33.3 // indirect
k8s.io/component-base v0.33.3 // indirect
k8s.io/apiserver v0.34.0 // indirect
k8s.io/component-base v0.34.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
Expand Down
Loading
Loading