Skip to content

Commit 9bbe349

Browse files
authored
fix: Edit deployment action to remove one of two clusters (#293)
1 parent 4ab4b7a commit 9bbe349

File tree

6 files changed

+254
-6
lines changed

6 files changed

+254
-6
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ env/
4747
node_modules/
4848

4949
# Build outputs
50-
*/build/
50+
*/build/*
5151
!*/build/Dockerfile.*
5252
*/out/
5353
*/bin/app-*

app-deployment-manager/build/Dockerfile.gateway

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5-
FROM golang:1.24.9@sha256:5034fa44b36163a4109b71ed75c67dbdcb52c3cd9750953befe00054315d9fd2 AS build
5+
FROM golang:1.25.5@sha256:6cc2338c038bc20f96ab32848da2b5c0641bb9bb5363f2c33e9b7c8838f9a208 AS build
66

77
ENV APP_ROOT=$GOPATH/src/github.com/open-edge-platform/app-orch-deployment/app-deployment-manager
88
ENV CGO_ENABLED=0

app-deployment-manager/controllers/deployment/deployment_controller.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,89 @@ func (r *Reconciler) cleanupOrphanedDeploymentClusters(ctx context.Context, d *v
563563
return nil
564564
}
565565

566+
// cleanupRemovedTargetClustersPhase is a reconcile phase wrapper for cleanupRemovedTargetClusters
567+
func (r *Reconciler) cleanupRemovedTargetClustersPhase(ctx context.Context, d *v1beta1.Deployment) (ctrl.Result, error) {
568+
return ctrl.Result{}, r.cleanupRemovedTargetClusters(ctx, d)
569+
}
570+
571+
// cleanupRemovedTargetClusters removes DeploymentCluster resources for clusters that are no longer
572+
// in the deployment's target list (handles the case when a cluster is removed during edit)
573+
func (r *Reconciler) cleanupRemovedTargetClusters(ctx context.Context, d *v1beta1.Deployment) error {
574+
log := log.FromContext(ctx)
575+
576+
// Only applies to targeted/manual deployments
577+
if d.Spec.DeploymentType != v1beta1.Targeted {
578+
return nil
579+
}
580+
581+
log.V(2).Info("Checking for removed target clusters", "deploymentID", d.GetId(), "deploymentName", d.Name)
582+
583+
// Build a set of current target cluster IDs from the deployment spec
584+
currentTargets := make(map[string]bool)
585+
for _, app := range d.Spec.Applications {
586+
for _, target := range app.Targets {
587+
if clusterName, ok := target[string(v1beta1.ClusterName)]; ok && clusterName != "" {
588+
currentTargets[clusterName] = true
589+
}
590+
}
591+
}
592+
593+
// Find all DeploymentClusters that belong to this deployment
594+
dcList := &v1beta1.DeploymentClusterList{}
595+
labels := map[string]string{
596+
string(v1beta1.DeploymentID): d.GetId(),
597+
}
598+
599+
if err := r.List(ctx, dcList, client.MatchingLabels(labels)); err != nil {
600+
log.Error(err, "Failed to list DeploymentClusters", "deploymentID", d.GetId())
601+
return err
602+
}
603+
604+
if len(dcList.Items) == 0 {
605+
log.V(2).Info("No DeploymentClusters found", "deploymentID", d.GetId())
606+
return nil
607+
}
608+
609+
// Delete DeploymentClusters for clusters no longer in the target list
610+
deletedCount := 0
611+
for i := range dcList.Items {
612+
dc := &dcList.Items[i]
613+
clusterID := dc.Spec.ClusterID
614+
615+
// If this cluster is not in the current targets, delete the DeploymentCluster
616+
if !currentTargets[clusterID] {
617+
log.Info("Deleting DeploymentCluster for removed target cluster",
618+
"deploymentCluster", dc.Name,
619+
"namespace", dc.Namespace,
620+
"deploymentID", d.GetId(),
621+
"clusterID", clusterID)
622+
623+
if err := r.Client.Delete(ctx, dc); err != nil && !apierrors.IsNotFound(err) {
624+
log.Error(err, "Failed to delete DeploymentCluster for removed target",
625+
"deploymentCluster", dc.Name,
626+
"deploymentID", d.GetId(),
627+
"clusterID", clusterID)
628+
return err
629+
}
630+
deletedCount++
631+
log.Info("Successfully deleted DeploymentCluster for removed target",
632+
"deploymentCluster", dc.Name,
633+
"deploymentID", d.GetId(),
634+
"clusterID", clusterID)
635+
}
636+
}
637+
638+
if deletedCount > 0 {
639+
log.Info("Completed cleanup of removed target clusters",
640+
"deleted", deletedCount,
641+
"deploymentID", d.GetId())
642+
} else {
643+
log.V(2).Info("No removed target clusters to cleanup", "deploymentID", d.GetId())
644+
}
645+
646+
return nil
647+
}
648+
566649
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrlRes ctrl.Result, reterr error) {
567650
log := log.FromContext(ctx)
568651

@@ -707,6 +790,7 @@ func (r *Reconciler) delete(ctx context.Context, d *v1beta1.Deployment) (ctrl.Re
707790
func (r *Reconciler) reconcile(ctx context.Context, d *v1beta1.Deployment) (ctrl.Result, error) {
708791
phases := []func(context.Context, *v1beta1.Deployment) (ctrl.Result, error){
709792
r.reconcileState,
793+
r.cleanupRemovedTargetClustersPhase,
710794
r.reconcileDependency,
711795
r.reconcileRepository,
712796
r.reconcileGitRepo,

app-deployment-manager/controllers/deployment/deployment_controller_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/rancher/wrangler/v3/pkg/genericcondition"
2121
batchv1 "k8s.io/api/batch/v1"
2222
v1 "k8s.io/api/core/v1"
23+
apierrors "k8s.io/apimachinery/pkg/api/errors"
2324
"k8s.io/apimachinery/pkg/api/meta"
2425
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2526
"k8s.io/apimachinery/pkg/types"
@@ -1375,4 +1376,156 @@ var _ = Describe("Deployment controller", func() {
13751376
})
13761377
})
13771378
})
1379+
1380+
When("cleaning up removed target clusters", func() {
1381+
var (
1382+
ctx context.Context
1383+
r *Reconciler
1384+
c client.WithWatch
1385+
d v1beta1.Deployment
1386+
dc1 v1beta1.DeploymentCluster
1387+
dc2 v1beta1.DeploymentCluster
1388+
dc3 v1beta1.DeploymentCluster
1389+
)
1390+
1391+
BeforeEach(func() {
1392+
ctx = context.Background()
1393+
1394+
d = v1beta1.Deployment{
1395+
ObjectMeta: metav1.ObjectMeta{
1396+
Name: "test-deployment",
1397+
Namespace: "default",
1398+
UID: "test-uid-12345",
1399+
},
1400+
Spec: v1beta1.DeploymentSpec{
1401+
DeploymentType: v1beta1.Targeted,
1402+
Applications: []v1beta1.Application{
1403+
{
1404+
Name: "app1",
1405+
Version: "1.0.0",
1406+
Namespace: "default",
1407+
Targets: []map[string]string{
1408+
{string(v1beta1.ClusterName): "cluster-1"},
1409+
{string(v1beta1.ClusterName): "cluster-2"},
1410+
},
1411+
},
1412+
},
1413+
},
1414+
}
1415+
1416+
dc1 = v1beta1.DeploymentCluster{
1417+
ObjectMeta: metav1.ObjectMeta{
1418+
Name: "test-deployment-cluster-1",
1419+
Namespace: "default",
1420+
Labels: map[string]string{
1421+
string(v1beta1.BundleName): "test-deployment",
1422+
string(v1beta1.DeploymentID): "test-uid-12345",
1423+
},
1424+
OwnerReferences: []metav1.OwnerReference{
1425+
{APIVersion: "app.edge-orchestrator.intel.com/v1beta1", Kind: "Deployment", Name: "test-deployment", UID: "test-uid-12345"},
1426+
},
1427+
},
1428+
Spec: v1beta1.DeploymentClusterSpec{ClusterID: "cluster-1"},
1429+
}
1430+
1431+
dc2 = v1beta1.DeploymentCluster{
1432+
ObjectMeta: metav1.ObjectMeta{
1433+
Name: "test-deployment-cluster-2",
1434+
Namespace: "default",
1435+
Labels: map[string]string{
1436+
string(v1beta1.BundleName): "test-deployment",
1437+
string(v1beta1.DeploymentID): "test-uid-12345",
1438+
},
1439+
OwnerReferences: []metav1.OwnerReference{
1440+
{APIVersion: "app.edge-orchestrator.intel.com/v1beta1", Kind: "Deployment", Name: "test-deployment", UID: "test-uid-12345"},
1441+
},
1442+
},
1443+
Spec: v1beta1.DeploymentClusterSpec{ClusterID: "cluster-2"},
1444+
}
1445+
1446+
dc3 = v1beta1.DeploymentCluster{
1447+
ObjectMeta: metav1.ObjectMeta{
1448+
Name: "test-deployment-cluster-3",
1449+
Namespace: "default",
1450+
Labels: map[string]string{
1451+
string(v1beta1.BundleName): "test-deployment",
1452+
string(v1beta1.DeploymentID): "test-uid-12345",
1453+
},
1454+
OwnerReferences: []metav1.OwnerReference{
1455+
{APIVersion: "app.edge-orchestrator.intel.com/v1beta1", Kind: "Deployment", Name: "test-deployment", UID: "test-uid-12345"},
1456+
},
1457+
},
1458+
Spec: v1beta1.DeploymentClusterSpec{ClusterID: "cluster-3"},
1459+
}
1460+
1461+
c = fake.NewClientBuilder().
1462+
WithScheme(scheme.Scheme).
1463+
WithObjects(&d, &dc1, &dc2, &dc3).
1464+
WithStatusSubresource(&v1beta1.Deployment{}).
1465+
Build()
1466+
1467+
r = &Reconciler{
1468+
Client: c,
1469+
Scheme: scheme.Scheme,
1470+
recorder: record.NewFakeRecorder(1024),
1471+
}
1472+
})
1473+
1474+
Context("when cluster is removed from targeted deployment", func() {
1475+
It("should delete the DeploymentCluster for removed cluster", func() {
1476+
err := r.cleanupRemovedTargetClusters(ctx, &d)
1477+
Expect(err).NotTo(HaveOccurred())
1478+
1479+
err = r.Get(ctx, types.NamespacedName{Name: dc1.Name, Namespace: dc1.Namespace}, &dc1)
1480+
Expect(err).NotTo(HaveOccurred())
1481+
1482+
err = r.Get(ctx, types.NamespacedName{Name: dc2.Name, Namespace: dc2.Namespace}, &dc2)
1483+
Expect(err).NotTo(HaveOccurred())
1484+
1485+
err = r.Get(ctx, types.NamespacedName{Name: dc3.Name, Namespace: dc3.Namespace}, &dc3)
1486+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
1487+
})
1488+
})
1489+
1490+
Context("when deployment is auto-scaling type", func() {
1491+
It("should not delete any DeploymentClusters", func() {
1492+
d.Spec.DeploymentType = v1beta1.AutoScaling
1493+
err := c.Update(ctx, &d)
1494+
Expect(err).NotTo(HaveOccurred())
1495+
1496+
err = r.cleanupRemovedTargetClusters(ctx, &d)
1497+
Expect(err).NotTo(HaveOccurred())
1498+
1499+
err = r.Get(ctx, types.NamespacedName{Name: dc1.Name, Namespace: dc1.Namespace}, &dc1)
1500+
Expect(err).NotTo(HaveOccurred())
1501+
1502+
err = r.Get(ctx, types.NamespacedName{Name: dc2.Name, Namespace: dc2.Namespace}, &dc2)
1503+
Expect(err).NotTo(HaveOccurred())
1504+
1505+
err = r.Get(ctx, types.NamespacedName{Name: dc3.Name, Namespace: dc3.Namespace}, &dc3)
1506+
Expect(err).NotTo(HaveOccurred())
1507+
})
1508+
})
1509+
1510+
Context("when all clusters are removed from deployment", func() {
1511+
It("should delete all DeploymentClusters", func() {
1512+
d.Spec.Applications[0].Targets = []map[string]string{}
1513+
err := c.Update(ctx, &d)
1514+
Expect(err).NotTo(HaveOccurred())
1515+
1516+
err = r.cleanupRemovedTargetClusters(ctx, &d)
1517+
Expect(err).NotTo(HaveOccurred())
1518+
1519+
err = r.Get(ctx, types.NamespacedName{Name: dc1.Name, Namespace: dc1.Namespace}, &dc1)
1520+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
1521+
1522+
err = r.Get(ctx, types.NamespacedName{Name: dc2.Name, Namespace: dc2.Namespace}, &dc2)
1523+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
1524+
1525+
err = r.Get(ctx, types.NamespacedName{Name: dc3.Name, Namespace: dc3.Namespace}, &dc3)
1526+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
1527+
})
1528+
})
1529+
})
1530+
13781531
})

app-deployment-manager/internal/northbound/northbound.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,16 @@ func (s *DeploymentSvc) CreateDeployment(ctx context.Context, in *deploymentpb.C
962962
return nil, errors.Status(err).Err()
963963
}
964964

965+
// Propagate targets to child deployments (dependencies)
966+
// When the deployment is created with multiple clusters, ensure child deployments
967+
// (dependencies) are also deployed to the same clusters
968+
if len(createInstance.Spec.ChildDeploymentList) > 0 {
969+
err = s.propagateTargetsToChildDeployments(ctx, deploymentCR)
970+
if err != nil {
971+
log.Warnf("failed to propagate targets to child deployments during creation: %v", err)
972+
}
973+
}
974+
965975
utils.LogActivity(ctx, "create", "ADM", "deployment-name "+d.Name, "deploy-id "+d.DeployID, "deployment-app-version "+d.AppVersion)
966976
return &deploymentpb.CreateDeploymentResponse{DeploymentId: d.DeployID}, nil
967977
}

app-deployment-manager/internal/northbound/utils.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,13 @@ func createDeploymentCr(d *Deployment, scenario string, resourceVersion string,
128128
}
129129
}
130130

131-
// For update scenario, start with existing targets from the deployed CR
132-
// This ensures targets accumulate across updates (UI sends one target at a time)
133-
if scenario == "update" && existingDeployment != nil {
131+
// For update scenario:
132+
// - For AutoScaling: accumulate targets (UI sends one label set at a time)
133+
// - For Targeted: replacement logic (UI sends complete desired state)
134+
if scenario == "update" && existingDeployment != nil && d.DeploymentType == string(deploymentv1beta1.AutoScaling) {
134135
for _, existingApp := range existingDeployment.Spec.Applications {
135136
if existingApp.Name == app.Name {
136-
// Start with existing targets
137+
// Start with existing targets for AutoScaling only
137138
targetsList = append(targetsList, existingApp.Targets...)
138139
break
139140
}

0 commit comments

Comments
 (0)