Skip to content

Commit 95ac113

Browse files
committed
make workspace updates code-drive vs user drive
1 parent 4b908e1 commit 95ac113

File tree

11 files changed

+249
-101
lines changed

11 files changed

+249
-101
lines changed

config/root-phase0/apiexport-tenancy.kcp.io.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ spec:
99
resources:
1010
- group: tenancy.kcp.io
1111
name: workspaces
12-
schema: v241020-fce06d31d.workspaces.tenancy.kcp.io
12+
schema: v250315-28b43d5a9.workspaces.tenancy.kcp.io
1313
storage:
1414
crd: {}
1515
- group: tenancy.kcp.io

pkg/admission/workspace/admission.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,11 @@ func (o *workspace) Validate(ctx context.Context, a admission.Attributes, _ admi
213213
return admission.NewForbidden(a, errors.New("spec.type cannot be set for mounted workspaces"))
214214
}
215215
// Check for immutability of spec.type via pointers checks first.
216-
if (old.Spec.Type == nil && ws.Spec.Type != nil) || (old.Spec.Type != nil && ws.Spec.Type == nil) {
216+
if !isSystemPrivileged && ((old.Spec.Type == nil && ws.Spec.Type != nil) || (old.Spec.Type != nil && ws.Spec.Type == nil)) {
217217
return admission.NewForbidden(a, errors.New("spec.type is immutable"))
218218
}
219219
// Check for immutability of spec.type via field checks.
220-
if old.Spec.Type != nil && ws.Spec.Type != nil {
220+
if !isSystemPrivileged && old.Spec.Type != nil && ws.Spec.Type != nil {
221221
if old.Spec.Type.Path != ws.Spec.Type.Path || old.Spec.Type.Name != ws.Spec.Type.Name {
222222
return admission.NewForbidden(a, errors.New("spec.type is immutable"))
223223
}

pkg/indexers/indexers.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,19 @@ func ByPathAndNameWithFallback[T runtime.Object](groupResource schema.GroupResou
137137
// Didn't find it locally - try remote
138138
return ByPathAndName[T](groupResource, globalIndexer, path, name)
139139
}
140+
141+
// IndexByWorkspaceLogicalClusterAndURL indexes by logical cluster path and object name, if the annotation exists.
142+
func IndexByWorkspaceLogicalClusterAndURL(obj interface{}) ([]string, error) {
143+
metaObj, ok := obj.(metav1.Object)
144+
if !ok {
145+
return []string{}, fmt.Errorf("obj is supposed to be a metav1.Object, but is %T", obj)
146+
}
147+
if path, found := metaObj.GetAnnotations()[core.LogicalClusterPathAnnotationKey]; found {
148+
return []string{
149+
logicalcluster.NewPath(path).Join(metaObj.GetName()).String(),
150+
logicalcluster.From(metaObj).Path().Join(metaObj.GetName()).String(),
151+
}, nil
152+
}
153+
154+
return []string{logicalcluster.From(metaObj).Path().Join(metaObj.GetName()).String()}, nil
155+
}

pkg/indexers/workspace.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
Copyright 2022 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package indexers
18+
19+
import (
20+
"fmt"
21+
22+
tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
23+
)
24+
25+
const (
26+
// WorkspaceByURL is the indexer workspace by its url.
27+
WorkspaceByURL = "WorkspaceByURL"
28+
)
29+
30+
// IndexAPIExportByIdentity is an index function that indexes an APIExport by its identity hash.
31+
func IndexWorkspaceByURL(obj interface{}) (string, error) {
32+
ws, ok := obj.(*tenancyv1alpha1.Workspace)
33+
if !ok {
34+
return "", fmt.Errorf("obj %T is not an APIExportEndpointSlice", obj)
35+
}
36+
37+
return ws.Spec.URL, nil
38+
}

pkg/indexers/workspace_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
Copyright 2025 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package indexers
18+
19+
import (
20+
"reflect"
21+
"testing"
22+
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
25+
tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
26+
)
27+
28+
func TestIndexWorkspaceByURL(t *testing.T) {
29+
tests := map[string]struct {
30+
obj interface{}
31+
want string
32+
wantErr bool
33+
}{
34+
"not a Workspace": {
35+
obj: "not a Workspace",
36+
want: "",
37+
wantErr: true,
38+
},
39+
"valid Workspace without url": {
40+
obj: &tenancyv1alpha1.Workspace{
41+
ObjectMeta: metav1.ObjectMeta{
42+
Name: "foo",
43+
},
44+
Spec: tenancyv1alpha1.WorkspaceSpec{},
45+
},
46+
wantErr: false,
47+
want: "",
48+
},
49+
"valid Workspace with url": {
50+
obj: &tenancyv1alpha1.Workspace{
51+
ObjectMeta: metav1.ObjectMeta{
52+
Name: "foo",
53+
},
54+
Spec: tenancyv1alpha1.WorkspaceSpec{
55+
URL: "https://example.com",
56+
},
57+
},
58+
wantErr: false,
59+
want: "https://example.com",
60+
},
61+
}
62+
63+
for name, tt := range tests {
64+
t.Run(name, func(t *testing.T) {
65+
got, err := IndexWorkspaceByURL(tt.obj)
66+
if (err != nil) != tt.wantErr {
67+
t.Errorf("IndexWorkspaceByURL() error = %v, wantErr %v", err, tt.wantErr)
68+
return
69+
}
70+
if !reflect.DeepEqual(got, tt.want) {
71+
t.Errorf("IndexWorkspaceByURL() got = %v, want %v", got, tt.want)
72+
}
73+
})
74+
}
75+
}

pkg/reconciler/tenancy/workspace/workspace_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ func (c *Controller) process(ctx context.Context, key string) (bool, error) {
275275
}
276276

277277
// InstallIndexers adds the additional indexers that this controller requires to the informers.
278-
func InstallIndexers(workspaceInformer tenancyv1alpha1informers.WorkspaceClusterInformer,
278+
func InstallIndexers(
279+
workspaceInformer tenancyv1alpha1informers.WorkspaceClusterInformer,
279280
globalShardInformer corev1alpha1informers.ShardClusterInformer,
280281
globalWorkspaceTypeInformer tenancyv1alpha1informers.WorkspaceTypeClusterInformer,
281282
) {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
Copyright 2022 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package workspacemounts
18+
19+
import (
20+
"encoding/json"
21+
"fmt"
22+
"strings"
23+
24+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
26+
27+
"github.com/kcp-dev/logicalcluster/v3"
28+
29+
tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
30+
)
31+
32+
const workspaceByURL = "WorkspaceByURL"
33+
34+
// indexWorkspaceByURL is an index for workspaces by their URL.
35+
func indexWorkspaceByURL(obj interface{}) ([]string, error) {
36+
ws, ok := obj.(*tenancyv1alpha1.Workspace)
37+
if !ok {
38+
return nil, fmt.Errorf("obj %T is not an Workspace", obj)
39+
}
40+
41+
return []string{ws.Spec.URL}, nil
42+
}
43+
44+
const workspaceMountsReferenceIndex = "WorkspacesByMountReference"
45+
46+
type workspaceMountsReferenceKey struct {
47+
ClusterName string `json:"clusterName"`
48+
Group string `json:"group"`
49+
Resource string `json:"resource"`
50+
Name string `json:"name"`
51+
Namespace string `json:"namespace,omitempty"`
52+
}
53+
54+
func indexWorkspaceByMountObject(obj interface{}) ([]string, error) {
55+
ws, ok := obj.(*tenancyv1alpha1.Workspace)
56+
if !ok {
57+
return []string{}, fmt.Errorf("obj is supposed to be a Workspace, but is %T", obj)
58+
}
59+
60+
if ws.Spec.Mount == nil {
61+
return nil, nil
62+
}
63+
64+
key := workspaceMountsReferenceKey{
65+
ClusterName: logicalcluster.From(ws).String(),
66+
// TODO(sttts): do proper REST mapping
67+
Resource: strings.ToLower(ws.Spec.Mount.Reference.Kind) + "s",
68+
Name: ws.Spec.Mount.Reference.Name,
69+
Namespace: ws.Spec.Mount.Reference.Namespace,
70+
}
71+
cs := strings.SplitN(ws.Spec.Mount.Reference.APIVersion, "/", 2)
72+
if len(cs) == 2 {
73+
key.Group = cs[0]
74+
}
75+
bs, err := json.Marshal(key)
76+
if err != nil {
77+
return nil, fmt.Errorf("unable to marshal mount reference: %w", err)
78+
}
79+
80+
return []string{string(bs)}, nil
81+
}
82+
83+
func indexWorkspaceByMountObjectValue(gvr schema.GroupVersionResource, obj *unstructured.Unstructured) (string, error) {
84+
key := workspaceMountsReferenceKey{
85+
ClusterName: logicalcluster.From(obj).String(),
86+
Group: gvr.Group,
87+
Resource: gvr.Resource,
88+
Name: obj.GetName(),
89+
Namespace: obj.GetNamespace(),
90+
}
91+
bs, err := json.Marshal(key)
92+
if err != nil {
93+
return "", fmt.Errorf("unable to marshal mount reference: %w", err)
94+
}
95+
return string(bs), nil
96+
}

pkg/reconciler/tenancy/workspacemounts/workspacemounts_controller.go

Lines changed: 5 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package workspacemounts
1818

1919
import (
2020
"context"
21-
"encoding/json"
2221
"fmt"
2322
"strings"
2423
"time"
@@ -238,7 +237,8 @@ func (c *Controller) process(ctx context.Context, key string) (bool, error) {
238237

239238
// reconcile the spec
240239
specUpdater := &workspaceSpecUpdater{
241-
getMountObject: getMountObjectFunc,
240+
getMountObject: getMountObjectFunc,
241+
workspaceIndexer: c.workspaceIndexer,
242242
}
243243
status, err = specUpdater.reconcile(ctx, workspace)
244244
if err != nil {
@@ -277,65 +277,15 @@ func (c *Controller) enqueuePotentiallyMountResource(gvr schema.GroupVersionReso
277277
}
278278
}
279279

280-
const workspaceMountsReferenceIndex = "WorkspacesByMountReference"
281-
282-
type workspaceMountsReferenceKey struct {
283-
ClusterName string `json:"clusterName"`
284-
Group string `json:"group"`
285-
Resource string `json:"resource"`
286-
Name string `json:"name"`
287-
Namespace string `json:"namespace,omitempty"`
288-
}
289-
290280
// InstallIndexers adds the additional indexers that this controller requires to the informers.
291281
func InstallIndexers(
292282
workspaceInformer tenancyv1alpha1informers.WorkspaceClusterInformer,
293283
) {
294284
indexers.AddIfNotPresentOrDie(workspaceInformer.Informer().GetIndexer(), cache.Indexers{
295285
workspaceMountsReferenceIndex: indexWorkspaceByMountObject,
296286
})
297-
}
298287

299-
func indexWorkspaceByMountObject(obj interface{}) ([]string, error) {
300-
ws, ok := obj.(*tenancyv1alpha1.Workspace)
301-
if !ok {
302-
return []string{}, fmt.Errorf("obj is supposed to be a Workspace, but is %T", obj)
303-
}
304-
305-
if ws.Spec.Mount == nil {
306-
return nil, nil
307-
}
308-
309-
key := workspaceMountsReferenceKey{
310-
ClusterName: logicalcluster.From(ws).String(),
311-
// TODO(sttts): do proper REST mapping
312-
Resource: strings.ToLower(ws.Spec.Mount.Reference.Kind) + "s",
313-
Name: ws.Spec.Mount.Reference.Name,
314-
Namespace: ws.Spec.Mount.Reference.Namespace,
315-
}
316-
cs := strings.SplitN(ws.Spec.Mount.Reference.APIVersion, "/", 2)
317-
if len(cs) == 2 {
318-
key.Group = cs[0]
319-
}
320-
bs, err := json.Marshal(key)
321-
if err != nil {
322-
return nil, fmt.Errorf("unable to marshal mount reference: %w", err)
323-
}
324-
325-
return []string{string(bs)}, nil
326-
}
327-
328-
func indexWorkspaceByMountObjectValue(gvr schema.GroupVersionResource, obj *unstructured.Unstructured) (string, error) {
329-
key := workspaceMountsReferenceKey{
330-
ClusterName: logicalcluster.From(obj).String(),
331-
Group: gvr.Group,
332-
Resource: gvr.Resource,
333-
Name: obj.GetName(),
334-
Namespace: obj.GetNamespace(),
335-
}
336-
bs, err := json.Marshal(key)
337-
if err != nil {
338-
return "", fmt.Errorf("unable to marshal mount reference: %w", err)
339-
}
340-
return string(bs), nil
288+
indexers.AddIfNotPresentOrDie(workspaceInformer.Informer().GetIndexer(), cache.Indexers{
289+
workspaceByURL: indexWorkspaceByURL,
290+
})
341291
}

0 commit comments

Comments
 (0)