diff --git a/config/crds/tenancy.kcp.io_workspaces.yaml b/config/crds/tenancy.kcp.io_workspaces.yaml index 6eec08278c0..934a7832c42 100644 --- a/config/crds/tenancy.kcp.io_workspaces.yaml +++ b/config/crds/tenancy.kcp.io_workspaces.yaml @@ -147,6 +147,55 @@ spec: type: object x-kubernetes-map-type: atomic type: object + mount: + description: |- + Mount is a reference to a mount that is used to mount the workspace. + If specified, logicalcluster will not be created and the workspace will be mounted + using reference mount object. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: mount is immutable + rule: self == oldSelf type: description: |- type defines properties of the workspace both on creation (e.g. initial @@ -184,6 +233,8 @@ spec: rule: '!has(oldSelf.URL) || has(self.URL)' - message: cluster cannot be unset rule: '!has(oldSelf.cluster) || has(self.cluster)' + - message: spec.mount and spec.type cannot both be set + rule: '!(has(self.mount) && has(self.type))' status: default: {} description: WorkspaceStatus communicates the observed state of the Workspace. diff --git a/config/root-phase0/apiexport-tenancy.kcp.io.yaml b/config/root-phase0/apiexport-tenancy.kcp.io.yaml index 6f3a5912fae..3749a94d968 100644 --- a/config/root-phase0/apiexport-tenancy.kcp.io.yaml +++ b/config/root-phase0/apiexport-tenancy.kcp.io.yaml @@ -6,7 +6,7 @@ metadata: spec: latestResourceSchemas: - v240903-d6797056a.workspacetypes.tenancy.kcp.io - - v241020-fce06d31d.workspaces.tenancy.kcp.io + - v250202-4c2d1061d.workspaces.tenancy.kcp.io maximalPermissionPolicy: local: {} status: {} diff --git a/config/root-phase0/apiresourceschema-workspaces.tenancy.kcp.io.yaml b/config/root-phase0/apiresourceschema-workspaces.tenancy.kcp.io.yaml index bb58fc43ac5..64bdb8bbc98 100644 --- a/config/root-phase0/apiresourceschema-workspaces.tenancy.kcp.io.yaml +++ b/config/root-phase0/apiresourceschema-workspaces.tenancy.kcp.io.yaml @@ -2,7 +2,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: creationTimestamp: null - name: v241020-fce06d31d.workspaces.tenancy.kcp.io + name: v250202-4c2d1061d.workspaces.tenancy.kcp.io spec: group: tenancy.kcp.io names: @@ -145,6 +145,55 @@ spec: type: object x-kubernetes-map-type: atomic type: object + mount: + description: |- + Mount is a reference to a mount that is used to mount the workspace. + If specified, logicalcluster will not be created and the workspace will be mounted + using reference mount object. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: mount is immutable + rule: self == oldSelf type: description: |- type defines properties of the workspace both on creation (e.g. initial @@ -182,6 +231,8 @@ spec: rule: '!has(oldSelf.URL) || has(self.URL)' - message: cluster cannot be unset rule: '!has(oldSelf.cluster) || has(self.cluster)' + - message: spec.mount and spec.type cannot both be set + rule: '!(has(self.mount) && has(self.type))' status: default: {} description: WorkspaceStatus communicates the observed state of the Workspace. diff --git a/mounts/crd.yaml b/mounts/crd.yaml new file mode 100644 index 00000000000..7eb9c15b70c --- /dev/null +++ b/mounts/crd.yaml @@ -0,0 +1,144 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: kubeclusters.contrib.kcp.io +spec: + group: contrib.kcp.io + names: + kind: KubeCluster + listKind: KubeClusterList + plural: kubeclusters + singular: kubecluster + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="KubeClusterReady")].status + name: Ready + type: string + - jsonPath: .status.phase + name: Phase + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: KubeCluster describes the current KubeCluster proxy object. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: KubeClusterSpec is the specification of the Kube cluster + proxy resource. + properties: + workspaceURL: + description: workspaceURL is the address under which the workspace + can be found. + type: string + type: object + status: + description: KubeClusterStatus communicates the observed state of the + Kube cluster proxy. + properties: + URL: + description: |- + url is the address under which the Kubernetes-cluster-like endpoint + can be found. This URL can be used to access the cluster with standard Kubernetes + client libraries and command line tools via proxy. + type: string + conditions: + description: Current processing state of the Cluster proxy. + items: + description: Condition defines an observation of a object operational + state. + properties: + lastTransitionTime: + description: |- + Last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + A human readable message indicating details about the transition. + This field may be empty. + type: string + reason: + description: |- + The reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may not be empty. + type: string + severity: + description: |- + Severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + lastProxyHeartbeatTime: + description: A timestamp indicating when the proxy last reported status. + format: date-time + type: string + phase: + default: Initializing + description: Phase of the cluster proxy (Initializing, Ready). + enum: + - Initializing + - Connecting + - Ready + - Unknown + type: string + tunnelWorkspaces: + description: |- + TunnelWorkspaces contains all URLs (one per shard) that is being used + by the proxy to connect to the tunnel for a given shard. + items: + properties: + url: + description: |- + url is the URL the Proxy should use to connect + to the Proxy tunnel for a given shard. + minLength: 1 + type: string + required: + - url + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/mounts/mount.yaml b/mounts/mount.yaml new file mode 100644 index 00000000000..a7c211a0bdc --- /dev/null +++ b/mounts/mount.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: contrib.kcp.io/v1alpha1 +kind: KubeCluster +metadata: + name: proxy-cluster +spec: diff --git a/mounts/ws.yaml b/mounts/ws.yaml new file mode 100644 index 00000000000..00010f1d9b0 --- /dev/null +++ b/mounts/ws.yaml @@ -0,0 +1,9 @@ +apiVersion: tenancy.kcp.io/v1alpha1 +kind: Workspace +metadata: + name: mount +spec: + mount: + name: proxy-cluster + kind: KubeCluster + apiVersion: contrib.kcp.io/v1alpha1 diff --git a/pkg/admission/workspace/admission_test.go b/pkg/admission/workspace/admission_test.go index f7752097dff..cf3a77e0ab8 100644 --- a/pkg/admission/workspace/admission_test.go +++ b/pkg/admission/workspace/admission_test.go @@ -105,7 +105,7 @@ func TestAdmit(t *testing.T) { Name: "test", }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -126,7 +126,7 @@ func TestAdmit(t *testing.T) { }, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -147,7 +147,7 @@ func TestAdmit(t *testing.T) { }, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "Foo", Path: "root:org", }, @@ -168,7 +168,7 @@ func TestAdmit(t *testing.T) { }, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "Foo", Path: "root:org", }, @@ -189,7 +189,7 @@ func TestAdmit(t *testing.T) { }, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "Foo", Path: "root:org", }, @@ -210,7 +210,7 @@ func TestAdmit(t *testing.T) { }, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "Foo", Path: "root:org", }, @@ -334,7 +334,7 @@ func TestValidate(t *testing.T) { Annotations: map[string]string{"experimental.tenancy.kcp.io/owner": "{}"}, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -346,7 +346,7 @@ func TestValidate(t *testing.T) { Annotations: map[string]string{"experimental.tenancy.kcp.io/owner": "{}"}, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "universal", Path: "root:org", }, @@ -365,7 +365,7 @@ func TestValidate(t *testing.T) { Annotations: map[string]string{"experimental.tenancy.kcp.io/owner": "{}"}, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -378,7 +378,7 @@ func TestValidate(t *testing.T) { }, Spec: tenancyv1alpha1.WorkspaceSpec{ Cluster: "somewhere", - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -399,7 +399,7 @@ func TestValidate(t *testing.T) { Spec: tenancyv1alpha1.WorkspaceSpec{ Cluster: "somewhere", URL: "https://kcp.bigcorp.com/clusters/org:test", - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -415,7 +415,7 @@ func TestValidate(t *testing.T) { Annotations: map[string]string{"experimental.tenancy.kcp.io/owner": "{}"}, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -439,7 +439,7 @@ func TestValidate(t *testing.T) { Spec: tenancyv1alpha1.WorkspaceSpec{ Cluster: "somewhere", URL: "https://kcp.bigcorp.com/clusters/org:test", - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -463,7 +463,7 @@ func TestValidate(t *testing.T) { Spec: tenancyv1alpha1.WorkspaceSpec{ Cluster: "somewhere", URL: "https://kcp.bigcorp.com/clusters/org:test", - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -488,7 +488,7 @@ func TestValidate(t *testing.T) { Spec: tenancyv1alpha1.WorkspaceSpec{ Cluster: "somewhere", URL: "https://kcp.bigcorp.com/clusters/org:test", - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -506,7 +506,7 @@ func TestValidate(t *testing.T) { Spec: tenancyv1alpha1.WorkspaceSpec{ Cluster: "somewhere", URL: "https://kcp.otherbigcorp.com/clusters/org:test", - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -530,7 +530,7 @@ func TestValidate(t *testing.T) { }, Spec: tenancyv1alpha1.WorkspaceSpec{ Cluster: "somewhere", - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -546,7 +546,7 @@ func TestValidate(t *testing.T) { Annotations: map[string]string{"experimental.tenancy.kcp.io/owner": "{}"}, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -593,7 +593,7 @@ func TestValidate(t *testing.T) { Annotations: map[string]string{"experimental.tenancy.kcp.io/owner": "{}"}, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "foo", Path: "root:org", }, @@ -621,7 +621,7 @@ func TestValidate(t *testing.T) { }, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "Foo", Path: "root:org", }, @@ -651,7 +651,7 @@ func TestValidate(t *testing.T) { }, }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: "Foo", Path: "root:org", }, diff --git a/pkg/admission/workspacetypeexists/admission.go b/pkg/admission/workspacetypeexists/admission.go index dbe39c5ee98..f2744e3269d 100644 --- a/pkg/admission/workspacetypeexists/admission.go +++ b/pkg/admission/workspacetypeexists/admission.go @@ -23,6 +23,7 @@ import ( "io" "strings" + "github.com/davecgh/go-spew/spew" kcpkubernetesclientset "github.com/kcp-dev/client-go/kubernetes" "github.com/kcp-dev/logicalcluster/v3" @@ -35,6 +36,7 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/client-go/tools/cache" + "k8s.io/utils/ptr" kcpinitializers "github.com/kcp-dev/kcp/pkg/admission/initializers" "github.com/kcp-dev/kcp/pkg/authorization/delegated" @@ -118,6 +120,10 @@ func (o *workspacetypeExists) Admit(ctx context.Context, a admission.Attributes, return fmt.Errorf("failed to convert unstructured to Workspace: %w", err) } + if ws.Spec.Mount != nil { + return nil + } + if !o.WaitForReady() { return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) } @@ -132,8 +138,9 @@ func (o *workspacetypeExists) Admit(ctx context.Context, a admission.Attributes, } // if the user has not provided any type, use the default from the parent workspace - empty := tenancyv1alpha1.WorkspaceTypeReference{} - if ws.Spec.Type == empty { + // We default to workspaceType only if not mount. + spew.Dump(ws.Spec.Type) + if ws.Spec.Type == nil || *ws.Spec.Type == (tenancyv1alpha1.WorkspaceTypeReference{}) { typeAnnotation, found := logicalCluster.Annotations[tenancyv1alpha1.LogicalClusterTypeAnnotationKey] if !found { return admission.NewForbidden(a, fmt.Errorf("annotation %s on LogicalCluster must be set", tenancyv1alpha1.LogicalClusterTypeAnnotationKey)) @@ -149,10 +156,11 @@ func (o *workspacetypeExists) Admit(ctx context.Context, a admission.Attributes, if parentWt.Spec.DefaultChildWorkspaceType == nil { return admission.NewForbidden(a, errors.New("spec.defaultChildWorkspaceType of workspace type %s:%s must be set")) } - ws.Spec.Type = tenancyv1alpha1.WorkspaceTypeReference{ + ws.Spec.Type = &tenancyv1alpha1.WorkspaceTypeReference{ Path: parentWt.Spec.DefaultChildWorkspaceType.Path, Name: parentWt.Spec.DefaultChildWorkspaceType.Name, } + spew.Dump(ws) } thisPath := logicalCluster.Annotations[core.LogicalClusterPathAnnotationKey] @@ -231,6 +239,11 @@ func (o *workspacetypeExists) Validate(ctx context.Context, a admission.Attribut return fmt.Errorf("failed to convert unstructured to Workspace: %w", err) } + spew.Dump(ws) + if ws.Spec.Mount != nil { + return nil + } + switch a.GetOperation() { case admission.Update: if a.GetOldObject().GetObjectKind().GroupVersionKind() != tenancyv1alpha1.SchemeGroupVersion.WithKind("Workspace") { @@ -245,7 +258,7 @@ func (o *workspacetypeExists) Validate(ctx context.Context, a admission.Attribut return fmt.Errorf("failed to convert unstructured to Workspace: %w", err) } - if old.Spec.Type != ws.Spec.Type { + if !ptr.Equal[tenancyv1alpha1.WorkspaceTypeReference](old.Spec.Type, ws.Spec.Type) { return admission.NewForbidden(a, errors.New("spec.type is immutable")) } case admission.Create: diff --git a/pkg/admission/workspacetypeexists/admission_test.go b/pkg/admission/workspacetypeexists/admission_test.go index 5b3645f7857..01d9f52757d 100644 --- a/pkg/admission/workspacetypeexists/admission_test.go +++ b/pkg/admission/workspacetypeexists/admission_test.go @@ -875,7 +875,7 @@ func newWorkspace(qualifiedName string) wsBuilder { func (b wsBuilder) withType(qualifiedName string) wsBuilder { path, name := logicalcluster.NewPath(qualifiedName).Split() - b.Spec.Type = tenancyv1alpha1.WorkspaceTypeReference{ + b.Spec.Type = &tenancyv1alpha1.WorkspaceTypeReference{ Path: path.String(), Name: tenancyv1alpha1.WorkspaceTypeName(name), } diff --git a/pkg/openapi/zz_generated.openapi.go b/pkg/openapi/zz_generated.openapi.go index d8dc9f7be05..2be8faff2f3 100644 --- a/pkg/openapi/zz_generated.openapi.go +++ b/pkg/openapi/zz_generated.openapi.go @@ -2540,7 +2540,6 @@ func schema_sdk_apis_tenancy_v1alpha1_WorkspaceSpec(ref common.ReferenceCallback "type": { SchemaProps: spec.SchemaProps{ Description: "type defines properties of the workspace both on creation (e.g. initial resources and initially installed APIs) and during runtime (e.g. permissions). If no type is provided, the default type for the workspace in which this workspace is nesting will be used.\n\nThe type is a reference to a WorkspaceType in the listed workspace, but lower-cased. The WorkspaceType existence is validated at admission during creation. The type is immutable after creation. The use of a type is gated via the RBAC workspacetypes/use resource permission.", - Default: map[string]interface{}{}, Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceTypeReference"), }, }, @@ -2564,11 +2563,17 @@ func schema_sdk_apis_tenancy_v1alpha1_WorkspaceSpec(ref common.ReferenceCallback Format: "", }, }, + "mount": { + SchemaProps: spec.SchemaProps{ + Description: "Mount is a reference to a mount that is used to mount the workspace. If specified, logicalcluster will not be created and the workspace will be mounted using reference mount object.", + Ref: ref("k8s.io/api/core/v1.ObjectReference"), + }, + }, }, }, }, Dependencies: []string{ - "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceLocation", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceTypeReference"}, + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceLocation", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceTypeReference", "k8s.io/api/core/v1.ObjectReference"}, } } diff --git a/pkg/reconciler/tenancy/workspace/workspace_reconcile_metadata.go b/pkg/reconciler/tenancy/workspace/workspace_reconcile_metadata.go index 4a3d556b2d3..bbe5d11c58f 100644 --- a/pkg/reconciler/tenancy/workspace/workspace_reconcile_metadata.go +++ b/pkg/reconciler/tenancy/workspace/workspace_reconcile_metadata.go @@ -32,6 +32,9 @@ type metaDataReconciler struct { func (r *metaDataReconciler) reconcile(ctx context.Context, workspace *tenancyv1alpha1.Workspace) (reconcileStatus, error) { logger := klog.FromContext(ctx).WithValues("reconciler", "metadata") + if workspace.Spec.Mount != nil { + return reconcileStatusContinue, nil + } changed := false expected := string(workspace.Status.Phase) diff --git a/pkg/reconciler/tenancy/workspace/workspace_reconcile_phase.go b/pkg/reconciler/tenancy/workspace/workspace_reconcile_phase.go index e5b82c380a8..9627495846e 100644 --- a/pkg/reconciler/tenancy/workspace/workspace_reconcile_phase.go +++ b/pkg/reconciler/tenancy/workspace/workspace_reconcile_phase.go @@ -41,6 +41,9 @@ type phaseReconciler struct { func (r *phaseReconciler) reconcile(ctx context.Context, workspace *tenancyv1alpha1.Workspace) (reconcileStatus, error) { logger := klog.FromContext(ctx).WithValues("reconciler", "phase") + if workspace.Spec.Mount != nil { + return reconcileStatusContinue, nil + } switch workspace.Status.Phase { case corev1alpha1.LogicalClusterPhaseScheduling: diff --git a/pkg/reconciler/tenancy/workspace/workspace_reconcile_scheduling.go b/pkg/reconciler/tenancy/workspace/workspace_reconcile_scheduling.go index 109ec3f5d7d..d4c10d3ef24 100644 --- a/pkg/reconciler/tenancy/workspace/workspace_reconcile_scheduling.go +++ b/pkg/reconciler/tenancy/workspace/workspace_reconcile_scheduling.go @@ -82,6 +82,9 @@ type schedulingReconciler struct { func (r *schedulingReconciler) reconcile(ctx context.Context, workspace *tenancyv1alpha1.Workspace) (reconcileStatus, error) { logger := klog.FromContext(ctx).WithValues("reconciler", "scheduling") + if workspace.Spec.Mount != nil { + return reconcileStatusContinue, nil + } switch { case !workspace.DeletionTimestamp.IsZero(): diff --git a/pkg/reconciler/tenancy/workspace/workspace_reconcile_scheduling_test.go b/pkg/reconciler/tenancy/workspace/workspace_reconcile_scheduling_test.go index 357484be3a8..ae293cf8f81 100644 --- a/pkg/reconciler/tenancy/workspace/workspace_reconcile_scheduling_test.go +++ b/pkg/reconciler/tenancy/workspace/workspace_reconcile_scheduling_test.go @@ -399,7 +399,7 @@ func wellKnownFooWSForPhaseTwo() *tenancyv1alpha1.Workspace { ws.Annotations["experimental.tenancy.kcp.io/owner"] = `{"username":"kcp-admin"}` ws.Finalizers = append(ws.Finalizers, "core.kcp.io/logicalcluster") // type info is assigned by an admission plugin - ws.Spec.Type = tenancyv1alpha1.WorkspaceTypeReference{ + ws.Spec.Type = &tenancyv1alpha1.WorkspaceTypeReference{ Name: "universal", Path: "root", } diff --git a/sdk/apis/tenancy/v1alpha1/types_workspace.go b/sdk/apis/tenancy/v1alpha1/types_workspace.go index a9a12d846c6..96c7788252c 100644 --- a/sdk/apis/tenancy/v1alpha1/types_workspace.go +++ b/sdk/apis/tenancy/v1alpha1/types_workspace.go @@ -19,6 +19,7 @@ package v1alpha1 import ( "fmt" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" @@ -134,6 +135,7 @@ type Workspace struct { // WorkspaceSpec holds the desired state of the Workspace. // +kubebuilder:validation:XValidation:rule="!has(oldSelf.URL) || has(self.URL)",message="URL cannot be unset" // +kubebuilder:validation:XValidation:rule="!has(oldSelf.cluster) || has(self.cluster)",message="cluster cannot be unset" +// +kubebuilder:validation:XValidation:rule="!(has(self.mount) && has(self.type))",message="spec.mount and spec.type cannot both be set" type WorkspaceSpec struct { // type defines properties of the workspace both on creation (e.g. initial // resources and initially installed APIs) and during runtime (e.g. permissions). @@ -149,7 +151,7 @@ type WorkspaceSpec struct { // +kubebuilder:validation:XValidation:rule="self.name == oldSelf.name",message="name is immutable" // +kubebuilder:validation:XValidation:rule="has(oldSelf.path) == has(self.path)",message="path is immutable" // +kubebuilder:validation:XValidation:rule="!has(oldSelf.path) || !has(self.path) || self.path == oldSelf.path",message="path is immutable" - Type WorkspaceTypeReference `json:"type,omitempty"` + Type *WorkspaceTypeReference `json:"type,omitempty"` // location constraints where this workspace can be scheduled to. // @@ -174,6 +176,14 @@ type WorkspaceSpec struct { // // +kubebuilder:format:uri URL string `json:"URL,omitempty"` + + // Mount is a reference to a mount that is used to mount the workspace. + // If specified, logicalcluster will not be created and the workspace will be mounted + // using reference mount object. + // + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="mount is immutable" + Mount *corev1.ObjectReference `json:"mount,omitempty"` } type WorkspaceLocation struct { diff --git a/sdk/apis/tenancy/v1alpha1/zz_generated.deepcopy.go b/sdk/apis/tenancy/v1alpha1/zz_generated.deepcopy.go index 63ee47fd361..fa7814b0569 100644 --- a/sdk/apis/tenancy/v1alpha1/zz_generated.deepcopy.go +++ b/sdk/apis/tenancy/v1alpha1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -224,12 +225,21 @@ func (in *WorkspaceLocation) DeepCopy() *WorkspaceLocation { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkspaceSpec) DeepCopyInto(out *WorkspaceSpec) { *out = *in - out.Type = in.Type + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(WorkspaceTypeReference) + **out = **in + } if in.Location != nil { in, out := &in.Location, &out.Location *out = new(WorkspaceLocation) (*in).DeepCopyInto(*out) } + if in.Mount != nil { + in, out := &in.Mount, &out.Mount + *out = new(corev1.ObjectReference) + **out = **in + } return } diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/workspacespec.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/workspacespec.go index f9e649d53d3..15c6488c557 100644 --- a/sdk/client/applyconfiguration/tenancy/v1alpha1/workspacespec.go +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/workspacespec.go @@ -18,6 +18,10 @@ limitations under the License. package v1alpha1 +import ( + v1 "k8s.io/api/core/v1" +) + // WorkspaceSpecApplyConfiguration represents a declarative configuration of the WorkspaceSpec type for use // with apply. type WorkspaceSpecApplyConfiguration struct { @@ -25,6 +29,7 @@ type WorkspaceSpecApplyConfiguration struct { Location *WorkspaceLocationApplyConfiguration `json:"location,omitempty"` Cluster *string `json:"cluster,omitempty"` URL *string `json:"URL,omitempty"` + Mount *v1.ObjectReference `json:"mount,omitempty"` } // WorkspaceSpecApplyConfiguration constructs a declarative configuration of the WorkspaceSpec type for use with @@ -64,3 +69,11 @@ func (b *WorkspaceSpecApplyConfiguration) WithURL(value string) *WorkspaceSpecAp b.URL = &value return b } + +// WithMount sets the Mount field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Mount field is set to the value of the last call. +func (b *WorkspaceSpecApplyConfiguration) WithMount(value v1.ObjectReference) *WorkspaceSpecApplyConfiguration { + b.Mount = &value + return b +} diff --git a/test/e2e/framework/workspaces.go b/test/e2e/framework/workspaces.go index bebb8009beb..acb363da303 100644 --- a/test/e2e/framework/workspaces.go +++ b/test/e2e/framework/workspaces.go @@ -104,7 +104,7 @@ func WithRequiredGroups(groups ...string) PrivilegedWorkspaceOption { func WithType(path logicalcluster.Path, name tenancyv1alpha1.WorkspaceTypeName) UnprivilegedWorkspaceOption { return func(ws *tenancyv1alpha1.Workspace) { - ws.Spec.Type = tenancyv1alpha1.WorkspaceTypeReference{ + ws.Spec.Type = &tenancyv1alpha1.WorkspaceTypeReference{ Name: name, Path: path.String(), } @@ -135,7 +135,7 @@ func newWorkspaceFixture[O WorkspaceOption](t *testing.T, createClusterClient, c GenerateName: "e2e-workspace-", }, Spec: tenancyv1alpha1.WorkspaceSpec{ - Type: tenancyv1alpha1.WorkspaceTypeReference{ + Type: &tenancyv1alpha1.WorkspaceTypeReference{ Name: tenancyv1alpha1.WorkspaceTypeName("universal"), Path: "root", },