Skip to content

Commit 353ce37

Browse files
committed
Reject affinity update on running cluster
1 parent 40e7807 commit 353ce37

File tree

6 files changed

+422
-3
lines changed

6 files changed

+422
-3
lines changed

plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterEventTypes.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ public class KubernetesClusterEventTypes {
2525
public static final String EVENT_KUBERNETES_CLUSTER_UPGRADE = "KUBERNETES.CLUSTER.UPGRADE";
2626
public static final String EVENT_KUBERNETES_CLUSTER_NODES_ADD = "KUBERNETES.CLUSTER.NODES.ADD";
2727
public static final String EVENT_KUBERNETES_CLUSTER_NODES_REMOVE = "KUBERNETES.CLUSTER.NODES.REMOVE";
28+
public static final String EVENT_KUBERNETES_CLUSTER_AFFINITY_UPDATE = "KUBERNETES.CLUSTER.AFFINITY.UPDATE";
2829
}

plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import org.apache.cloudstack.acl.SecurityChecker;
5959
import org.apache.cloudstack.affinity.AffinityGroupVO;
6060
import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
61+
import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao;
6162
import org.apache.cloudstack.annotation.AnnotationService;
6263
import org.apache.cloudstack.annotation.dao.AnnotationDao;
6364
import org.apache.cloudstack.api.ApiCommandResourceType;
@@ -86,6 +87,7 @@
8687
import org.apache.cloudstack.api.command.user.kubernetes.cluster.ScaleKubernetesClusterCmd;
8788
import org.apache.cloudstack.api.command.user.kubernetes.cluster.StartKubernetesClusterCmd;
8889
import org.apache.cloudstack.api.command.user.kubernetes.cluster.StopKubernetesClusterCmd;
90+
import org.apache.cloudstack.api.command.user.kubernetes.cluster.UpdateKubernetesClusterAffinityGroupCmd;
8991
import org.apache.cloudstack.api.command.user.kubernetes.cluster.UpgradeKubernetesClusterCmd;
9092
import org.apache.cloudstack.api.command.user.loadbalancer.AssignToLoadBalancerRuleCmd;
9193
import org.apache.cloudstack.api.command.user.loadbalancer.CreateLoadBalancerRuleCmd;
@@ -332,6 +334,8 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne
332334
@Inject
333335
protected AffinityGroupDao affinityGroupDao;
334336
@Inject
337+
protected AffinityGroupVMMapDao affinityGroupVMMapDao;
338+
@Inject
335339
protected ServiceOfferingDao serviceOfferingDao;
336340
@Inject
337341
protected UserDataDao userDataDao;
@@ -952,7 +956,7 @@ protected void setNodeTypeAffinityGroupResponse(KubernetesClusterResponse respon
952956

953957
protected void setAffinityGroupResponseForNodeType(KubernetesClusterResponse response, long clusterId, String nodeType) {
954958
List<Long> affinityGroupIds = kubernetesClusterAffinityGroupMapDao.listAffinityGroupIdsByClusterIdAndNodeType(clusterId, nodeType);
955-
if (affinityGroupIds == null || affinityGroupIds.isEmpty()) {
959+
if (CollectionUtils.isEmpty(affinityGroupIds)) {
956960
return;
957961
}
958962
List<String> affinityGroupUuids = new ArrayList<>();
@@ -2313,6 +2317,94 @@ public boolean upgradeKubernetesCluster(UpgradeKubernetesClusterCmd cmd) throws
23132317
return upgradeWorker.upgradeCluster();
23142318
}
23152319

2320+
@Override
2321+
@ActionEvent(eventType = KubernetesClusterEventTypes.EVENT_KUBERNETES_CLUSTER_AFFINITY_UPDATE,
2322+
eventDescription = "updating Kubernetes cluster affinity groups")
2323+
public boolean updateKubernetesClusterAffinityGroups(UpdateKubernetesClusterAffinityGroupCmd cmd) throws CloudRuntimeException {
2324+
if (!KubernetesServiceEnabled.value()) {
2325+
logAndThrow(Level.ERROR, "Kubernetes Service plugin is disabled");
2326+
}
2327+
KubernetesClusterVO kubernetesCluster = validateClusterForAffinityGroupUpdate(cmd.getId());
2328+
Map<String, List<Long>> affinityGroupNodeTypeMap = cmd.getAffinityGroupNodeTypeMap();
2329+
validateNodeAffinityGroups(affinityGroupNodeTypeMap, kubernetesCluster.getAccountId());
2330+
2331+
final Long clusterId = kubernetesCluster.getId();
2332+
Transaction.execute(new TransactionCallbackNoReturn() {
2333+
@Override
2334+
public void doInTransactionWithoutResult(TransactionStatus status) {
2335+
kubernetesClusterAffinityGroupMapDao.removeByClusterId(clusterId);
2336+
persistAffinityGroupMappings(clusterId, affinityGroupNodeTypeMap);
2337+
syncVmAffinityGroups(clusterId, affinityGroupNodeTypeMap);
2338+
}
2339+
});
2340+
logger.info("Updated affinity groups for Kubernetes cluster {}", kubernetesCluster.getName());
2341+
return true;
2342+
}
2343+
2344+
private KubernetesClusterVO validateClusterForAffinityGroupUpdate(Long clusterId) {
2345+
KubernetesClusterVO kubernetesCluster = kubernetesClusterDao.findById(clusterId);
2346+
if (Objects.isNull(kubernetesCluster) || Objects.nonNull(kubernetesCluster.getRemoved())) {
2347+
throw new InvalidParameterValueException("Invalid Kubernetes cluster ID");
2348+
}
2349+
if (!KubernetesCluster.ClusterType.CloudManaged.equals(kubernetesCluster.getClusterType())) {
2350+
throw new InvalidParameterValueException("Affinity groups can only be updated for CloudManaged Kubernetes clusters");
2351+
}
2352+
if (!KubernetesCluster.State.Stopped.equals(kubernetesCluster.getState())) {
2353+
throw new InvalidParameterValueException(String.format(
2354+
"Kubernetes cluster %s must be stopped before updating affinity groups (current state: %s)",
2355+
kubernetesCluster.getName(), kubernetesCluster.getState()));
2356+
}
2357+
accountManager.checkAccess(CallContext.current().getCallingAccount(),
2358+
SecurityChecker.AccessType.OperateEntry, false, kubernetesCluster);
2359+
return kubernetesCluster;
2360+
}
2361+
2362+
private void validateNodeAffinityGroups(Map<String, List<Long>> affinityGroupNodeTypeMap, long ownerAccountId) {
2363+
if (MapUtils.isEmpty(affinityGroupNodeTypeMap)) {
2364+
return;
2365+
}
2366+
Account owner = accountDao.findById(ownerAccountId);
2367+
for (List<Long> affinityGroupIds : affinityGroupNodeTypeMap.values()) {
2368+
for (Long affinityGroupId : affinityGroupIds) {
2369+
AffinityGroupVO affinityGroup = affinityGroupDao.findById(affinityGroupId);
2370+
if (Objects.isNull(affinityGroup)) {
2371+
throw new InvalidParameterValueException("Unable to find affinity group with ID: " + affinityGroupId);
2372+
}
2373+
if (affinityGroup.getAccountId() != owner.getAccountId()) {
2374+
throw new InvalidParameterValueException(String.format(
2375+
"Affinity group %s does not belong to the cluster owner account %s",
2376+
affinityGroup.getName(), owner.getAccountName()));
2377+
}
2378+
}
2379+
}
2380+
}
2381+
2382+
private void syncVmAffinityGroups(Long clusterId, Map<String, List<Long>> affinityGroupNodeTypeMap) {
2383+
List<KubernetesClusterVmMapVO> clusterVmMappings = kubernetesClusterVmMapDao.listByClusterId(clusterId);
2384+
if (CollectionUtils.isEmpty(clusterVmMappings)) {
2385+
return;
2386+
}
2387+
Map<String, List<Long>> nodeTypeAffinityMap = MapUtils.isEmpty(affinityGroupNodeTypeMap)
2388+
? Collections.emptyMap() : affinityGroupNodeTypeMap;
2389+
for (KubernetesClusterVmMapVO clusterVmMapping : clusterVmMappings) {
2390+
if (clusterVmMapping.isExternalNode()) {
2391+
continue;
2392+
}
2393+
String nodeType = getNodeType(clusterVmMapping);
2394+
affinityGroupVMMapDao.updateMap(clusterVmMapping.getVmId(),
2395+
nodeTypeAffinityMap.getOrDefault(nodeType, Collections.emptyList()));
2396+
}
2397+
}
2398+
2399+
private String getNodeType(KubernetesClusterVmMapVO clusterVmMapping) {
2400+
if (clusterVmMapping.isControlNode()) {
2401+
return CONTROL.name();
2402+
} else if (clusterVmMapping.isEtcdNode()) {
2403+
return ETCD.name();
2404+
}
2405+
return WORKER.name();
2406+
}
2407+
23162408
private void updateNodeCount(KubernetesClusterVO kubernetesCluster) {
23172409
List<KubernetesClusterVmMapVO> nodeList = kubernetesClusterVmMapDao.listByClusterId(kubernetesCluster.getId());
23182410
kubernetesCluster.setControlNodeCount(nodeList.stream().filter(KubernetesClusterVmMapVO::isControlNode).count());
@@ -2664,6 +2756,7 @@ public List<Class<?>> getCommands() {
26642756
cmdList.add(RemoveVirtualMachinesFromKubernetesClusterCmd.class);
26652757
cmdList.add(AddNodesToKubernetesClusterCmd.class);
26662758
cmdList.add(RemoveNodesFromKubernetesClusterCmd.class);
2759+
cmdList.add(UpdateKubernetesClusterAffinityGroupCmd.class);
26672760
return cmdList;
26682761
}
26692762

plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.cloudstack.api.command.user.kubernetes.cluster.ScaleKubernetesClusterCmd;
3333
import org.apache.cloudstack.api.command.user.kubernetes.cluster.StartKubernetesClusterCmd;
3434
import org.apache.cloudstack.api.command.user.kubernetes.cluster.StopKubernetesClusterCmd;
35+
import org.apache.cloudstack.api.command.user.kubernetes.cluster.UpdateKubernetesClusterAffinityGroupCmd;
3536
import org.apache.cloudstack.api.command.user.kubernetes.cluster.UpgradeKubernetesClusterCmd;
3637
import org.apache.cloudstack.api.response.KubernetesClusterConfigResponse;
3738
import org.apache.cloudstack.api.response.KubernetesClusterResponse;
@@ -171,6 +172,8 @@ public interface KubernetesClusterService extends PluggableService, Configurable
171172

172173
boolean upgradeKubernetesCluster(UpgradeKubernetesClusterCmd cmd) throws CloudRuntimeException;
173174

175+
boolean updateKubernetesClusterAffinityGroups(UpdateKubernetesClusterAffinityGroupCmd cmd) throws CloudRuntimeException;
176+
174177
boolean addVmsToCluster(AddVirtualMachinesToKubernetesClusterCmd cmd);
175178

176179
boolean addNodesToKubernetesCluster(AddNodesToKubernetesClusterCmd cmd);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.api.command.user.kubernetes.cluster;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
import javax.inject.Inject;
23+
24+
import org.apache.cloudstack.acl.RoleType;
25+
import org.apache.cloudstack.acl.SecurityChecker;
26+
import org.apache.cloudstack.api.ACL;
27+
import org.apache.cloudstack.api.APICommand;
28+
import org.apache.cloudstack.api.ApiCommandResourceType;
29+
import org.apache.cloudstack.api.ApiConstants;
30+
import org.apache.cloudstack.api.ApiErrorCode;
31+
import org.apache.cloudstack.api.BaseCmd;
32+
import org.apache.cloudstack.api.Parameter;
33+
import org.apache.cloudstack.api.ResponseObject;
34+
import org.apache.cloudstack.api.ServerApiException;
35+
import org.apache.cloudstack.api.response.KubernetesClusterResponse;
36+
import org.apache.cloudstack.context.CallContext;
37+
38+
import com.cloud.kubernetes.cluster.KubernetesCluster;
39+
import com.cloud.kubernetes.cluster.KubernetesClusterService;
40+
import com.cloud.kubernetes.cluster.KubernetesServiceHelper;
41+
import com.cloud.utils.exception.CloudRuntimeException;
42+
43+
@APICommand(name = "updateKubernetesClusterAffinityGroups",
44+
description = "Updates the affinity group mappings for a stopped Kubernetes cluster",
45+
responseObject = KubernetesClusterResponse.class,
46+
responseView = ResponseObject.ResponseView.Restricted,
47+
entityType = {KubernetesCluster.class},
48+
requestHasSensitiveInfo = false,
49+
responseHasSensitiveInfo = true,
50+
since = "4.23.0",
51+
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
52+
public class UpdateKubernetesClusterAffinityGroupCmd extends BaseCmd {
53+
54+
@Inject
55+
public KubernetesClusterService kubernetesClusterService;
56+
@Inject
57+
protected KubernetesServiceHelper kubernetesServiceHelper;
58+
59+
/////////////////////////////////////////////////////
60+
//////////////// API parameters /////////////////////
61+
/////////////////////////////////////////////////////
62+
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, required = true,
63+
entityType = KubernetesClusterResponse.class,
64+
description = "The ID of the Kubernetes cluster")
65+
private Long id;
66+
67+
@ACL(accessType = SecurityChecker.AccessType.UseEntry)
68+
@Parameter(name = ApiConstants.NODE_TYPE_AFFINITY_GROUP_MAP, type = CommandType.MAP,
69+
description = "Node Type to Affinity Group ID mapping. VMs of each node type will be added to the specified affinity group",
70+
since = "4.23.0")
71+
private Map<String, Map<String, String>> affinityGroupNodeTypeMap;
72+
73+
/////////////////////////////////////////////////////
74+
/////////////////// Accessors ///////////////////////
75+
/////////////////////////////////////////////////////
76+
77+
public Long getId() {
78+
return id;
79+
}
80+
81+
public Map<String, List<Long>> getAffinityGroupNodeTypeMap() {
82+
return kubernetesServiceHelper.getAffinityGroupNodeTypeMap(affinityGroupNodeTypeMap);
83+
}
84+
85+
@Override
86+
public long getEntityOwnerId() {
87+
return CallContext.current().getCallingAccount().getId();
88+
}
89+
90+
@Override
91+
public ApiCommandResourceType getApiResourceType() {
92+
return ApiCommandResourceType.KubernetesCluster;
93+
}
94+
95+
/////////////////////////////////////////////////////
96+
/////////////// API Implementation///////////////////
97+
/////////////////////////////////////////////////////
98+
99+
@Override
100+
public void execute() throws ServerApiException {
101+
try {
102+
if (!kubernetesClusterService.updateKubernetesClusterAffinityGroups(this)) {
103+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR,
104+
String.format("Failed to update affinity groups for Kubernetes cluster ID: %d", getId()));
105+
}
106+
final KubernetesClusterResponse response = kubernetesClusterService.createKubernetesClusterResponse(getId());
107+
response.setResponseName(getCommandName());
108+
setResponseObject(response);
109+
} catch (CloudRuntimeException exception) {
110+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, exception.getMessage());
111+
}
112+
}
113+
}

ui/src/config/section/compute.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ export default {
329329
docHelp: 'adminguide/virtual_machines.html#change-affinity-group-for-an-existing-vm',
330330
dataView: true,
331331
args: ['affinitygroupids'],
332-
show: (record) => { return record.hypervisor !== 'External' && ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' },
332+
show: (record) => { return record.hypervisor !== 'External' && ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' && record.vmtype !== 'cksnode' },
333333
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ChangeAffinity'))),
334334
popup: true
335335
},
@@ -619,7 +619,7 @@ export default {
619619
},
620620
{
621621
api: 'scaleKubernetesCluster',
622-
icon: 'swap-outlined',
622+
icon: 'arrows-alt-outlined',
623623
label: 'label.kubernetes.cluster.scale',
624624
message: 'message.kubernetes.cluster.scale',
625625
docHelp: 'plugins/cloudstack-kubernetes-service.html#scaling-kubernetes-cluster',
@@ -628,6 +628,15 @@ export default {
628628
popup: true,
629629
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ScaleKubernetesCluster.vue')))
630630
},
631+
{
632+
api: 'updateKubernetesClusterAffinityGroups',
633+
icon: 'swap-outlined',
634+
label: 'label.change.affinity',
635+
dataView: true,
636+
show: (record) => { return ['Stopped'].includes(record.state) && record.clustertype === 'CloudManaged' },
637+
popup: true,
638+
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ChangeKubernetesClusterAffinity.vue')))
639+
},
631640
{
632641
api: 'upgradeKubernetesCluster',
633642
icon: 'plus-circle-outlined',

0 commit comments

Comments
 (0)