diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml
index 9b7c50d688..822578f908 100644
--- a/.github/workflows/security-release-gate.yml
+++ b/.github/workflows/security-release-gate.yml
@@ -57,6 +57,20 @@ jobs:
import xml.etree.ElementTree as ET
required = {
+ 'io.cattle.platform.api.schema.FileSchemaFactoryTest': {
+ 'restoresOnlyNativeReadFieldInPackagedFrozenVolumeRoleSchemas',
+ },
+ 'io.cattle.platform.schema.processor.VolumeNativeSchemaAuthorizationTest': {
+ 'currentRoleOverlaysExposeNativeClassificationWithoutGrantingMutation',
+ 'readonlyPipelinePreservesCrudWhileRetainingServerOwnedNativeReadField',
+ },
+ 'io.github.ibuildthecloud.gdapi.model.impl.VolumeNativeWrappedResourceTest': {
+ 'formatterRetainsTheActualServerTrueAndFalseClassification',
+ 'onlyAnExplicitSchemaContractCanProvideTheServerDefault',
+ },
+ 'io.github.ibuildthecloud.gdapi.validation.ValidationHandlerTest': {
+ 'clientCannotSetServerOwnedVolumeNativeClassification',
+ },
'io.cattle.platform.core.dao.impl.NativeContainerNameRefreshDaoTest': {
'exactExistingImportedSnapshotAndNameOnlyCas',
'compareAndSwapContainsEveryOriginalColumnIncludingBinaryTextAndJson',
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index b866f96af7..28940d2310 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -4,6 +4,20 @@ The migration preserves established `io.cattle.*` Java packages, Maven coordinat
New operator-facing names use PastureStack and `PASTURESTACK_*`. Compatibility identifiers must be changed only with an explicit data migration, a dual-read or dual-write transition, a rollback plan, and cross-repository verification.
+## Volume native classification
+
+Candidate `0.183.332` exposes the existing `volume.isNative` boolean as read-only
+for readable Volume resources. Current role overlays retain the field; the v1
+loader copies only this missing field from the current core schema into frozen
+role schemas. It grants neither create nor update permission on the field and
+does not widen collection/object authorization, methods, actions or other
+fields. The API preserves stored true/false values and applies the established
+server default only through an explicit schema contract. Client input cannot
+set this server-owned classification on POST or PUT. No database migration is
+required. Rolling back removes the field from affected role responses, so the
+strict Web Console unallocated-volume list may again hide eligible volumes.
+Native UI lifecycle acceptance and artifact publication are still pending.
+
## Certificate lifecycle
Engine `0.183.327` allows partial Certificate updates that omit `cert`, without
diff --git a/README.md b/README.md
index 15ff0fd887..fb928513b3 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,14 @@ preserved upstream boundary.
## Current release
+Candidate `v0.183.332` restores the server-owned Volume `isNative` classification
+as a read-only field in both v1 frozen role schemas and v2 role overlays. It does
+not change Volume CRUD permissions, infer missing fields in the browser, or
+allow clients to change native classification. Eighteen targeted local tests
+passed, including six new regressions. Formal artifact publication and native
+Volume UI lifecycle acceptance are pending; see the
+[332 candidate note](docs/releases/orchestration-engine-0.183.332.md).
+
The published release `v0.183.331` corrects the stopped-container mapping condition
in 330. The common selection/update predicate accepts only running/active
or stopped/inactive pairs; all source-account, unique-mapping, managed-container
@@ -206,7 +214,7 @@ bash scripts/check-cattle-jdk25-full-package
After the gate passes, package and check the release artifact:
```sh
-ENGINE_VERSION=0.183.331 bash scripts/build --release
+ENGINE_VERSION=0.183.332 bash scripts/build --release
bash scripts/check-release-artifact dist/artifacts/cattle.jar
```
diff --git a/code/framework/api-pub-sub-jetty/pom.xml b/code/framework/api-pub-sub-jetty/pom.xml
index 44c868aec5..542ba068e3 100644
--- a/code/framework/api-pub-sub-jetty/pom.xml
+++ b/code/framework/api-pub-sub-jetty/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.331
+ 0.183.332
../../parent/pom.xml
diff --git a/code/framework/api-pub-sub/pom.xml b/code/framework/api-pub-sub/pom.xml
index 271d8de9c5..3b0345b4ab 100644
--- a/code/framework/api-pub-sub/pom.xml
+++ b/code/framework/api-pub-sub/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.331
+ 0.183.332
../../parent/pom.xml
diff --git a/code/framework/api/pom.xml b/code/framework/api/pom.xml
index 945a59497a..a381c83636 100644
--- a/code/framework/api/pom.xml
+++ b/code/framework/api/pom.xml
@@ -4,7 +4,7 @@
cattle-parent
io.cattle
- 0.183.331
+ 0.183.332
../../parent/pom.xml
diff --git a/code/framework/api/src/main/java/io/cattle/platform/api/schema/FileSchemaFactory.java b/code/framework/api/src/main/java/io/cattle/platform/api/schema/FileSchemaFactory.java
index 6f8a8b5a0b..ff64691086 100644
--- a/code/framework/api/src/main/java/io/cattle/platform/api/schema/FileSchemaFactory.java
+++ b/code/framework/api/src/main/java/io/cattle/platform/api/schema/FileSchemaFactory.java
@@ -92,6 +92,7 @@ protected void copyAccessors(Schema schema) {
Schema coreSchema = parentSchemaFactory.getSchema(schema.getId());
mergeProjectMemberExternalIdTypeOptions(schema, coreSchema);
mergeProjectTemplatePublicReadField(schema, coreSchema);
+ mergeVolumeNativeReadField(schema, coreSchema);
Class> clz = parentSchemaFactory.getSchemaClass(schema.getId());
if (clz == null) {
return;
@@ -157,6 +158,29 @@ public String getFile() {
return file;
}
+ protected void mergeVolumeNativeReadField(Schema schema, Schema parentSchema) {
+ if (parentSchema == null || !"volume".equals(schema.getId()) ||
+ schema.getResourceFields().containsKey("isNative")) {
+ return;
+ }
+
+ Field parentField = parentSchema.getResourceFields().get("isNative");
+ if (!(parentField instanceof FieldImpl)) {
+ return;
+ }
+
+ // v1 reads frozen role schemas, so the current authorization overlay
+ // cannot restore this missing classification. Expose only the existing
+ // server-owned flag; do not grant mutation or replace other fields.
+ FieldImpl readOnly = new FieldImpl(parentField);
+ readOnly.setName("isNative");
+ readOnly.setCreate(false);
+ readOnly.setUpdate(false);
+ readOnly.setReadOnCreateOnly(false);
+ readOnly.setIncludeInList(true);
+ schema.getResourceFields().put("isNative", readOnly);
+ }
+
public void setFile(String file) {
this.file = file;
}
diff --git a/code/framework/api/src/test/java/io/cattle/platform/api/schema/FileSchemaFactoryTest.java b/code/framework/api/src/test/java/io/cattle/platform/api/schema/FileSchemaFactoryTest.java
index ae54e0b76e..0cb2ec468b 100644
--- a/code/framework/api/src/test/java/io/cattle/platform/api/schema/FileSchemaFactoryTest.java
+++ b/code/framework/api/src/test/java/io/cattle/platform/api/schema/FileSchemaFactoryTest.java
@@ -212,6 +212,71 @@ private FileSchemaFactory factory(String resourceName) {
return factory(resourceName, new EmptySchemaFactory());
}
+ @Test
+ public void restoresOnlyNativeReadFieldInPackagedFrozenVolumeRoleSchemas() throws Exception {
+ SchemaImpl core = schema("volume", "volumes");
+ FieldImpl coreField = new FieldImpl();
+ coreField.setName("isNative");
+ coreField.setType("boolean");
+ coreField.setDefault(Boolean.FALSE);
+ // Even a more permissive parent cannot grant writes through this merge.
+ coreField.setCreate(true);
+ coreField.setUpdate(true);
+ core.getResourceFields().put("isNative", coreField);
+ Path root = Paths.get("").toAbsolutePath();
+ while (root != null && !Files.isRegularFile(root.resolve("resources/content/schema/v1/owner.ser"))) {
+ root = root.getParent();
+ }
+ assertNotNull("Packaged frozen role schemas are required", root);
+ for (String role : Arrays.asList("owner", "member", "readonly", "restricted", "user", "admin")) {
+ String resourceName = "schema/v1/" + role + ".ser";
+ byte[] bytes = Files.readAllBytes(root.resolve("resources/content/").resolve(resourceName));
+ Thread.currentThread().setContextClassLoader(new ResourceClassLoader(resourceName, bytes));
+ FileSchemaFactory original = factory(resourceName);
+ original.start();
+ Thread.currentThread().setContextClassLoader(new ResourceClassLoader(resourceName, bytes));
+ FileSchemaFactory repaired = factory(resourceName, new SingleSchemaFactory(core));
+ repaired.start();
+
+ Schema before = original.getSchema("volume"), after = repaired.getSchema("volume");
+ assertNotNull(role, before);
+ assertNotNull(role, after);
+ Map originalFields = new LinkedHashMap();
+ for (Map.Entry entry : before.getResourceFields().entrySet()) {
+ originalFields.put(entry.getKey(), (FieldImpl) entry.getValue());
+ }
+ FieldImpl actual = (FieldImpl) after.getResourceFields().get("isNative");
+ assertNotNull(role, actual);
+ assertEquals(role, "boolean", actual.getType());
+ assertEquals(role, Boolean.FALSE, actual.getDefault());
+ assertFalse(role, actual.isNullable());
+ assertFalse(role, actual.isCreate());
+ assertFalse(role, actual.isUpdate());
+ assertFalse(role, actual.isReadOnCreateOnly());
+ assertTrue(role, actual.isIncludeInList());
+ assertEquals(role, before.getCollectionMethods(), after.getCollectionMethods());
+ assertEquals(role, before.getResourceMethods(), after.getResourceMethods());
+ assertTrue(role + "/resourceActions", Arrays.equals(
+ serialize(Arrays.