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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ PastureStack is an independent community effort to preserve, audit, and moderniz

## Project status

The current compatibility release is `1.6.103`. It retains the existing Node 24, Ember, Sass,
The current compatibility release is `1.6.104`. It retains the existing Node 24, Ember, Sass,
dependency, browser-smoke, terminal, console, and test-harness modernization.
It adds a provider-neutral OpenID Connect administration and sign-in flow with
PKCE S256, staged configuration validation, a real test login before
activation, and local-authentication recovery. Product-owned names, logos,
icons, package metadata, and visible text use PastureStack branding. API
models and protocol fields remain compatible.

Release `1.6.104` keeps the authenticated browser-session and OIDC corrections
from `1.6.103`. It also keeps the init-process checkbox inside its own resource
grid column, with the launch-configuration binding unchanged, so the control no
longer touches the adjacent process-limit input on create or upgrade forms.

Release `1.6.103` keeps an authenticated browser session when a non-auth API
request fails during startup, displays the nested OIDC/API explanation instead
of an empty alert, and activates a newly verified provider in unrestricted mode
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Supported state

The maintained compatibility release is the pure numeric `1.6.103` line used
The maintained compatibility release is the pure numeric `1.6.104` line used
by the current PastureStack Server release. Earlier branded coordinates are
historical records and are not current release or deployment targets.
Authentication-provider combinations must still be validated by an
Expand Down
1 change: 1 addition & 0 deletions app/components/form-resources/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export default Component.extend({
let callback = this.get('setRequestedHost');
if ( typeof callback === 'function' ) { callback(hostId); } else { this.set('instance.requestedHostId', hostId); }
},
setBoolean(field, event) { this.set(`instance.${field}`, !!event.target.checked); },
setField(field, event) { this.set(`instance.${field}`, event.target.value || null); },
setShm(value) {
updateHardwareDraft(this.get('instance'), {shmError: null});
Expand Down
6 changes: 3 additions & 3 deletions app/components/form-resources/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,10 @@
<input class="form-control" type="number" min="-1" step="1" data-hardware="pids" value={{this.instance.pidsLimit}} oninput={{action 'setInteger' this.instance 'pidsLimit'}}>
{{/input-or-display}}
</div>
<div class="resource-field resource-advanced-field">
<label>{{t 'formResources.init'}}</label>
<div class="resource-field resource-advanced-field resource-init-field">
<label for="{{this.elementId}}-init">{{t 'formResources.init'}}</label>
{{#input-or-display editable=this.editing value=this.instance.runInit}}
<label class="checkbox">{{input type='checkbox' checked=this.instance.runInit}} {{t 'formResources.init'}}</label>
<label class="resource-inline-checkbox" for="{{this.elementId}}-init"><input id="{{this.elementId}}-init" data-hardware="init" type="checkbox" checked={{this.instance.runInit}} onchange={{action 'setBoolean' 'runInit'}}> <span>{{t 'formResources.init'}}</span></label>
{{/input-or-display}}
</div>
<div class="resource-field resource-advanced-field">
Expand Down
14 changes: 14 additions & 0 deletions app/styles/components/_form-resources.scss
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,20 @@ $well-bg: #f5f5f5 !default;
grid-column: span 4;
}

.resource-inline-checkbox {
align-items: center;
display: flex;
gap: 8px;
min-height: 43px;
margin: 0;

input[type="checkbox"] {
flex: 0 0 auto;
margin: 0;
position: static;
}
}

.resource-advanced-group {
background: $well-bg;
border: 1px solid $table-border-color;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.103",
"version": "1.6.104",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
"version": "1.6.103",
"version": "1.6.104",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
Expand Down
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.103",
"version": "1.6.104",
"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.103" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.103"
if [[ "$version" != "1.6.104" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.104"
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.103 browser-session broker-broadcast
1.6.104 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.103":
if package.get("version") != "1.6.104":
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
2 changes: 1 addition & 1 deletion scripts/check-ui-ember-api-store-fetch-upgrade
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ storage_template = (repo / "app/host/storage/template.hbs").read_text(encoding="
if 'pageSizeChanged=(action "storagePageSizeChanged")' not in storage_template:
fail("storage page-size callback is missing from the host storage table")

if template_action_count != 586:
if template_action_count != 587:
fail(f"unexpected template action count: {template_action_count}")
if power_select_count != 15:
fail(f"unexpected modern PowerSelect count: {power_select_count}")
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-ui-test-harness-blockers
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ if module_for_count != 0:
fail("MODULE_FOR_USAGE_COUNT_UNEXPECTED", actual=module_for_count, expected=0)
if module_for_component_count != 0:
fail("MODULE_FOR_COMPONENT_USAGE_UNEXPECTED", actual=module_for_component_count, expected=0)
if direct_qunit_import_count != 105:
fail("DIRECT_QUNIT_IMPORT_COUNT_UNEXPECTED", actual=direct_qunit_import_count, expected=105)
if direct_qunit_import_count != 106:
fail("DIRECT_QUNIT_IMPORT_COUNT_UNEXPECTED", actual=direct_qunit_import_count, expected=106)

volatile_computed_count = 0
for path in Path("app").rglob("*.js"):
Expand Down
17 changes: 17 additions & 0 deletions tests/integration/components/form-resources-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ module('Integration | Component | hardware resources', function(hooks) {
assert.ok(unit.getBoundingClientRect().width < input.getBoundingClientRect().width, 'the unit is compact and the value gets useful space');
assert.ok(find('.resource-field-host select[id$="-host"]'), 'host inventory is prominent in the hardware section');
assert.ok(find('.resource-advanced-toggle[aria-expanded="false"]'), 'rare settings remain discoverable without overwhelming the common path');
await click('.resource-advanced-toggle');
let pids = find('[data-hardware="pids"]');
let init = find('[data-hardware="init"]');
let pidsRect = pids.getBoundingClientRect();
let initRect = init.getBoundingClientRect();
let initFieldRect = init.closest('.resource-init-field').getBoundingClientRect();
assert.ok(initRect.left >= initFieldRect.left,
'the init checkbox stays inside its own grid column instead of protruding into the previous control');
if ( initRect.top < pidsRect.bottom && initRect.bottom > pidsRect.top ) {
assert.ok(initRect.left - pidsRect.right >= 16,
'same-row PID and init controls retain a visible grid gap');
} else {
assert.ok(initRect.top >= pidsRect.bottom, 'responsive rows remain vertically separated');
}
assert.equal(getComputedStyle(init).position, 'static', 'the checkbox does not use the legacy negative-offset layout');
await click(init);
assert.true(this.instance.get('runInit'), 'the aligned control still writes the launch configuration');
});

test('switching primary and sidekick resets drafts and advanced maps without rewriting either config', async function(assert) {
Expand Down
120 changes: 120 additions & 0 deletions tests/unit/components/new-container-hardware-payload-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { A } from '@ember/array';
import EmberObject from '@ember/object';
import { run } from '@ember/runloop';
import { module, test } from 'qunit';

import NewContainerComponent from 'ui/components/new-container/component';
import inertRenderer from '../../helpers/inert-renderer';
import { createOwned, destroyOwned } from '../../helpers/owned-subject';

module('Unit | Component | new container hardware payload');

function hardwareLaunchConfig() {
return EmberObject.create({
labels: {},
ports: A(),
secrets: A(),
shmSize: 2147483648,
ipcMode: 'private',
runtime: 'nvidia',
runInit: true,
pidsLimit: 512,
cpuQuota: 200000,
cpuPeriod: 100000,
groupAdd: ['993'],
tmpfs: {'/run': 'rw,noexec,nosuid,size=64m'},
sysctls: {'net.core.somaxconn': '1024'},
ulimits: [{name: 'memlock', soft: -1, hard: -1}],
devices: ['/dev/dri/renderD128:/dev/dri/renderD128:rw'],
deviceRequests: [{driver: 'nvidia', count: 1, capabilities: [['gpu']]}],
requestedHostId: '1h1',
});
}

function createComponent(service, launchConfig) {
let component;

run(() => {
component = createOwned(NewContainerComponent, {
renderer: inertRenderer(),
intl: EmberObject.create({t(key) { return key; }}),
launchConfig,
service,
primaryResource: service,
primaryService: service,
isService: true,
}, 'component');
});

return component;
}

function hardwareSnapshot(config) {
return {
shmSize: config.get('shmSize'),
ipcMode: config.get('ipcMode'),
runtime: config.get('runtime'),
runInit: config.get('runInit'),
pidsLimit: config.get('pidsLimit'),
cpuQuota: config.get('cpuQuota'),
cpuPeriod: config.get('cpuPeriod'),
groupAdd: config.get('groupAdd'),
tmpfs: config.get('tmpfs'),
sysctls: config.get('sysctls'),
ulimits: config.get('ulimits'),
devices: config.get('devices'),
deviceRequests: config.get('deviceRequests'),
requestedHostId: config.get('requestedHostId'),
};
}

test('service creation retains every resource and hardware launch field', async function(assert) {
let launchConfig = hardwareLaunchConfig();
let saved;
let service = EmberObject.create({
launchConfig,
secondaryLaunchConfigs: A(),
save() {
saved = hardwareSnapshot(this.get('launchConfig'));
return Promise.resolve(this);
},
});
let component = createComponent(service, launchConfig);

await component.doSave();

assert.deepEqual(saved, hardwareSnapshot(launchConfig), 'create payload keeps all configured fields');
destroyOwned(component);
});

test('service upgrade sends every resource and hardware field in the upgrade strategy', async function(assert) {
let launchConfig = hardwareLaunchConfig();
let action;
let payload;
let service = EmberObject.create({
launchConfig,
secondaryLaunchConfigs: A(),
save() { return Promise.resolve(this); },
waitForAction(name) {
assert.equal(name, 'upgrade', 'waits for the upgrade action');
return Promise.resolve();
},
doAction(name, value) {
action = name;
payload = value;
return Promise.resolve();
},
});
let component = createComponent(service, launchConfig);

run(() => component.setProperties({
isUpgrade: true,
upgradeOptions: {batchSize: 1, intervalMillis: 2000, startFirst: false},
}));
await component.doSave();

assert.equal(action, 'upgrade');
assert.deepEqual(hardwareSnapshot(payload.inServiceStrategy.launchConfig), hardwareSnapshot(launchConfig),
'upgrade payload keeps all configured fields');
destroyOwned(component);
});