Skip to content

Commit 1052e57

Browse files
authored
Merge pull request #96 from kcp-dev/fix-workspace-path
Fix support for workspace paths
2 parents db3f595 + 8ff47bb commit 1052e57

File tree

6 files changed

+170
-17
lines changed

6 files changed

+170
-17
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ $(YQ):
106106
yq_*
107107

108108
KCP = _tools/kcp
109-
KCP_VERSION ?= 0.28.1
109+
export KCP_VERSION ?= 0.28.1
110110

111111
.PHONY: $(KCP)
112112
$(KCP):

internal/controller/apiexport/controller.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ func (r *Reconciler) reconcile(ctx context.Context) error {
128128

129129
// for each PR, we note down the created ARS and also the GVKs of related resources
130130
arsList := sets.New[string]()
131-
claimedResources := sets.New[string]()
131+
claimedResources := sets.New[kcpdevv1alpha1.GroupResource]()
132132

133133
// PublishedResources use kinds, but the PermissionClaims use resource names (plural),
134134
// so we must translate accordingly
@@ -139,7 +139,17 @@ func (r *Reconciler) reconcile(ctx context.Context) error {
139139

140140
// to evaluate the namespace filter, the agent needs to fetch the namespace
141141
if filter := pubResource.Spec.Filter; filter != nil && filter.Namespace != nil {
142-
claimedResources.Insert("namespaces")
142+
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
143+
Group: "",
144+
Resource: "namespaces",
145+
})
146+
}
147+
148+
if pubResource.Spec.EnableWorkspacePaths {
149+
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
150+
Group: "core.kcp.io",
151+
Resource: "logicalclusters",
152+
})
143153
}
144154

145155
for _, rr := range pubResource.Spec.Related {
@@ -150,18 +160,27 @@ func (r *Reconciler) reconcile(ctx context.Context) error {
150160
return fmt.Errorf("unknown related resource kind %q: %w", rr.Kind, err)
151161
}
152162

153-
claimedResources.Insert(resource.Resource)
163+
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
164+
Group: "",
165+
Resource: resource.Resource,
166+
})
154167
}
155168
}
156169

157170
// Related resources (Secrets, ConfigMaps) are namespaced and so the Sync Agent will
158171
// always need to be able to see and manage namespaces.
159172
if claimedResources.Len() > 0 {
160-
claimedResources.Insert("namespaces")
173+
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
174+
Group: "",
175+
Resource: "namespaces",
176+
})
161177
}
162178

163179
// We always want to create events.
164-
claimedResources.Insert("events")
180+
claimedResources.Insert(kcpdevv1alpha1.GroupResource{
181+
Group: "",
182+
Resource: "events",
183+
})
165184

166185
if arsList.Len() == 0 {
167186
r.log.Debug("No ready PublishedResources available.")

internal/controller/apiexport/reconciler.go

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package apiexport
1818

1919
import (
20+
"fmt"
2021
"slices"
2122
"strings"
2223

@@ -36,7 +37,7 @@ import (
3637
// by a controller in kcp. Make sure you don't create a reconciling conflict!
3738
func (r *Reconciler) createAPIExportReconciler(
3839
availableResourceSchemas sets.Set[string],
39-
claimedResourceKinds sets.Set[string],
40+
claimedResourceKinds sets.Set[kcpdevv1alpha1.GroupResource],
4041
agentName string,
4142
apiExportName string,
4243
recorder record.EventRecorder,
@@ -59,28 +60,38 @@ func (r *Reconciler) createAPIExportReconciler(
5960
// only ensure the ones originating from the published resources;
6061
// step 1 is to collect all existing claims with the same properties
6162
// as ours.
62-
existingClaims := sets.New[string]()
63+
existingClaims := sets.New[kcpdevv1alpha1.GroupResource]()
6364
for _, claim := range existing.Spec.PermissionClaims {
64-
if claim.All && claim.Group == "" && len(claim.ResourceSelector) == 0 {
65-
existingClaims.Insert(claim.Resource)
65+
if claim.All && len(claim.ResourceSelector) == 0 {
66+
existingClaims.Insert(claim.GroupResource)
6667
}
6768
}
6869

6970
missingClaims := claimedResourceKinds.Difference(existingClaims)
7071

72+
claimsToAdd := missingClaims.UnsortedList()
73+
slices.SortStableFunc(claimsToAdd, func(a, b kcpdevv1alpha1.GroupResource) int {
74+
if a.Group != b.Group {
75+
return strings.Compare(a.Group, b.Group)
76+
}
77+
78+
return strings.Compare(a.Resource, b.Resource)
79+
})
80+
7181
// add our missing claims
72-
for _, claimed := range sets.List(missingClaims) {
82+
for _, claimed := range claimsToAdd {
7383
existing.Spec.PermissionClaims = append(existing.Spec.PermissionClaims, kcpdevv1alpha1.PermissionClaim{
74-
GroupResource: kcpdevv1alpha1.GroupResource{
75-
Group: "",
76-
Resource: claimed,
77-
},
78-
All: true,
84+
GroupResource: claimed,
85+
All: true,
7986
})
8087
}
8188

8289
if missingClaims.Len() > 0 {
83-
recorder.Eventf(existing, corev1.EventTypeNormal, "AddingPermissionClaims", "Added new permission claim(s) for all %s.", strings.Join(sets.List(missingClaims), ", "))
90+
claims := make([]string, 0, len(claimsToAdd))
91+
for _, claimed := range claimsToAdd {
92+
claims = append(claims, groupResourceToString(claimed))
93+
}
94+
recorder.Eventf(existing, corev1.EventTypeNormal, "AddingPermissionClaims", "Added new permission claim(s) for all %s.", strings.Join(claims, ", "))
8495
}
8596

8697
// prevent reconcile loops by ensuring a stable order
@@ -101,6 +112,14 @@ func (r *Reconciler) createAPIExportReconciler(
101112
}
102113
}
103114

115+
func groupResourceToString(gr kcpdevv1alpha1.GroupResource) string {
116+
if gr.Group == "" {
117+
return gr.Resource
118+
}
119+
120+
return fmt.Sprintf("%s/%s", gr.Group, gr.Resource)
121+
}
122+
104123
func mergeResourceSchemas(existing []string, configured sets.Set[string]) []string {
105124
var result []string
106125

internal/controller/syncmanager/controller.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
3030

3131
kcpapisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
32+
kcpcorev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
3233
apiexportprovider "github.com/kcp-dev/multicluster-provider/apiexport"
3334
mccontroller "sigs.k8s.io/multicluster-runtime/pkg/controller"
3435
mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"
@@ -248,6 +249,10 @@ func (r *Reconciler) ensureManager(log *zap.SugaredLogger, vwURL string) error {
248249
return fmt.Errorf("failed to register scheme %s: %w", kcpapisv1alpha1.SchemeGroupVersion, err)
249250
}
250251

252+
if err := kcpcorev1alpha1.AddToScheme(scheme); err != nil {
253+
return fmt.Errorf("failed to register scheme %s: %w", kcpcorev1alpha1.SchemeGroupVersion, err)
254+
}
255+
251256
if r.vwProvider == nil {
252257
log.Debug("Setting up APIExport provider…")
253258

test/e2e/sync/primary_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,3 +739,103 @@ spec:
739739
t.Fatalf("Failed to wait for object to be synced down: %v", err)
740740
}
741741
}
742+
743+
func TestSyncWithWorkspacePath(t *testing.T) {
744+
const (
745+
apiExportName = "kcp.example.com"
746+
kcpGroupName = "kcp.example.com"
747+
orgWorkspace = "sync-with-paths"
748+
)
749+
750+
ctx := t.Context()
751+
ctrlruntime.SetLogger(logr.Discard())
752+
753+
// setup a test environment in kcp
754+
orgKubconfig := utils.CreateOrganization(t, ctx, orgWorkspace, apiExportName)
755+
756+
// start a service cluster
757+
envtestKubeconfig, envtestClient, _ := utils.RunEnvtest(t, []string{
758+
"test/crds/crontab.yaml",
759+
})
760+
761+
// publish Crontabs and Backups
762+
t.Logf("Publishing CRDs…")
763+
prCrontabs := &syncagentv1alpha1.PublishedResource{
764+
ObjectMeta: metav1.ObjectMeta{
765+
Name: "publish-crontabs",
766+
},
767+
Spec: syncagentv1alpha1.PublishedResourceSpec{
768+
Resource: syncagentv1alpha1.SourceResourceDescriptor{
769+
APIGroup: "example.com",
770+
Version: "v1",
771+
Kind: "CronTab",
772+
},
773+
// These rules make finding the local object easier, but should not be used in production.
774+
Naming: &syncagentv1alpha1.ResourceNaming{
775+
Name: "{{ .Object.metadata.name }}",
776+
Namespace: "synced-{{ .Object.metadata.namespace }}",
777+
},
778+
Projection: &syncagentv1alpha1.ResourceProjection{
779+
Group: kcpGroupName,
780+
},
781+
EnableWorkspacePaths: true,
782+
},
783+
}
784+
785+
if err := envtestClient.Create(ctx, prCrontabs); err != nil {
786+
t.Fatalf("Failed to create PublishedResource: %v", err)
787+
}
788+
789+
// start the agent in the background to update the APIExport with the CronTabs API
790+
utils.RunAgent(ctx, t, "bob", orgKubconfig, envtestKubeconfig, apiExportName)
791+
792+
// wait until the API is available
793+
kcpClusterClient := utils.GetKcpAdminClusterClient(t)
794+
795+
teamClusterPath := logicalcluster.NewPath("root").Join(orgWorkspace).Join("team-1")
796+
teamClient := kcpClusterClient.Cluster(teamClusterPath)
797+
798+
utils.WaitForBoundAPI(t, ctx, teamClient, schema.GroupVersionResource{
799+
Group: kcpGroupName,
800+
Version: "v1",
801+
Resource: "crontabs",
802+
})
803+
804+
// create a Crontab object in a team workspace
805+
t.Log("Creating CronTab in kcp…")
806+
crontab := utils.YAMLToUnstructured(t, `
807+
apiVersion: kcp.example.com/v1
808+
kind: CronTab
809+
metadata:
810+
namespace: default
811+
name: my-crontab
812+
spec:
813+
cronSpec: '* * *'
814+
image: ubuntu:latest
815+
`)
816+
817+
if err := teamClient.Create(ctx, crontab); err != nil {
818+
t.Fatalf("Failed to create CronTab in kcp: %v", err)
819+
}
820+
821+
// wait for the agent to sync the object down into the service cluster
822+
823+
t.Logf("Wait for CronTab to be synced…")
824+
copy := &unstructured.Unstructured{}
825+
copy.SetAPIVersion("example.com/v1")
826+
copy.SetKind("CronTab")
827+
828+
err := wait.PollUntilContextTimeout(ctx, 500*time.Millisecond, 30*time.Second, false, func(ctx context.Context) (done bool, err error) {
829+
copyKey := types.NamespacedName{Namespace: "synced-default", Name: "my-crontab"}
830+
return envtestClient.Get(ctx, copyKey, copy) == nil, nil
831+
})
832+
if err != nil {
833+
t.Fatalf("Failed to wait for object to be synced down: %v", err)
834+
}
835+
836+
ann := "syncagent.kcp.io/remote-object-workspace-path"
837+
838+
if value := copy.GetAnnotations()[ann]; value != teamClusterPath.String() {
839+
t.Fatalf("Expected %s annotation to be %q, but is %q.", ann, teamClusterPath.String(), value)
840+
}
841+
}

test/utils/fixtures.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,16 @@ func BindToAPIExport(t *testing.T, ctx context.Context, client ctrlruntimeclient
287287
},
288288
State: kcpapisv1alpha1.ClaimAccepted,
289289
},
290+
{
291+
PermissionClaim: kcpapisv1alpha1.PermissionClaim{
292+
GroupResource: kcpapisv1alpha1.GroupResource{
293+
Group: "core.kcp.io",
294+
Resource: "logicalclusters",
295+
},
296+
All: true,
297+
},
298+
State: kcpapisv1alpha1.ClaimAccepted,
299+
},
290300
},
291301
},
292302
}

0 commit comments

Comments
 (0)