From ac2242ece23517b7359056779438b32131529f3c Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Tue, 27 Jan 2026 14:37:51 +0530 Subject: [PATCH] api,server,ui: support tags for domains (#11964) * api,server,ui: support tags for domains Fixes #11608 Signed-off-by: Abhishek Kumar * address copilot comment Signed-off-by: Abhishek Kumar * fix import Signed-off-by: Abhishek Kumar * Added tags support to listDomains API --------- Signed-off-by: Abhishek Kumar Co-authored-by: Harikrishna Patnala --- .../java/com/cloud/server/ResourceTag.java | 10 ++--- .../command/admin/domain/ListDomainsCmd.java | 4 +- .../api/response/DomainResponse.java | 18 ++++---- .../java/com/cloud/api/ApiResponseHelper.java | 41 +++++++++++++------ .../com/cloud/api/query/QueryManagerImpl.java | 29 +++++++++++++ .../api/query/dao/DomainJoinDaoImpl.java | 10 ++--- ui/src/components/view/InfoCard.vue | 2 +- 7 files changed, 81 insertions(+), 33 deletions(-) diff --git a/api/src/main/java/com/cloud/server/ResourceTag.java b/api/src/main/java/com/cloud/server/ResourceTag.java index b3026deceff..32305753f1a 100644 --- a/api/src/main/java/com/cloud/server/ResourceTag.java +++ b/api/src/main/java/com/cloud/server/ResourceTag.java @@ -16,14 +16,14 @@ // under the License. package com.cloud.server; -import org.apache.cloudstack.acl.ControlledEntity; -import org.apache.cloudstack.api.Identity; -import org.apache.cloudstack.api.InternalIdentity; - import java.util.HashMap; import java.util.Locale; import java.util.Map; +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + public interface ResourceTag extends ControlledEntity, Identity, InternalIdentity { // FIXME - extract enum to another interface as its used both by resourceTags and resourceMetaData code @@ -70,7 +70,7 @@ public interface ResourceTag extends ControlledEntity, Identity, InternalIdentit GuestOs(false, true), NetworkOffering(false, true), VpcOffering(true, false), - Domain(false, false, true), + Domain(true, false, true), ObjectStore(false, false, true); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/ListDomainsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/ListDomainsCmd.java index 5c5a92c45ca..aa197804226 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/ListDomainsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/domain/ListDomainsCmd.java @@ -23,7 +23,7 @@ import java.util.List; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants.DomainDetails; -import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.BaseListTaggedResourcesCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.command.user.UserCmd; @@ -39,7 +39,7 @@ import com.cloud.server.ResourceTag; @APICommand(name = "listDomains", description = "Lists domains and provides detailed information for listed domains", responseObject = DomainResponse.class, responseView = ResponseView.Restricted, entityType = {Domain.class}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class ListDomainsCmd extends BaseListCmd implements UserCmd { +public class ListDomainsCmd extends BaseListTaggedResourcesCmd implements UserCmd { private static final String s_name = "listdomainsresponse"; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/DomainResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/DomainResponse.java index e018b1a0f72..453c6b229e9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/DomainResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/DomainResponse.java @@ -16,21 +16,21 @@ // under the License. package org.apache.cloudstack.api.response; -import com.google.gson.annotations.SerializedName; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseResponseWithAnnotations; +import org.apache.cloudstack.api.BaseResponseWithTagInformation; import org.apache.cloudstack.api.EntityReference; import com.cloud.domain.Domain; import com.cloud.serializer.Param; - -import java.util.Date; -import java.util.List; -import java.util.Map; +import com.google.gson.annotations.SerializedName; @EntityReference(value = Domain.class) -public class DomainResponse extends BaseResponseWithAnnotations implements ResourceLimitAndCountResponse, SetResourceIconResponse { +public class DomainResponse extends BaseResponseWithTagInformation implements ResourceLimitAndCountResponse, SetResourceIconResponse { @SerializedName(ApiConstants.ID) @Param(description = "The ID of the domain") private String id; @@ -589,4 +589,8 @@ public class DomainResponse extends BaseResponseWithAnnotations implements Resou public void setTaggedResourceLimitsAndCounts(List taggedResourceLimitsAndCounts) { this.taggedResources = taggedResourceLimitsAndCounts; } + + public void setTags(Set tags) { + this.tags = tags; + } } diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index ce794cf5388..48696118a34 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -39,19 +39,6 @@ import java.util.stream.Collectors; import javax.inject.Inject; -import com.cloud.bgp.ASNumber; -import com.cloud.bgp.ASNumberRange; -import com.cloud.configuration.ConfigurationService; -import com.cloud.dc.ASNumberRangeVO; -import com.cloud.dc.ASNumberVO; -import com.cloud.dc.VlanDetailsVO; -import com.cloud.dc.dao.ASNumberDao; -import com.cloud.dc.dao.ASNumberRangeDao; -import com.cloud.dc.dao.VlanDetailsDao; -import com.cloud.hypervisor.Hypervisor; -import com.cloud.network.vpc.VpcGateway; -import com.cloud.network.vpn.Site2SiteVpnManager; -import com.cloud.storage.BucketVO; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.affinity.AffinityGroup; @@ -277,14 +264,19 @@ import com.cloud.api.query.vo.UserVmJoinVO; import com.cloud.api.query.vo.VolumeJoinVO; import com.cloud.api.query.vo.VpcOfferingJoinVO; import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.bgp.ASNumber; +import com.cloud.bgp.ASNumberRange; import com.cloud.capacity.Capacity; import com.cloud.capacity.CapacityVO; import com.cloud.capacity.dao.CapacityDaoImpl.SummedCapacity; import com.cloud.configuration.ConfigurationManager; +import com.cloud.configuration.ConfigurationService; import com.cloud.configuration.Resource.ResourceOwnerType; import com.cloud.configuration.Resource.ResourceType; import com.cloud.configuration.ResourceCount; import com.cloud.configuration.ResourceLimit; +import com.cloud.dc.ASNumberRangeVO; +import com.cloud.dc.ASNumberVO; import com.cloud.dc.ClusterDetailsDao; import com.cloud.dc.ClusterVO; import com.cloud.dc.DataCenter; @@ -295,7 +287,11 @@ import com.cloud.dc.Pod; import com.cloud.dc.StorageNetworkIpRange; import com.cloud.dc.Vlan; import com.cloud.dc.Vlan.VlanType; +import com.cloud.dc.VlanDetailsVO; import com.cloud.dc.VlanVO; +import com.cloud.dc.dao.ASNumberDao; +import com.cloud.dc.dao.ASNumberRangeDao; +import com.cloud.dc.dao.VlanDetailsDao; import com.cloud.domain.Domain; import com.cloud.domain.DomainVO; import com.cloud.event.Event; @@ -304,6 +300,7 @@ import com.cloud.exception.PermissionDeniedException; import com.cloud.host.ControlState; import com.cloud.host.Host; import com.cloud.host.HostVO; +import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.HypervisorCapabilities; import com.cloud.network.GuestVlan; import com.cloud.network.GuestVlanRange; @@ -367,9 +364,11 @@ import com.cloud.network.vpc.NetworkACLItem; import com.cloud.network.vpc.PrivateGateway; import com.cloud.network.vpc.StaticRoute; import com.cloud.network.vpc.Vpc; +import com.cloud.network.vpc.VpcGateway; import com.cloud.network.vpc.VpcOffering; import com.cloud.network.vpc.VpcVO; import com.cloud.network.vpc.dao.VpcOfferingDao; +import com.cloud.network.vpn.Site2SiteVpnManager; import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.NetworkOffering.Detail; @@ -388,6 +387,7 @@ import com.cloud.server.ResourceIconManager; import com.cloud.server.ResourceTag; import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.service.ServiceOfferingVO; +import com.cloud.storage.BucketVO; import com.cloud.storage.DataStoreRole; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.GuestOS; @@ -586,6 +586,7 @@ public class ApiResponseHelper implements ResponseGenerator { if (domain.getChildCount() > 0) { domainResponse.setHasChild(true); } + populateDomainTags(domain.getUuid(), domainResponse); domainResponse.setObjectName("domain"); return domainResponse; } @@ -3048,6 +3049,20 @@ public class ApiResponseHelper implements ResponseGenerator { response.setDomainPath(getPrettyDomainPath(object.getDomainPath())); } + public static void populateDomainTags(String domainUuid, DomainResponse domainResponse) { + List tags = ApiDBUtils.listResourceTagViewByResourceUUID(domainUuid, + ResourceTag.ResourceObjectType.Domain); + if (CollectionUtils.isEmpty(tags)) { + return; + } + Set tagResponses = new HashSet<>(); + for (ResourceTagJoinVO tag : tags) { + ResourceTagResponse tagResponse = ApiDBUtils.newResourceTagResponse(tag, true); + tagResponses.add(tagResponse); + } + domainResponse.setTags(tagResponses); + } + private void populateAccount(ControlledEntityResponse response, long accountId) { Account account = ApiDBUtils.findAccountById(accountId); if (account == null) { diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 472be9a12b7..a25fcdf22ef 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -2860,6 +2860,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q boolean listAll = cmd.listAll(); boolean isRecursive = false; Domain domain = null; + Map tags = cmd.getTags(); if (domainId != null) { domain = _domainDao.findById(domainId); @@ -2889,6 +2890,24 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q domainSearchBuilder.and("path", domainSearchBuilder.entity().getPath(), SearchCriteria.Op.LIKE); domainSearchBuilder.and("state", domainSearchBuilder.entity().getState(), SearchCriteria.Op.EQ); + if (MapUtils.isNotEmpty(tags)) { + SearchBuilder resourceTagSearch = resourceTagDao.createSearchBuilder(); + resourceTagSearch.and("resourceType", resourceTagSearch.entity().getResourceType(), Op.EQ); + resourceTagSearch.and().op(); + for (int count = 0; count < tags.size(); count++) { + if (count == 0) { + resourceTagSearch.op("tagKey" + count, resourceTagSearch.entity().getKey(), Op.EQ); + } else { + resourceTagSearch.or().op("tagKey" + count, resourceTagSearch.entity().getKey(), Op.EQ); + } + resourceTagSearch.and("tagValue" + count, resourceTagSearch.entity().getValue(), Op.EQ); + resourceTagSearch.cp(); + } + resourceTagSearch.cp(); + + domainSearchBuilder.join("tags", resourceTagSearch, resourceTagSearch.entity().getResourceId(), domainSearchBuilder.entity().getId(), JoinBuilder.JoinType.INNER); + } + if (keyword != null) { domainSearchBuilder.and("keywordName", domainSearchBuilder.entity().getName(), SearchCriteria.Op.LIKE); } @@ -2918,6 +2937,16 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } } + if (MapUtils.isNotEmpty(tags)) { + int count = 0; + sc.setJoinParameters("tags", "resourceType", ResourceObjectType.Domain); + for (Map.Entry entry : tags.entrySet()) { + sc.setJoinParameters("tags", "tagKey" + count, entry.getKey()); + sc.setJoinParameters("tags", "tagValue" + count, entry.getValue()); + count++; + } + } + // return only Active domains to the API sc.setParameters("state", Domain.State.Active); diff --git a/server/src/main/java/com/cloud/api/query/dao/DomainJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/DomainJoinDaoImpl.java index d4865c5550e..b6a3370e6bc 100644 --- a/server/src/main/java/com/cloud/api/query/dao/DomainJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/DomainJoinDaoImpl.java @@ -20,10 +20,8 @@ import java.util.ArrayList; import java.util.EnumSet; import java.util.List; +import javax.inject.Inject; -import com.cloud.api.ApiResponseHelper; -import com.cloud.configuration.Resource; -import com.cloud.user.AccountManager; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ApiConstants.DomainDetails; @@ -35,15 +33,16 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.springframework.stereotype.Component; import com.cloud.api.ApiDBUtils; +import com.cloud.api.ApiResponseHelper; import com.cloud.api.query.vo.DomainJoinVO; +import com.cloud.configuration.Resource; import com.cloud.configuration.Resource.ResourceType; import com.cloud.domain.Domain; +import com.cloud.user.AccountManager; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; -import javax.inject.Inject; - @Component public class DomainJoinDaoImpl extends GenericDaoBase implements DomainJoinDao { @@ -110,6 +109,7 @@ public class DomainJoinDaoImpl extends GenericDaoBase implem } domainResponse.setDetails(ApiDBUtils.getDomainDetails(domain.getId())); + ApiResponseHelper.populateDomainTags(domain.getUuid(), domainResponse); domainResponse.setObjectName("domain"); return domainResponse; diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index 0031d730f56..996e30ead3b 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -1094,7 +1094,7 @@ export default { return ['UserVm', 'Template', 'ISO', 'Volume', 'Snapshot', 'Backup', 'Network', 'LoadBalancer', 'PortForwardingRule', 'FirewallRule', 'SecurityGroup', 'SecurityGroupRule', 'PublicIpAddress', 'Project', 'Account', 'Vpc', 'NetworkACL', 'StaticRoute', 'VMSnapshot', - 'RemoteAccessVpn', 'User', 'SnapshotPolicy', 'VpcOffering'] + 'RemoteAccessVpn', 'User', 'SnapshotPolicy', 'VpcOffering', 'Domain'] }, name () { return this.resource.displayname || this.resource.name || this.resource.displaytext || this.resource.username ||