Skip to content

Commit f5a25aa

Browse files
committed
Merge branch 'feat/federated-deployment-scheduling' into feat/datumctl-compute-plugin
2 parents 69c7ff5 + 82955e2 commit f5a25aa

18 files changed

Lines changed: 1648 additions & 110 deletions

api/v1alpha/instance_types.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,26 @@ type SandboxContainer struct {
107107
// +kubebuilder:validation:Required
108108
Image string `json:"image"`
109109

110+
// Entrypoint array to run in the container image, overriding the image's
111+
// ENTRYPOINT. Each element is a separate token, not a shell command — to run a
112+
// shell command use: ["sh", "-c", "my command"].
113+
//
114+
// If not provided, the container image's own ENTRYPOINT is used.
115+
//
116+
// +kubebuilder:validation:Optional
117+
Command []string `json:"command,omitempty"`
118+
119+
// Arguments to the entrypoint, overriding the image's CMD. Combined with
120+
// Command: when Command is also set the resulting invocation is
121+
// append(Command, Args...). When only Args is set it overrides CMD while
122+
// preserving the image's ENTRYPOINT.
123+
//
124+
// If neither Command nor Args is set, the image's own ENTRYPOINT and CMD
125+
// are used unchanged.
126+
//
127+
// +kubebuilder:validation:Optional
128+
Args []string `json:"args,omitempty"`
129+
110130
// List of environment variables to set in the container.
111131
//
112132
// +kubebuilder:validation:Optional

api/v1alpha/labels.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@ const (
1515

1616
InstanceIndexLabel = LabelNamespace + "/instance-index"
1717

18-
// CityCodeLabel carries the city code (e.g. "DFW") that the Instance is
19-
// scheduled to. Stamped at creation time and immutable.
18+
// CityCodeLabel carries the city code of the WorkloadDeployment that owns
19+
// an Instance, matching WorkloadDeploymentSpec.CityCode.
2020
CityCodeLabel = LabelNamespace + "/city-code"
2121

22-
// WorkloadNameLabel carries the name of the Workload that owns this
23-
// Instance. Stamped at creation time and immutable.
22+
// WorkloadNameLabel carries the name of the Workload that an Instance
23+
// ultimately belongs to, sourced from WorkloadDeploymentSpec.WorkloadRef.Name.
2424
WorkloadNameLabel = LabelNamespace + "/workload-name"
2525

26-
// PlacementNameLabel carries the name of the placement entry within the
27-
// Workload spec that produced this Instance. Stamped at creation time and
28-
// immutable.
26+
// PlacementNameLabel carries the placement name from the Workload that drove
27+
// this Instance's deployment, sourced from WorkloadDeploymentSpec.PlacementName.
2928
PlacementNameLabel = LabelNamespace + "/placement-name"
3029
)

api/v1alpha/workloaddeployment_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package v1alpha
22

33
import (
44
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5+
6+
networkingv1alpha "go.datum.net/network-services-operator/api/v1alpha"
57
)
68

79
// WorkloadDeploymentSpec defines the desired state of WorkloadDeployment
@@ -35,6 +37,11 @@ type WorkloadDeploymentSpec struct {
3537

3638
// WorkloadDeploymentStatus defines the observed state of WorkloadDeployment
3739
type WorkloadDeploymentStatus struct {
40+
// The location which the deployment has been scheduled to
41+
//
42+
// +kubebuilder:validation:Optional
43+
Location *networkingv1alpha.LocationReference `json:"location,omitempty"`
44+
3845
// Represents the observations of a deployment's current state.
3946
// Known condition types are: "Available", "Progressing"
4047
Conditions []metav1.Condition `json:"conditions,omitempty"`
@@ -73,6 +80,8 @@ const (
7380
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.readyReplicas`
7481
// +kubebuilder:printcolumn:name="Desired",type=string,JSONPath=`.status.desiredReplicas`
7582
// +kubebuilder:printcolumn:name="Up-to-date",type=string,JSONPath=`.status.currentReplicas`
83+
// +kubebuilder:printcolumn:name="Location Namespace",type=string,JSONPath=`.status.location.namespace`,priority=1
84+
// +kubebuilder:printcolumn:name="Location Name",type=string,JSONPath=`.status.location.name`,priority=1
7685
type WorkloadDeployment struct {
7786
metav1.TypeMeta `json:",inline"`
7887
metav1.ObjectMeta `json:"metadata,omitempty"`

api/v1alpha/zz_generated.deepcopy.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/base/crd/bases/compute.datumapis.com_instances.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,28 @@ spec:
266266
description: A list of containers to run within the sandbox.
267267
items:
268268
properties:
269+
args:
270+
description: |-
271+
Arguments to the entrypoint, overriding the image's CMD. Combined with
272+
Command: when Command is also set the resulting invocation is
273+
append(Command, Args...). When only Args is set it overrides CMD while
274+
preserving the image's ENTRYPOINT.
275+
276+
If neither Command nor Args is set, the image's own ENTRYPOINT and CMD
277+
are used unchanged.
278+
items:
279+
type: string
280+
type: array
281+
command:
282+
description: |-
283+
Entrypoint array to run in the container image, overriding the image's
284+
ENTRYPOINT. Each element is a separate token, not a shell command — to run a
285+
shell command use: ["sh", "-c", "my command"].
286+
287+
If not provided, the container image's own ENTRYPOINT is used.
288+
items:
289+
type: string
290+
type: array
269291
env:
270292
description: |-
271293
List of environment variables to set in the container.

config/base/crd/bases/compute.datumapis.com_workloaddeployments.yaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ spec:
3737
- jsonPath: .status.currentReplicas
3838
name: Up-to-date
3939
type: string
40+
- jsonPath: .status.location.namespace
41+
name: Location Namespace
42+
priority: 1
43+
type: string
44+
- jsonPath: .status.location.name
45+
name: Location Name
46+
priority: 1
47+
type: string
4048
name: v1alpha
4149
schema:
4250
openAPIV3Schema:
@@ -367,6 +375,28 @@ spec:
367375
sandbox.
368376
items:
369377
properties:
378+
args:
379+
description: |-
380+
Arguments to the entrypoint, overriding the image's CMD. Combined with
381+
Command: when Command is also set the resulting invocation is
382+
append(Command, Args...). When only Args is set it overrides CMD while
383+
preserving the image's ENTRYPOINT.
384+
385+
If neither Command nor Args is set, the image's own ENTRYPOINT and CMD
386+
are used unchanged.
387+
items:
388+
type: string
389+
type: array
390+
command:
391+
description: |-
392+
Entrypoint array to run in the container image, overriding the image's
393+
ENTRYPOINT. Each element is a separate token, not a shell command — to run a
394+
shell command use: ["sh", "-c", "my command"].
395+
396+
If not provided, the container image's own ENTRYPOINT is used.
397+
items:
398+
type: string
399+
type: array
370400
env:
371401
description: |-
372402
List of environment variables to set in the container.
@@ -1065,6 +1095,20 @@ spec:
10651095
description: The desired number of instances
10661096
format: int32
10671097
type: integer
1098+
location:
1099+
description: The location which the deployment has been scheduled
1100+
to
1101+
properties:
1102+
name:
1103+
description: Name of a datum location
1104+
type: string
1105+
namespace:
1106+
description: Namespace for the datum location
1107+
type: string
1108+
required:
1109+
- name
1110+
- namespace
1111+
type: object
10681112
readyReplicas:
10691113
description: The number of instances which are ready.
10701114
format: int32

config/base/crd/bases/compute.datumapis.com_workloads.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,28 @@ spec:
385385
sandbox.
386386
items:
387387
properties:
388+
args:
389+
description: |-
390+
Arguments to the entrypoint, overriding the image's CMD. Combined with
391+
Command: when Command is also set the resulting invocation is
392+
append(Command, Args...). When only Args is set it overrides CMD while
393+
preserving the image's ENTRYPOINT.
394+
395+
If neither Command nor Args is set, the image's own ENTRYPOINT and CMD
396+
are used unchanged.
397+
items:
398+
type: string
399+
type: array
400+
command:
401+
description: |-
402+
Entrypoint array to run in the container image, overriding the image's
403+
ENTRYPOINT. Each element is a separate token, not a shell command — to run a
404+
shell command use: ["sh", "-c", "my command"].
405+
406+
If not provided, the container image's own ENTRYPOINT is used.
407+
items:
408+
type: string
409+
type: array
388410
env:
389411
description: |-
390412
List of environment variables to set in the container.

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/karmada-io/api v1.17.0
99
github.com/onsi/ginkgo/v2 v2.27.2
1010
github.com/onsi/gomega v1.38.2
11+
github.com/prometheus/client_golang v1.23.2
1112
github.com/spf13/cobra v1.10.2
1213
github.com/stretchr/testify v1.11.1
1314
go.datum.net/datumctl v0.14.1-0.20260523153711-b44de1c715c1
@@ -21,6 +22,7 @@ require (
2122
k8s.io/api v0.35.3
2223
k8s.io/apimachinery v0.35.3
2324
k8s.io/client-go v0.35.3
25+
k8s.io/component-base v0.35.3
2426
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5
2527
sigs.k8s.io/controller-runtime v0.23.3
2628
sigs.k8s.io/gateway-api v1.3.1-0.20250527223622-54df0a899c1c
@@ -72,7 +74,6 @@ require (
7274
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
7375
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
7476
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
75-
github.com/prometheus/client_golang v1.23.2 // indirect
7677
github.com/prometheus/client_model v0.6.2 // indirect
7778
github.com/prometheus/common v0.66.1 // indirect
7879
github.com/prometheus/procfs v0.17.0 // indirect
@@ -108,7 +109,6 @@ require (
108109
gopkg.in/yaml.v3 v3.0.1 // indirect
109110
k8s.io/apiextensions-apiserver v0.35.3 // indirect
110111
k8s.io/apiserver v0.35.3 // indirect
111-
k8s.io/component-base v0.35.3 // indirect
112112
k8s.io/klog/v2 v2.130.1 // indirect
113113
k8s.io/kube-openapi v0.0.0-20260330154417-16be699c7b31 // indirect
114114
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect

internal/controller/instance_controller.go

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package controller
55
import (
66
"context"
77
"fmt"
8+
"maps"
89
"strings"
910

1011
corev1 "k8s.io/api/core/v1"
@@ -221,15 +222,19 @@ func (r *InstanceReconciler) Reconcile(ctx context.Context, req mcreconcile.Requ
221222
if err := cl.GetClient().Status().Update(ctx, &instance); err != nil {
222223
return ctrl.Result{}, err
223224
}
224-
if err := r.writeBackToUpstream(ctx, req.ClusterName, &instance); err != nil {
225-
return ctrl.Result{}, err
225+
// Return with the quota error (nil or transient) so controller-runtime
226+
// requeues with backoff on failures. On the success path (quotaErr==nil)
227+
// we fall through to removeQuotaSchedulingGate below instead of returning
228+
// early, so the gate is cleared in the same reconcile pass rather than
229+
// waiting for a requeue that may never come (ResourceClaim is immutable
230+
// and local Instances are not watched).
231+
if quotaErr != nil {
232+
if err := r.writeBackToUpstream(ctx, req.ClusterName, &instance); err != nil {
233+
return ctrl.Result{}, err
234+
}
235+
return ctrl.Result{}, quotaErr
226236
}
227-
// Return after the status update. If there was a quota error, return it
228-
// so controller-runtime requeues with backoff for transient failures.
229-
return ctrl.Result{}, quotaErr
230-
}
231-
232-
if quotaErr != nil {
237+
} else if quotaErr != nil {
233238
// No status change but quota evaluation failed — return error to requeue.
234239
return ctrl.Result{}, quotaErr
235240
}
@@ -470,13 +475,37 @@ func (r *InstanceReconciler) writeBackToUpstream(ctx context.Context, clusterNam
470475
}
471476
}
472477

478+
logger := log.FromContext(ctx)
479+
missingLabels := []string{}
480+
for _, key := range []string{
481+
computev1alpha.WorkloadUIDLabel,
482+
computev1alpha.WorkloadDeploymentUIDLabel,
483+
computev1alpha.InstanceIndexLabel,
484+
} {
485+
if instance.Labels[key] == "" {
486+
missingLabels = append(missingLabels, key)
487+
}
488+
}
489+
if len(missingLabels) > 0 {
490+
logger.Info("instance is missing linking labels for write-back; projection owner-ref will not be set",
491+
"instance", instance.Name, "namespace", instance.Namespace,
492+
"missingLabels", missingLabels)
493+
}
494+
473495
writeBack := &computev1alpha.Instance{
474496
ObjectMeta: metav1.ObjectMeta{
475497
Name: instance.Name,
476498
Namespace: instance.Namespace,
477499
Labels: map[string]string{
478500
downstreamclient.UpstreamOwnerClusterNameLabel: encodedClusterName,
479501
downstreamclient.UpstreamOwnerNamespaceLabel: upstreamNamespace,
502+
computev1alpha.WorkloadUIDLabel: instance.Labels[computev1alpha.WorkloadUIDLabel],
503+
computev1alpha.WorkloadDeploymentUIDLabel: instance.Labels[computev1alpha.WorkloadDeploymentUIDLabel],
504+
computev1alpha.InstanceIndexLabel: instance.Labels[computev1alpha.InstanceIndexLabel],
505+
computev1alpha.WorkloadDeploymentNameLabel: instance.Labels[computev1alpha.WorkloadDeploymentNameLabel],
506+
computev1alpha.CityCodeLabel: instance.Labels[computev1alpha.CityCodeLabel],
507+
computev1alpha.WorkloadNameLabel: instance.Labels[computev1alpha.WorkloadNameLabel],
508+
computev1alpha.PlacementNameLabel: instance.Labels[computev1alpha.PlacementNameLabel],
480509
},
481510
},
482511
Spec: instance.Spec,
@@ -503,11 +532,24 @@ func (r *InstanceReconciler) writeBackToUpstream(ctx context.Context, clusterNam
503532
return fmt.Errorf("failed getting downstream instance: %w", err)
504533
}
505534

506-
// Update spec + labels only if they differ.
535+
// Build a comparable map containing only the keys this function owns so that
536+
// Karmada-managed labels on the existing object do not cause spurious updates.
537+
ownedLabels := make(map[string]string, len(writeBack.Labels))
538+
for k := range writeBack.Labels {
539+
ownedLabels[k] = existing.Labels[k]
540+
}
541+
542+
// Update spec + labels only if owned keys differ.
507543
if !apiequality.Semantic.DeepEqual(existing.Spec, instance.Spec) ||
508-
!apiequality.Semantic.DeepEqual(existing.Labels, writeBack.Labels) {
544+
!apiequality.Semantic.DeepEqual(ownedLabels, writeBack.Labels) {
509545
existing.Spec = instance.Spec
510-
existing.Labels = writeBack.Labels
546+
// Merge writeBack.Labels into existing.Labels. Only keys owned by
547+
// writeBackToUpstream are written; any labels Karmada or other actors
548+
// have placed on the downstream object are preserved.
549+
if existing.Labels == nil {
550+
existing.Labels = make(map[string]string)
551+
}
552+
maps.Copy(existing.Labels, writeBack.Labels)
511553
if err := r.FederationClient.Update(ctx, existing); err != nil {
512554
return fmt.Errorf("failed updating downstream write-back instance: %w", err)
513555
}

0 commit comments

Comments
 (0)