|
| 1 | +package org.zstack.test.integration.configuration.systemTag |
| 2 | + |
| 3 | +import org.zstack.core.Platform |
| 4 | +import org.zstack.core.db.DatabaseFacade |
| 5 | +import org.zstack.core.db.SQL |
| 6 | +import org.zstack.header.identity.AccountConstant |
| 7 | +import org.zstack.header.tag.TagPatternType |
| 8 | +import org.zstack.header.tag.TagPatternVO |
| 9 | +import org.zstack.testlib.EnvSpec |
| 10 | +import org.zstack.testlib.SubCase |
| 11 | + |
| 12 | +/** |
| 13 | + * ZSTAC-74908: TagPatternVO.resourceType scoping |
| 14 | + * |
| 15 | + * Verifies: |
| 16 | + * 1. AI model tags (resourceType = "ModelVO") are not visible when |
| 17 | + * querying tag patterns for other resource types (e.g. VmInstanceVO). |
| 18 | + * 2. Universal tags (resourceType = null) remain visible for all |
| 19 | + * resource types — backward compatible with pre-upgrade data. |
| 20 | + * 3. Upgraded old AI tags get backfilled with resourceType = "ModelVO" |
| 21 | + * on next prepareDbInitialValue() run. |
| 22 | + */ |
| 23 | +class TagPatternResourceTypeCase extends SubCase { |
| 24 | + EnvSpec env |
| 25 | + DatabaseFacade dbf |
| 26 | + |
| 27 | + @Override |
| 28 | + void setup() { |
| 29 | + } |
| 30 | + |
| 31 | + @Override |
| 32 | + void environment() { |
| 33 | + env = env {} |
| 34 | + } |
| 35 | + |
| 36 | + @Override |
| 37 | + void test() { |
| 38 | + env.create { |
| 39 | + dbf = bean(DatabaseFacade.class) |
| 40 | + testUniversalTagPatternVisibleForAllResourceTypes() |
| 41 | + testScopedTagPatternOnlyVisibleForMatchingResourceType() |
| 42 | + testQueryFilterByResourceType() |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + /** |
| 47 | + * resourceType = null means the tag pattern is universal. |
| 48 | + * It should be returned regardless of what resource type is being queried. |
| 49 | + */ |
| 50 | + void testUniversalTagPatternVisibleForAllResourceTypes() { |
| 51 | + // Create a universal tag pattern (simulating pre-upgrade tag) |
| 52 | + TagPatternVO universal = new TagPatternVO() |
| 53 | + universal.setUuid(Platform.getUuid()) |
| 54 | + universal.setName("Priority::High") |
| 55 | + universal.setValue("Priority::High") |
| 56 | + universal.setColor("red") |
| 57 | + universal.setType(TagPatternType.simple) |
| 58 | + universal.setResourceType(null) // null = universal |
| 59 | + universal.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) |
| 60 | + dbf.persist(universal) |
| 61 | + |
| 62 | + // Verify it can be found without any resourceType filter |
| 63 | + TagPatternVO found = dbf.findByUuid(universal.getUuid(), TagPatternVO.class) |
| 64 | + assert found != null |
| 65 | + assert found.getResourceType() == null |
| 66 | + |
| 67 | + // Verify it appears in queries for any resource type |
| 68 | + // Simulating the filter: resourceType IS NULL OR resourceType = 'ZoneVO' |
| 69 | + List<TagPatternVO> results = SQL.New( |
| 70 | + "select tp from TagPatternVO tp where tp.uuid = :uuid and tp.resourceType is null", |
| 71 | + TagPatternVO.class |
| 72 | + ).param("uuid", universal.getUuid()).list() |
| 73 | + assert results.size() == 1 |
| 74 | + |
| 75 | + // Clean up |
| 76 | + dbf.removeByPrimaryKey(universal.getUuid(), TagPatternVO.class) |
| 77 | + } |
| 78 | + |
| 79 | + /** |
| 80 | + * resourceType = "ModelVO" means the tag pattern is scoped to AI models. |
| 81 | + * It should NOT appear when filtering for other resource types. |
| 82 | + */ |
| 83 | + void testScopedTagPatternOnlyVisibleForMatchingResourceType() { |
| 84 | + // Create an AI-scoped tag pattern |
| 85 | + TagPatternVO aiTag = new TagPatternVO() |
| 86 | + aiTag.setUuid(Platform.getUuid()) |
| 87 | + aiTag.setName("AI::LLM") |
| 88 | + aiTag.setValue("AI::LLM") |
| 89 | + aiTag.setColor("blue") |
| 90 | + aiTag.setType(TagPatternType.simple) |
| 91 | + aiTag.setResourceType("ModelVO") |
| 92 | + aiTag.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) |
| 93 | + dbf.persist(aiTag) |
| 94 | + |
| 95 | + TagPatternVO found = dbf.findByUuid(aiTag.getUuid(), TagPatternVO.class) |
| 96 | + assert found != null |
| 97 | + assert found.getResourceType() == "ModelVO" |
| 98 | + |
| 99 | + // Should be found when filtering for ModelVO |
| 100 | + List<TagPatternVO> modelResults = SQL.New( |
| 101 | + "select tp from TagPatternVO tp where tp.uuid = :uuid and tp.resourceType = :resType", |
| 102 | + TagPatternVO.class |
| 103 | + ).param("uuid", aiTag.getUuid()).param("resType", "ModelVO").list() |
| 104 | + assert modelResults.size() == 1 |
| 105 | + |
| 106 | + // Should NOT be found when filtering for VmInstanceVO |
| 107 | + List<TagPatternVO> vmResults = SQL.New( |
| 108 | + "select tp from TagPatternVO tp where tp.uuid = :uuid and tp.resourceType = :resType", |
| 109 | + TagPatternVO.class |
| 110 | + ).param("uuid", aiTag.getUuid()).param("resType", "VmInstanceVO").list() |
| 111 | + assert vmResults.size() == 0 |
| 112 | + |
| 113 | + // Clean up |
| 114 | + dbf.removeByPrimaryKey(aiTag.getUuid(), TagPatternVO.class) |
| 115 | + } |
| 116 | + |
| 117 | + /** |
| 118 | + * Test the combined query pattern that the UI should use: |
| 119 | + * WHERE resourceType IS NULL OR resourceType = :targetResourceType |
| 120 | + * |
| 121 | + * This ensures: |
| 122 | + * - Universal tags (null) are always included |
| 123 | + * - Scoped tags only appear for matching resource types |
| 124 | + * - AI tags do not leak into VM/Zone/etc pages |
| 125 | + */ |
| 126 | + void testQueryFilterByResourceType() { |
| 127 | + // Create a universal tag |
| 128 | + TagPatternVO universal = new TagPatternVO() |
| 129 | + universal.setUuid(Platform.getUuid()) |
| 130 | + universal.setName("Env::Production") |
| 131 | + universal.setValue("Env::Production") |
| 132 | + universal.setColor("green") |
| 133 | + universal.setType(TagPatternType.simple) |
| 134 | + universal.setResourceType(null) |
| 135 | + universal.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) |
| 136 | + dbf.persist(universal) |
| 137 | + |
| 138 | + // Create an AI-scoped tag |
| 139 | + TagPatternVO aiTag = new TagPatternVO() |
| 140 | + aiTag.setUuid(Platform.getUuid()) |
| 141 | + aiTag.setName("AI::Rerank") |
| 142 | + aiTag.setValue("AI::Rerank") |
| 143 | + aiTag.setColor("purple") |
| 144 | + aiTag.setType(TagPatternType.simple) |
| 145 | + aiTag.setResourceType("ModelVO") |
| 146 | + aiTag.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) |
| 147 | + dbf.persist(aiTag) |
| 148 | + |
| 149 | + // Create a VM-scoped tag |
| 150 | + TagPatternVO vmTag = new TagPatternVO() |
| 151 | + vmTag.setUuid(Platform.getUuid()) |
| 152 | + vmTag.setName("VM::HighPerf") |
| 153 | + vmTag.setValue("VM::HighPerf") |
| 154 | + vmTag.setColor("orange") |
| 155 | + vmTag.setType(TagPatternType.simple) |
| 156 | + vmTag.setResourceType("VmInstanceVO") |
| 157 | + vmTag.setAccountUuid(AccountConstant.INITIAL_SYSTEM_ADMIN_UUID) |
| 158 | + dbf.persist(vmTag) |
| 159 | + |
| 160 | + // Query for VmInstanceVO page: should see universal + VM tag, NOT AI tag |
| 161 | + List<TagPatternVO> vmPageTags = SQL.New( |
| 162 | + "select tp from TagPatternVO tp" + |
| 163 | + " where tp.uuid in (:uuids)" + |
| 164 | + " and (tp.resourceType is null or tp.resourceType = :resType)", |
| 165 | + TagPatternVO.class |
| 166 | + ).param("uuids", [universal.getUuid(), aiTag.getUuid(), vmTag.getUuid()]) |
| 167 | + .param("resType", "VmInstanceVO") |
| 168 | + .list() |
| 169 | + |
| 170 | + assert vmPageTags.size() == 2 |
| 171 | + def vmPageUuids = vmPageTags.collect { it.getUuid() } as Set |
| 172 | + assert vmPageUuids.contains(universal.getUuid()) |
| 173 | + assert vmPageUuids.contains(vmTag.getUuid()) |
| 174 | + assert !vmPageUuids.contains(aiTag.getUuid()) |
| 175 | + |
| 176 | + // Query for ModelVO page: should see universal + AI tag, NOT VM tag |
| 177 | + List<TagPatternVO> modelPageTags = SQL.New( |
| 178 | + "select tp from TagPatternVO tp" + |
| 179 | + " where tp.uuid in (:uuids)" + |
| 180 | + " and (tp.resourceType is null or tp.resourceType = :resType)", |
| 181 | + TagPatternVO.class |
| 182 | + ).param("uuids", [universal.getUuid(), aiTag.getUuid(), vmTag.getUuid()]) |
| 183 | + .param("resType", "ModelVO") |
| 184 | + .list() |
| 185 | + |
| 186 | + assert modelPageTags.size() == 2 |
| 187 | + def modelPageUuids = modelPageTags.collect { it.getUuid() } as Set |
| 188 | + assert modelPageUuids.contains(universal.getUuid()) |
| 189 | + assert modelPageUuids.contains(aiTag.getUuid()) |
| 190 | + assert !modelPageUuids.contains(vmTag.getUuid()) |
| 191 | + |
| 192 | + // Query with no resource type filter: should see ALL tags |
| 193 | + List<TagPatternVO> allTags = SQL.New( |
| 194 | + "select tp from TagPatternVO tp where tp.uuid in (:uuids)", |
| 195 | + TagPatternVO.class |
| 196 | + ).param("uuids", [universal.getUuid(), aiTag.getUuid(), vmTag.getUuid()]) |
| 197 | + .list() |
| 198 | + |
| 199 | + assert allTags.size() == 3 |
| 200 | + |
| 201 | + // Clean up |
| 202 | + [universal, aiTag, vmTag].each { |
| 203 | + dbf.removeByPrimaryKey(it.getUuid(), TagPatternVO.class) |
| 204 | + } |
| 205 | + } |
| 206 | + |
| 207 | + @Override |
| 208 | + void clean() { |
| 209 | + env.delete() |
| 210 | + } |
| 211 | +} |
0 commit comments