Skip to content

Disable APIExport VirtualWorkspace URL population #3411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 4, 2025
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ test-e2e-sharded-minimal: build-all

# This is just easy target to run 2 shard test server locally until manually killed.
# You can targer test to it by running:
# go test ./test/e2e/apibinding/... --kcp-kubeconfig=${WORK_DIR:-.}/.kcp/admin.kubeconfig --shard-kubeconfigs=root=${WORK_DIR:-.}/.kcp-0/admin.kubeconfig -run=^TestAPIBindingEndpointSlicesSharded$
# go test ./test/e2e/apibinding/... --kcp-kubeconfig=$(pwd)/.kcp/admin.kubeconfig --shard-kubeconfigs=root=$(pwd)/.kcp-0/admin.kubeconfig -run=^TestAPIBinding$
test-run-sharded-server: WORK_DIR ?= $(PWD)
test-run-sharded-server: LOG_DIR ?= $(WORK_DIR)/.kcp
test-run-sharded-server:
Expand Down
10 changes: 9 additions & 1 deletion pkg/features/kcp_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ const (
// alpha: v0.1
// Enables cache apis and controllers.
CacheAPIs featuregate.Feature = "CacheAPIs"

// owner: @mjudeikis
// alpha: v0.1
// Enables VirtualWorkspace urls on APIExport. This enables to use Deprecated APIExport VirtualWorkspace urls.
// This is a temporary feature to ease the migration to the new VirtualWorkspace urls.
EnableDeprecatedAPIExportVirtualWorkspacesUrls featuregate.Feature = "EnableDeprecatedAPIExportVirtualWorkspacesUrls"
)

// DefaultFeatureGate exposes the upstream feature gate, but with our gate setting applied.
Expand Down Expand Up @@ -120,7 +126,9 @@ var defaultVersionedGenericControlPlaneFeatureGates = map[featuregate.Feature]fe
CacheAPIs: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},

EnableDeprecatedAPIExportVirtualWorkspacesUrls: {
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
},
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side:
genericfeatures.APIResponseCompression: {
Expand Down
13 changes: 13 additions & 0 deletions pkg/reconciler/apis/apiexport/apiexport_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ import (
"github.com/kcp-dev/kcp/pkg/logging"
"github.com/kcp-dev/kcp/pkg/reconciler/committer"
"github.com/kcp-dev/kcp/pkg/reconciler/events"
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
apisv1alpha2 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha2"
corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster"
apisv1alpha2client "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/typed/apis/v1alpha2"
apisv1alpha1informers "github.com/kcp-dev/kcp/sdk/client/informers/externalversions/apis/v1alpha1"
apisv1alpha2informers "github.com/kcp-dev/kcp/sdk/client/informers/externalversions/apis/v1alpha2"
corev1alpha1informers "github.com/kcp-dev/kcp/sdk/client/informers/externalversions/core/v1alpha1"
)
Expand All @@ -59,6 +61,7 @@ const (
func NewController(
kcpClusterClient kcpclientset.ClusterInterface,
apiExportInformer apisv1alpha2informers.APIExportClusterInformer,
apiExportEndpointSliceInformer apisv1alpha1informers.APIExportEndpointSliceClusterInformer,
globalShardInformer corev1alpha1informers.ShardClusterInformer,
kubeClusterClient kcpkubernetesclientset.ClusterInterface,
namespaceInformer kcpcorev1informers.NamespaceClusterInformer,
Expand Down Expand Up @@ -89,6 +92,13 @@ func NewController(
getAPIExport: func(clusterName logicalcluster.Name, name string) (*apisv1alpha2.APIExport, error) {
return apiExportInformer.Lister().Cluster(clusterName).Get(name)
},
getAPIExportEndpointSlice: func(clusterName logicalcluster.Name, name string) (*apisv1alpha1.APIExportEndpointSlice, error) {
return apiExportEndpointSliceInformer.Lister().Cluster(clusterName).Get(name)
},
createAPIExportEndpointSlice: func(ctx context.Context, clusterName logicalcluster.Path, apiExportEndpointSlice *apisv1alpha1.APIExportEndpointSlice) error {
_, err := kcpClusterClient.Cluster(clusterName).ApisV1alpha1().APIExportEndpointSlices().Create(ctx, apiExportEndpointSlice, metav1.CreateOptions{})
return err
},

getNamespace: func(clusterName logicalcluster.Name, name string) (*corev1.Namespace, error) {
return namespaceInformer.Lister().Cluster(clusterName).Get(name)
Expand Down Expand Up @@ -183,6 +193,9 @@ type controller struct {
listAPIExportsForSecret func(secret *corev1.Secret) ([]*apisv1alpha2.APIExport, error)
getAPIExport func(clusterName logicalcluster.Name, name string) (*apisv1alpha2.APIExport, error)

getAPIExportEndpointSlice func(clusterName logicalcluster.Name, name string) (*apisv1alpha1.APIExportEndpointSlice, error)
createAPIExportEndpointSlice func(ctx context.Context, clusterName logicalcluster.Path, apiExportEndpointSlice *apisv1alpha1.APIExportEndpointSlice) error

getNamespace func(clusterName logicalcluster.Name, name string) (*corev1.Namespace, error)
createNamespace func(ctx context.Context, clusterName logicalcluster.Path, ns *corev1.Namespace) error

Expand Down
110 changes: 79 additions & 31 deletions pkg/reconciler/apis/apiexport/apiexport_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (

"github.com/kcp-dev/logicalcluster/v3"

kcpfeatures "github.com/kcp-dev/kcp/pkg/features"
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
apisv1alpha2 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha2"
corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
Expand All @@ -39,6 +40,9 @@ import (
)

func TestReconcile(t *testing.T) {
// Save original feature gate value
originalFeatureGate := kcpfeatures.DefaultFeatureGate.Enabled(kcpfeatures.EnableDeprecatedAPIExportVirtualWorkspacesUrls)

tests := map[string]struct {
secretRefSet bool
secretExists bool
Expand All @@ -49,19 +53,21 @@ func TestReconcile(t *testing.T) {
apiExportHasSomeOtherHash bool
hasPreexistingVerifyFailure bool
listShardsError error
apiExportEndpointSliceNotFound bool
skipEndpointSliceAnnotation bool

apiBindings []interface{}

wantGenerationFailed bool
wantError bool
wantCreateSecretCalled bool
wantUnsetIdentity bool
wantDefaultSecretRef bool
wantStatusHashSet bool
wantVerifyFailure bool
wantIdentityValid bool
wantVirtualWorkspaceURLsError bool
wantVirtualWorkspaceURLsReady bool
wantGenerationFailed bool
wantError bool
wantCreateSecretCalled bool
wantUnsetIdentity bool
wantDefaultSecretRef bool
wantStatusHashSet bool
wantVerifyFailure bool
wantIdentityValid bool
wantCreateAPIExportEndpointSlice bool
wantVirtualWorkspaceURLs bool
}{
"create secret when ref is nil and secret doesn't exist": {
secretExists: false,
Expand Down Expand Up @@ -122,26 +128,55 @@ func TestReconcile(t *testing.T) {
apiBindings: []interface{}{
"something",
},
listShardsError: errors.New("foo"),
wantVirtualWorkspaceURLsError: true,
listShardsError: errors.New("foo"),
},
"virtualWorkspaceURLs set when APIBindings present": {
"create APIExportEndpointSlice when APIBindings present": {
secretRefSet: true,
secretExists: true,

wantStatusHashSet: true,
wantIdentityValid: true,
wantStatusHashSet: true,
apiExportEndpointSliceNotFound: true,
wantCreateAPIExportEndpointSlice: true,
wantIdentityValid: true,
},
"skip APIExportEndpointSlice creation when skip annotation is present": {
secretRefSet: true,
secretExists: true,

apiBindings: []interface{}{
"something",
},
wantVirtualWorkspaceURLsReady: true,
wantStatusHashSet: true,
apiExportEndpointSliceNotFound: true,
wantCreateAPIExportEndpointSlice: false,
wantIdentityValid: true,
skipEndpointSliceAnnotation: true,
},
"virtual workspace URLs when feature gate enabled": {
secretRefSet: true,
secretExists: true,

wantStatusHashSet: true,
wantIdentityValid: true,
wantVirtualWorkspaceURLs: true,
},
"error listing shards with feature gate enabled": {
secretRefSet: true,
secretExists: true,

wantStatusHashSet: true,
wantIdentityValid: true,
wantVirtualWorkspaceURLs: false,
listShardsError: errors.New("foo"),
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// Skip virtual workspace URL tests if feature gate is not enabled
if tc.wantVirtualWorkspaceURLs && !originalFeatureGate {
t.Skip("Skipping test that requires EnableDeprecatedAPIExportVirtualWorkspacesUrls feature gate")
}

createSecretCalled := false
createEndpointSliceCalled := false

expectedKey := "abc"
expectedHash := fmt.Sprintf("%x", sha256.Sum256([]byte(expectedKey)))
Expand All @@ -155,6 +190,16 @@ func TestReconcile(t *testing.T) {
return nil
},
secretNamespace: "default-ns",
getAPIExportEndpointSlice: func(clusterName logicalcluster.Name, name string) (*apisv1alpha1.APIExportEndpointSlice, error) {
if tc.apiExportEndpointSliceNotFound {
return nil, apierrors.NewNotFound(corev1.Resource("apiexportendpointslices"), name)
}
return &apisv1alpha1.APIExportEndpointSlice{}, nil
},
createAPIExportEndpointSlice: func(ctx context.Context, clusterName logicalcluster.Path, apiExportEndpointSlice *apisv1alpha1.APIExportEndpointSlice) error {
createEndpointSliceCalled = true
return nil
},
getSecret: func(ctx context.Context, clusterName logicalcluster.Name, ns, name string) (*corev1.Secret, error) {
if tc.secretExists {
secret := &corev1.Secret{
Expand Down Expand Up @@ -237,6 +282,10 @@ func TestReconcile(t *testing.T) {
conditions.MarkFalse(apiExport, apisv1alpha2.APIExportIdentityValid, apisv1alpha2.IdentityVerificationFailedReason, conditionsv1alpha1.ConditionSeverityError, "")
}

if tc.skipEndpointSliceAnnotation {
apiExport.Annotations[apisv1alpha2.APIExportEndpointSliceSkipAnnotation] = "true"
}

err := c.reconcile(context.Background(), apiExport)
if tc.wantError {
require.Error(t, err, "expected an error")
Expand Down Expand Up @@ -288,19 +337,18 @@ func TestReconcile(t *testing.T) {
requireConditionMatches(t, apiExport, conditions.TrueCondition(apisv1alpha2.APIExportIdentityValid))
}

if tc.wantVirtualWorkspaceURLsError {
requireConditionMatches(t, apiExport,
conditions.FalseCondition(
apisv1alpha2.APIExportVirtualWorkspaceURLsReady,
apisv1alpha2.ErrorGeneratingURLsReason,
conditionsv1alpha1.ConditionSeverityError,
"",
),
)
}
require.Equal(t, tc.wantCreateAPIExportEndpointSlice, createEndpointSliceCalled, "expected createEndpointSliceCalled to be %v", tc.wantCreateAPIExportEndpointSlice)

if tc.wantVirtualWorkspaceURLsReady {
requireConditionMatches(t, apiExport, conditions.TrueCondition(apisv1alpha2.APIExportVirtualWorkspaceURLsReady))
if tc.wantVirtualWorkspaceURLs {
//nolint:staticcheck
require.NotNil(t, apiExport.Status.VirtualWorkspaces, "expected virtual workspace URLs to be set")
//nolint:staticcheck
require.Len(t, apiExport.Status.VirtualWorkspaces, 2, "expected 2 virtual workspace URLs")
require.True(t, conditions.IsTrue(apiExport, apisv1alpha2.APIExportVirtualWorkspaceURLsReady), "expected virtual workspace URLs to be ready")
} else {
//nolint:staticcheck
require.Nil(t, apiExport.Status.VirtualWorkspaces, "expected virtual workspace URLs to be nil")
require.False(t, conditions.Has(apiExport, apisv1alpha2.APIExportVirtualWorkspaceURLsReady), "expected virtual workspace URLs condition to not exist")
}
})
}
Expand Down
74 changes: 53 additions & 21 deletions pkg/reconciler/apis/apiexport/apiexport_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ import (
"github.com/kcp-dev/logicalcluster/v3"

virtualworkspacesoptions "github.com/kcp-dev/kcp/cmd/virtual-workspaces/options"
kcpfeatures "github.com/kcp-dev/kcp/pkg/features"
"github.com/kcp-dev/kcp/pkg/logging"
apiexportbuilder "github.com/kcp-dev/kcp/pkg/virtual/apiexport/builder"
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
apisv1alpha2 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha2"
"github.com/kcp-dev/kcp/sdk/apis/core"
conditionsv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/third_party/conditions/apis/conditions/v1alpha1"
"github.com/kcp-dev/kcp/sdk/apis/third_party/conditions/util/conditions"
)
Expand All @@ -46,6 +48,7 @@ func (c *controller) reconcile(ctx context.Context, apiExport *apisv1alpha2.APIE
}

clusterName := logicalcluster.From(apiExport)
clusterPath := apiExport.Annotations[core.LogicalClusterPathAnnotationKey]

if identity.SecretRef == nil {
c.ensureSecretNamespaceExists(ctx, clusterName)
Expand Down Expand Up @@ -97,29 +100,58 @@ func (c *controller) reconcile(ctx context.Context, apiExport *apisv1alpha2.APIE
)
}

// TODO(sttts): reactivate this with multi-shard support eventually
/*
// check if any APIBindings are bound to this APIExport. If so, add a virtualworkspaceURL
apiBindings, err := c.getAPIBindingsForAPIExport(clusterName, apiExport.Name)
// Ensure the APIExportEndpointSlice exists
if _, ok := apiExport.Annotations[apisv1alpha2.APIExportEndpointSliceSkipAnnotation]; !ok {
_, err := c.getAPIExportEndpointSlice(clusterName, apiExport.Name)
if err != nil {
return fmt.Errorf("error checking for APIBindings with APIExport %s|%s: %w", clusterName, apiExport.Name, err)
if errors.IsNotFound(err) {
// Create the APIExportEndpointSlice
apiExportEndpointSlice := &apisv1alpha1.APIExportEndpointSlice{
ObjectMeta: metav1.ObjectMeta{
Name: apiExport.Name,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: apisv1alpha1.SchemeGroupVersion.String(),
Kind: "APIExport",
Name: apiExport.Name,
UID: apiExport.UID,
},
},
},
Spec: apisv1alpha1.APIExportEndpointSliceSpec{
APIExport: apisv1alpha1.ExportBindingReference{
Name: apiExport.Name,
Path: clusterPath,
},
},
}
if err := c.createAPIExportEndpointSlice(ctx, clusterName.Path(), apiExportEndpointSlice); err != nil {
return fmt.Errorf("error creating APIExportEndpointSlice for APIExport %s|%s: %w", clusterName, apiExport.Name, err)
}
} else {
return fmt.Errorf("error getting APIExportEndpointSlice for APIExport %s|%s: %w", clusterName, apiExport.Name, err)
}
}
}

// If there are no bindings, then we can't create a URL yet.
if len(apiBindings) == 0 {
return nil
// Ensure the APIExport has a virtual workspace URL
// TODO(mjudeikis): Remove this once we remove feature gate.
if kcpfeatures.DefaultFeatureGate.Enabled(kcpfeatures.EnableDeprecatedAPIExportVirtualWorkspacesUrls) {
if err := c.updateVirtualWorkspaceURLs(ctx, apiExport); err != nil {
conditions.MarkFalse(
apiExport,
apisv1alpha2.APIExportVirtualWorkspaceURLsReady,
Copy link
Member

Choose a reason for hiding this comment

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

Question: What happens to this condition when the feature gate is disabled (i.e. the default from the next release)? Should we clean this up somehow? Otherwise we will have a dangling condition on APIExports from previous versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good call. I think we should remove this in else statment

Copy link
Member

Choose a reason for hiding this comment

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

On a similar note, what do we do about APIExports that have the virtual workspace URL set? Should we maybe also wipe them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated with both

apisv1alpha2.ErrorGeneratingURLsReason,
conditionsv1alpha1.ConditionSeverityError,
"%v",
err,
)
}
*/

if err := c.updateVirtualWorkspaceURLs(ctx, apiExport); err != nil {
conditions.MarkFalse(
apiExport,
apisv1alpha2.APIExportVirtualWorkspaceURLsReady,
apisv1alpha2.ErrorGeneratingURLsReason,
conditionsv1alpha1.ConditionSeverityError,
"%v",
err,
)
} else {
// Remove the condition and status.virtualWorkspaces if the feature gate is disabled.
conditions.Delete(apiExport, apisv1alpha2.APIExportVirtualWorkspaceURLsReady)
//nolint:staticcheck
apiExport.Status.VirtualWorkspaces = nil
}

return nil
Expand Down Expand Up @@ -216,11 +248,11 @@ func (c *controller) updateVirtualWorkspaceURLs(ctx context.Context, apiExport *
desiredURLs.Insert(u.String())
}

//nolint:staticcheck // SA1019 VirtualWorkspaces is deprecated but not removed yet
//nolint:staticcheck
apiExport.Status.VirtualWorkspaces = nil

for _, u := range sets.List[string](desiredURLs) {
//nolint:staticcheck // SA1019 VirtualWorkspaces is deprecated but not removed yet
//nolint:staticcheck
apiExport.Status.VirtualWorkspaces = append(apiExport.Status.VirtualWorkspaces, apisv1alpha2.VirtualWorkspace{
URL: u,
})
Expand Down
2 changes: 2 additions & 0 deletions pkg/server/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,7 @@ func (s *Server) installAPIExportController(ctx context.Context, config *rest.Co
c, err := apiexport.NewController(
kcpClusterClient,
s.KcpSharedInformerFactory.Apis().V1alpha2().APIExports(),
s.KcpSharedInformerFactory.Apis().V1alpha1().APIExportEndpointSlices(),
s.CacheKcpSharedInformerFactory.Core().V1alpha1().Shards(),
kubeClusterClient,
s.KubeSharedInformerFactory.Core().V1().Namespaces(),
Expand All @@ -1068,6 +1069,7 @@ func (s *Server) installAPIExportController(ctx context.Context, config *rest.Co
return wait.PollUntilContextCancel(ctx, waitPollInterval, true, func(ctx context.Context) (bool, error) {
return s.ApiExtensionsSharedInformerFactory.Apiextensions().V1().CustomResourceDefinitions().Informer().HasSynced() &&
s.KcpSharedInformerFactory.Apis().V1alpha2().APIExports().Informer().HasSynced() &&
s.KcpSharedInformerFactory.Apis().V1alpha1().APIExportEndpointSlices().Informer().HasSynced() &&
s.KubeSharedInformerFactory.Core().V1().Namespaces().Informer().HasSynced() &&
s.KubeSharedInformerFactory.Core().V1().Secrets().Informer().HasSynced(), nil
})
Expand Down
1 change: 1 addition & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ func (s *Server) Run(ctx context.Context) error {
logger.Info("finished bootstrapping the shard workspace")

go s.KcpSharedInformerFactory.Apis().V1alpha2().APIExports().Informer().Run(hookContext.Done())
go s.KcpSharedInformerFactory.Apis().V1alpha1().APIExportEndpointSlices().Informer().Run(hookContext.Done())
go s.CacheKcpSharedInformerFactory.Apis().V1alpha2().APIExports().Informer().Run(hookContext.Done())
go s.KcpSharedInformerFactory.Core().V1alpha1().LogicalClusters().Informer().Run(hookContext.Done())
go s.KcpSharedInformerFactory.Cache().V1alpha1().CachedResources().Informer().Run(hookContext.Done())
Expand Down
Loading