Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Web Console preserves compatible API paths, schema and resource names, action na

Visible branding, product-owned assets, icon identifiers, package metadata, and operator documentation use PastureStack. Historical identifiers remain only where they are server data or protocol contracts and must not be mechanically replaced.

Source candidate `1.6.170` accepts null only for the optional expanded `mounts`
projection while retaining the real complete empty pool relationship and full
scoped mount-cache proof. It preserves nonempty raw ID binding, current-project
ownership, stale-proof invalidation and backend authorization. Null raw IDs and
malformed array-like objects remain excluded. No API or authentication contract
changes. Formal publication and packaged native lifecycle acceptance remain
pending; see the [release note](docs/releases/web-console-1.6.170.md).

Published Web Console `1.6.169` treats Volume `externalId` as an identifier rather than an
allocation reference, matching the existing engine pre-create contract. The
field remains part of relationship-proof invalidation. Host, image, instance,
Expand All @@ -16,9 +24,10 @@ produced two byte-identical production archives. The signed immutable numeric
release pins source `5962f57fccb4062a65b5921646c06b4663713b9b`; anonymous
public readback matches archive SHA-256
`e2bcb97b0da810f2ff216f9738739235e3c6f29ef46f1d99b623cf9c9f7258e2`
and size 2,981,057 bytes. Server `v1.6.506` / PR231 is still a source
candidate, not a formally published Server artifact. Packaged native
existing-volume terminal and fresh lifecycle acceptance remain pending.
and size 2,981,057 bytes. Server `v1.6.506` is published and deployed on QA 8080;
initial/restart checks passed with configuration and persistent volumes retained.
Its native existing-volume terminal exposed the null-projection defect above;
fresh lifecycle acceptance remains pending.
Historical HOLDs remain HOLD; the complete permission/resource/locale matrix
remains INCOMPLETE. See the [release note](docs/releases/web-console-1.6.169.md).

Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ PastureStack is an independent community effort to preserve, audit, and moderniz

## Project status

Source candidate `1.6.170` corrects an optional `mounts: null` projection being
mistaken for a real allocation in the shared local-volume list. It preserves
the complete advertised pool relationship, full scoped mount cache, exact-volume
binding checks and backend permissions. Raw IDs and malformed values do not
become empty evidence. Formal publication and packaged native lifecycle
acceptance remain pending; historical HOLDs and the complete matrix are not
promoted. See the [release note](docs/releases/web-console-1.6.170.md).

Published `1.6.169` corrects the shared unallocated-local-volume classifier.
The engine may generate `externalId` from a volume's name; that identifier does
not allocate the volume to a host, workload or storage pool. Classification
Expand All @@ -23,8 +31,9 @@ production archives. The signed immutable numeric
pins source `5962f57fccb4062a65b5921646c06b4663713b9b`; archive SHA-256 is
`e2bcb97b0da810f2ff216f9738739235e3c6f29ef46f1d99b623cf9c9f7258e2`
(2,981,057 bytes). Anonymous public downloads match the formal artifact.
Server `v1.6.506` remains a source candidate in PR231, not a formally
published Server artifact. Packaged native existing-volume terminal and fresh
Server `v1.6.506` is now published and deployed on the QA 8080 host, with initial
and restart HTTP 200 checks and preserved configuration/volumes. The packaged
native existing-volume terminal found the null-projection defect above; fresh
create/cancel/refresh/readonly-denial/remove acceptance remain pending.
Historical HOLDs are not promoted; the complete permission/resource/locale
matrix remains INCOMPLETE. See the
Expand Down
10 changes: 8 additions & 2 deletions app/utils/unallocated-volumes.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ function isCandidate(volume, projectId) {
}

function emptyArray(value) {
return isArray(value) && get(value, 'length') === 0;
// Ember's isArray also accepts arbitrary objects with a length property.
// Require a native array or actual Ember Array API, not an API object impostor.
return isArray(value) && (Array.isArray(value) || typeof get(value, 'objectAt') === 'function') &&
get(value, 'length') === 0;
}

function completeCollection(value) {
Expand All @@ -42,7 +45,10 @@ export function isUnallocatedLocalVolume(volume, projectId) {
}
for (let field of ['storagePoolIds', 'mountIds', 'mounts']) {
let value = get(volume, field);
if (value !== undefined && !emptyArray(value)) {
// API can explicitly serialize the optional expanded mounts projection as null.
// This is not absence proof: the complete pool read and scoped mount cache
// above/below are still mandatory, and nonempty raw IDs remain binding.
if (value !== undefined && !(field === 'mounts' && value === null) && !emptyArray(value)) {
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.169",
"version": "1.6.170",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
"version": "1.6.169",
"version": "1.6.170",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
Expand Down
45 changes: 45 additions & 0 deletions docs/releases/web-console-1.6.170.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Web Console 1.6.170

Source candidate; formal component publication and packaged native acceptance
remain separate gates.

## Root cause and scope

QA Server v1.6.506 supplied the exact inactive local Volume in the current
environment, with a complete empty storage-pool Collection and complete scoped
mount cache. The API also supplied `mounts: null`. The resource relationship
setter correctly retained that optional expanded projection, but the shared
unallocated-volume classifier rejected it as an allocation. The Store held the
resource while the native table incorrectly hid it.

The classifier accepts null only for the optional expanded `mounts` projection.
It does not replace it with an empty array or treat it as absence proof. The
real complete empty storage-pool relationship, full current-environment mount
cache, absence of exact-volume mounts (including inactive workloads), allocation
fingerprint and explicit local/non-native fields remain required. Raw nonempty
mount/pool IDs remain binding; null or malformed raw ID fields do not qualify.
An arbitrary object with `length: 0` is not an empty native/Ember array.

No API method, authorization, authentication, resource transition, backend,
HAProxy or production configuration changes. No migration is required.

## Verification boundary

The new real Store/Volume/Collection regression deserializes the null projection
through the actual computed setter. Positive classification still requires
real pool and mount evidence; incomplete/partial/nonempty pool data, incomplete
mount cache, inactive mounts, nonempty IDs and malformed values remain excluded.
Focused headless Chrome 153 validation passed 13/13 directly affected classifier
and storage-route cases, with zero failures, skips or todo. Independent scoped
review found no blocker. This is source regression evidence, not packaged QA.
Formal immutable component/archive results and packaged browser lifecycle
results will be recorded after they are actually complete. Earlier failed
native row receipts remain HOLD; the complete resource/permission/locale matrix
is not claimed as PASS.

## Upgrade and rollback

Use the separately published Server patch containing this exact component.
Do not overwrite Web Console 1.6.169 or Server v1.6.506. Existing named volumes,
runtime environment and the previous immutable Server image remain the rollback
boundary. This repair does not require or authorize company deployment.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.169",
"version": "1.6.170",
"private": true,
"description": "PastureStack browser console for the compatible control platform.",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-modernization-blockers
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f:
print(json.load(f).get('version', ''))
PY
)
if [[ "$version" != "1.6.169" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.169"
if [[ "$version" != "1.6.170" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.170"
failures=$((failures + 1))
fi

Expand Down
2 changes: 1 addition & 1 deletion scripts/check-ui-console-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,4 @@ if [[ -n ${PASTURESTACK_PRIVATE_MARKER:-} ]] && grep -RInF -- "$PASTURESTACK_PRI
fi

printf 'UI_CONSOLE_WORKSPACE_OK version=%s persistence=%s cross_tab=%s\n' \
1.6.169 browser-session broker-broadcast
1.6.170 browser-session broker-broadcast
2 changes: 1 addition & 1 deletion scripts/check-ui-critical-high-dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ if lock_bytes != baseline_bytes:
lock = json.loads(lock_bytes)
packages = lock.get("packages", {})
root = packages.get("", {})
if package.get("version") != "1.6.169":
if package.get("version") != "1.6.170":
fail(f"unexpected Web Console version: {package.get('version')}")
if root.get("version") != package.get("version"):
fail(f"lock root version differs: {root.get('version')}")
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/utils/unallocated-volumes-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,50 @@ module('Unit | Utils | unallocated volumes', function() {
} finally { f.dispose(); }
});

test('null API mounts projection needs real pool and full mount-cache proof', async function(assert) {
const f = volumeFixture();
// _typeify exercises the computed relationship setter used by actual API
// deserialization; missing mountIds alone otherwise computes an empty array.
const volume = f.volume({state: 'inactive', externalId: 'local-volume', mounts: null});
try {
assert.strictEqual(volume.get('mounts'), null, 'API null is preserved by the expanded relationship setter');
assert.false(isUnallocatedLocalVolume(volume, '1a2540'), 'null projection alone proves nothing');
await refreshUnallocatedVolumeRelations([volume], '1a2540');
assert.true(isUnallocatedLocalVolume(volume, '1a2540'), 'complete empty pool and mount evidence admits null projection');
f.store._state.foundAll.mount = false;
assert.false(isUnallocatedLocalVolume(volume, '1a2540'), 'null projection cannot bypass incomplete mount cache');
f.store._state.foundAll.mount = true;
const mount = f.store._typeify({type: 'mount', id: '1m-nullable', volumeId: volume.get('id'),
instanceId: 'not-in-cache', state: 'inactive'});
assert.false(isUnallocatedLocalVolume(volume, '1a2540'), 'actual inactive mount beats null expanded relationship');
f.store._remove('mount', mount);
assert.true(isUnallocatedLocalVolume(volume, '1a2540'));
for (const field of ['storagePoolIds', 'mountIds', 'mounts']) {
for (const value of [['not-in-cache'], false, 0, '', {}, {length: 0}]) {
volume.set(field, value);
assert.false(isUnallocatedLocalVolume(volume, '1a2540'), `${field} malformed or nonempty cannot qualify`);
}
if (field === 'mounts') {
volume.set(field, null);
} else {
volume.set(field, null);
assert.false(isUnallocatedLocalVolume(volume, '1a2540'), 'null raw ID field is not valid array input');
volume.set(field, undefined);
}
}
f.store.incrementProperty('generation');
f.store.rawRequest = () => resolve({status: 200, body: {type: 'collection', resourceType: 'storagePool',
data: [], pagination: {partial: true}}});
await assert.rejects(refreshUnallocatedVolumeRelations([volume], '1a2540'));
assert.false(isUnallocatedLocalVolume(volume, '1a2540'), 'null projection cannot bypass partial pool response');
f.store.incrementProperty('generation');
f.store.rawRequest = () => resolve({status: 200, body: {type: 'collection', resourceType: 'storagePool',
data: [{type: 'storagePool', id: '1sp-nullable'}], pagination: {partial: false}}});
await refreshUnallocatedVolumeRelations([volume], '1a2540');
assert.false(isUnallocatedLocalVolume(volume, '1a2540'), 'actual pool allocation beats null projection');
} finally { f.dispose(); }
});

test('external ID metadata cannot bypass typed input, pool allocation or inactive mounts', async function(assert) {
const f = volumeFixture();
try {
Expand Down
Loading