Skip to content

Commit 16bd7b1

Browse files
committed
fix: restore OIDC settings and terminal sessions
1 parent ee079c3 commit 16bd7b1

18 files changed

Lines changed: 281 additions & 22 deletions

app/components/workspace-terminal/component.js

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DEFAULT_COMMAND } from 'ui/components/container-shell/component';
44

55
const Terminal = window.Terminal;
66
const FitAddon = window.FitAddon.FitAddon;
7+
const MAX_RECONNECT_ATTEMPTS = 4;
78

89
function decodeTerminalData(data) {
910
try {
@@ -19,12 +20,28 @@ function terminalCloseAction(options) {
1920
}
2021

2122
if (!options.hasHello && !options.createAttempted) {
22-
return options.entryStatus === 'ended' ? 'ended' : 'create';
23+
return options.entryStatus === 'ended' ? 'ended' : 'probe';
2324
}
2425

2526
return options.status === 'ended' ? 'none' : 'reconnect';
2627
}
2728

29+
function terminalBrokerStatusAction(httpStatus, brokerStatus) {
30+
if (httpStatus === 404) {
31+
return 'create';
32+
}
33+
if (httpStatus === 403 || httpStatus === 409) {
34+
return 'rotate';
35+
}
36+
if (typeof httpStatus !== 'number' || httpStatus < 200 || httpStatus >= 300) {
37+
return 'error';
38+
}
39+
if (brokerStatus === 'ended' || brokerStatus === 'error') {
40+
return 'ended';
41+
}
42+
return 'connect';
43+
}
44+
2845
export default Ember.Component.extend(ThrottledResize, {
2946
classNames: ['workspace-terminal'],
3047
workspace: Ember.inject.service('console-workspace'),
@@ -74,6 +91,7 @@ export default Ember.Component.extend(ThrottledResize, {
7491
this.setProperties({
7592
hasHello: false,
7693
createAttempted: false,
94+
reconnectAttempts: 0,
7795
status: 'connecting',
7896
});
7997
this.connect(false);
@@ -129,7 +147,50 @@ export default Ember.Component.extend(ThrottledResize, {
129147
return;
130148
}
131149

132-
this.openSocket(this.get('workspace').brokerUrl(this.get('entry')), false);
150+
this.probeBrokerSession();
151+
},
152+
153+
probeBrokerSession() {
154+
let workspace = this.get('workspace');
155+
let entry = this.get('entry');
156+
157+
this.set('status', 'connecting');
158+
workspace.brokerStatus(entry).then((response) => {
159+
if (this.get('userClosed') || this.isDestroyed || this.isDestroying) {
160+
return;
161+
}
162+
this.applyBrokerStatusAction(terminalBrokerStatusAction(200, response && response.status));
163+
}).catch((error) => {
164+
if (this.get('userClosed') || this.isDestroyed || this.isDestroying) {
165+
return;
166+
}
167+
this.applyBrokerStatusAction(terminalBrokerStatusAction(error && error.status, null));
168+
});
169+
},
170+
171+
applyBrokerStatusAction(action) {
172+
let workspace = this.get('workspace');
173+
let entry = this.get('entry');
174+
175+
if (action === 'create') {
176+
workspace.updateSession(entry, {brokerReady: false, status: 'initializing'});
177+
this.set('createAttempted', false);
178+
this.createBrokerSession();
179+
} else if (action === 'rotate') {
180+
workspace.rotateBrokerIdentity(entry);
181+
this.set('createAttempted', false);
182+
this.createBrokerSession();
183+
} else if (action === 'ended') {
184+
this.setTerminalInputEnabled(false);
185+
this.set('status', 'ended');
186+
workspace.updateSession(entry, {brokerReady: false, status: 'ended'});
187+
} else if (action === 'connect') {
188+
this.openSocket(workspace.brokerUrl(entry), false);
189+
} else {
190+
this.setTerminalInputEnabled(false);
191+
this.set('status', 'error');
192+
workspace.updateSession(entry, {status: 'error'});
193+
}
133194
},
134195

135196
createBrokerSession() {
@@ -154,10 +215,14 @@ export default Ember.Component.extend(ThrottledResize, {
154215
return;
155216
}
156217
return this.get('workspace').createBrokerSession(this.get('entry'), access);
157-
}).then(() => {
218+
}).then((response) => {
158219
if (this.get('userClosed') || this.isDestroyed || this.isDestroying) {
159220
return;
160221
}
222+
if (terminalBrokerStatusAction(200, response && response.status) === 'ended') {
223+
this.applyBrokerStatusAction('ended');
224+
return;
225+
}
161226
this.get('workspace').updateSession(this.get('entry'), {
162227
brokerReady: true,
163228
status: 'connecting',
@@ -205,9 +270,7 @@ export default Ember.Component.extend(ThrottledResize, {
205270

206271
if (action === 'ended') {
207272
this.set('status', 'ended');
208-
} else if (action === 'create') {
209-
this.connect(true);
210-
} else if (action === 'reconnect') {
273+
} else if (action === 'probe' || action === 'reconnect') {
211274
this.setTerminalInputEnabled(false);
212275
this.set('status', 'disconnected');
213276
this.get('workspace').updateSession(this.get('entry'), {status: 'disconnected'});
@@ -334,6 +397,11 @@ export default Ember.Component.extend(ThrottledResize, {
334397
scheduleReconnect() {
335398
this.cancelReconnect();
336399
let attempt = this.incrementProperty('reconnectAttempts');
400+
if (attempt > MAX_RECONNECT_ATTEMPTS) {
401+
this.set('status', 'error');
402+
this.get('workspace').updateSession(this.get('entry'), {status: 'error'});
403+
return;
404+
}
337405
let delay = Math.min(10000, 500 * Math.pow(2, Math.min(attempt, 5)));
338406
this._reconnectTimer = Ember.run.later(this, () => {
339407
this.set('createAttempted', false);
@@ -380,4 +448,8 @@ export default Ember.Component.extend(ThrottledResize, {
380448
},
381449
});
382450

383-
export { decodeTerminalData, terminalCloseAction };
451+
export {
452+
decodeTerminalData,
453+
terminalBrokerStatusAction,
454+
terminalCloseAction,
455+
};

app/models/oidcconfig.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Resource from 'ember-api-store/models/resource';
2+
3+
// The control API exposes a writable provider displayName. The shared
4+
// Resource mixin also defines a read-only computed displayName for ordinary
5+
// infrastructure resources, so this embedded configuration model must own a
6+
// plain writable field of its own.
7+
export default Resource.extend({
8+
displayName: null,
9+
});

app/services/console-workspace.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,30 @@ export default Ember.Service.extend({
511511
return brokerWebSocketUrl(entry.get('sessionId'));
512512
},
513513

514+
brokerStatus(entry) {
515+
let url = `/v1/exec/sessions/${encodeURIComponent(entry.get('sessionId'))}`;
516+
517+
return Ember.$.ajax({
518+
url,
519+
method: 'GET',
520+
dataType: 'json',
521+
headers: {
522+
'X-PastureStack-Session-Secret': entry.get('secret'),
523+
},
524+
});
525+
},
526+
527+
rotateBrokerIdentity(entry) {
528+
entry.setProperties({
529+
sessionId: workspaceSessionId(),
530+
secret: workspaceSecret(),
531+
brokerReady: false,
532+
status: 'new',
533+
});
534+
this.saveSessions();
535+
this.saveLayouts();
536+
},
537+
514538
brokerProtocols(entry) {
515539
return brokerWebSocketProtocols(entry.get('secret'), this.get('clientId'));
516540
},

docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"name": "@pasturestack/web-console",
3-
"version": "1.6.56-pasturestack.39",
3+
"version": "1.6.56-pasturestack.40",
44
"lockfileVersion": 3,
55
"requires": true,
66
"packages": {
77
"": {
88
"name": "@pasturestack/web-console",
9-
"version": "1.6.56-pasturestack.39",
9+
"version": "1.6.56-pasturestack.40",
1010
"license": "Apache-2.0",
1111
"dependencies": {
1212
"sass": "1.99.0"

docs/console-workspace.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ Virtual machine console windows retain their list and layout. Their graphical
4343
connection is re-established when the window is reopened because the legacy
4444
console protocol does not provide terminal-style output replay.
4545

46+
Before attaching a saved terminal, the browser asks the same-origin broker for
47+
that session's current status using its random session credential. A broker
48+
restart therefore recreates a missing upstream terminal through the normal
49+
container execute action instead of repeatedly opening a stale WebSocket. A
50+
credential conflict rotates both random identifiers, ended sessions remain
51+
history, and transport failures stop after four automatic attempts before the
52+
localized manual reconnect action is offered.
53+
4654
Log windows use themed vertical and horizontal scrollbars. Line wrapping is
4755
disabled by default so long output remains intact and horizontally scrollable.
4856
The localized **Wrap lines** option hides the horizontal scrollbar while

docs/modernization.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,16 @@ closure actions, preventing native input events from recursively re-entering
183183
themselves. Navigation data is also normalized before it reaches Ember 6
184184
`LinkTo`, whose `@query` argument now requires an object even when a legacy menu
185185
item has no query parameters.
186+
187+
`v1.6.56-pasturestack.40` gives the embedded OpenID Connect configuration its
188+
own writable model boundary. This prevents its provider `displayName` field
189+
from colliding with the read-only display name computed for ordinary runtime
190+
resources after the Ember 6 upgrade. The same release probes a saved console
191+
broker session before reconnecting: a missing session is recreated through the
192+
normal execute action, a credential conflict rotates the random browser-side
193+
session identity, and a permanently failed WebSocket stops after four bounded
194+
attempts instead of retrying indefinitely.
195+
186196
The removed Handlebars `partial` helper is replaced by an explicit, audited
187197
inventory of tagless context components. Each compiled template is attached
188198
through Ember's public `setComponentTemplate` API, property reads and writes

docs/openid-connect.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ Changing any field invalidates the prior validation and test result.
3939
Authorization codes are single-use and are never carried from the test flow
4040
into activation.
4141

42+
The browser treats the embedded provider configuration as a dedicated writable
43+
resource. Provider labels are configuration data and are not derived from the
44+
display-name calculation used by infrastructure resources.
45+
4246
## Private certificate authorities
4347

4448
For a private identity provider, paste the PEM-encoded issuing certificate

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pasturestack/web-console",
3-
"version": "1.6.56-pasturestack.39",
3+
"version": "1.6.56-pasturestack.40",
44
"private": true,
55
"description": "PastureStack browser console for the compatible control platform.",
66
"repository": {

scripts/check-modernization-blockers

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f:
4141
print(json.load(f).get('version', ''))
4242
PY
4343
)
44-
if [[ "$version" != "1.6.56-pasturestack.39" ]]; then
45-
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.56-pasturestack.39"
44+
if [[ "$version" != "1.6.56-pasturestack.40" ]]; then
45+
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.56-pasturestack.40"
4646
failures=$((failures + 1))
4747
fi
4848

0 commit comments

Comments
 (0)