Skip to content

Commit 39b2840

Browse files
vrubezhnydgolovin
andcommitted
Use SSO account to configure sandbox in one click
Uses a POC ['feat: use sso account to configure sandbox in one click redhat-developer#4232'](redhat-developer#4232) by @dgolovin. Allows logging in to the DevSandbox using a SSO account if Service Account pipeline token is configured, otherwise a token from the Clipboard is to be used. Co-authored-by: Denis Golovin <dgolovin@redhat.com> Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
1 parent fead656 commit 39b2840

File tree

4 files changed

+183
-19
lines changed

4 files changed

+183
-19
lines changed

src/openshift/cluster.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See LICENSE file in the project root for license information.
44
*-----------------------------------------------------------------------------------------------*/
55

6-
import { KubernetesObject } from '@kubernetes/client-node';
6+
import { CoreV1Api, KubeConfig, KubernetesObject, V1Secret, V1ServiceAccount } from '@kubernetes/client-node';
77
import { Cluster as KcuCluster, Context as KcuContext } from '@kubernetes/client-node/dist/config_types';
88
import * as https from 'https';
99
import { ExtensionContext, QuickInputButtons, QuickPickItem, QuickPickItemButtonEvent, ThemeIcon, Uri, commands, env, window, workspace } from 'vscode';
@@ -1070,4 +1070,82 @@ export class Cluster extends OpenShiftItem {
10701070
const asUrl = new URL(url);
10711071
return asUrl.hostname.endsWith('openshiftapps.com');
10721072
}
1073+
1074+
static prepareSSOInKubeConfig(proxy: string, username: string, accessToken: string): KubeConfig {
1075+
const kcu = new KubeConfig();
1076+
const clusterProxy = {
1077+
name: 'sandbox-proxy',
1078+
server: proxy,
1079+
};
1080+
const user = {
1081+
name: 'sso-user',
1082+
token: accessToken,
1083+
};
1084+
const context = {
1085+
cluster: clusterProxy.name,
1086+
name: 'sandbox-proxy-context',
1087+
user: user.name,
1088+
namespace: `${username}-dev`,
1089+
};
1090+
kcu.addCluster(clusterProxy);
1091+
kcu.addUser(user)
1092+
kcu.addContext(context);
1093+
kcu.setCurrentContext(context.name);
1094+
return kcu;
1095+
}
1096+
1097+
static async installPipelineSecretToken(k8sApi: CoreV1Api, pipelineServiceAccount: V1ServiceAccount, username: string): Promise<V1Secret> {
1098+
const v1Secret = {
1099+
apiVersion: 'v1',
1100+
kind: 'Secret',
1101+
metadata: {
1102+
name: `pipeline-secret-${username}-dev`,
1103+
annotations: {
1104+
'kubernetes.io/service-account.name': pipelineServiceAccount.metadata.name,
1105+
'kubernetes.io/service-account.uid': pipelineServiceAccount.metadata.uid
1106+
}
1107+
},
1108+
type: 'kubernetes.io/service-account-token'
1109+
} as V1Secret
1110+
1111+
try {
1112+
await k8sApi.createNamespacedSecret(`${username}-dev`, v1Secret);
1113+
} catch {
1114+
// Ignore
1115+
}
1116+
const newSecrets = await k8sApi.listNamespacedSecret(`${username}-dev`);
1117+
return newSecrets?.body.items.find((secret) => secret.metadata.name === `pipeline-secret-${username}-dev`);
1118+
}
1119+
1120+
static async getPipelineServiceAccountToken(k8sApi: CoreV1Api, username: string): Promise<string> {
1121+
try {
1122+
const serviceAccounts = await k8sApi.listNamespacedServiceAccount(`${username}-dev`);
1123+
const pipelineServiceAccount = serviceAccounts.body.items.find(serviceAccount => serviceAccount.metadata.name === 'pipeline');
1124+
if (!pipelineServiceAccount) {
1125+
return;
1126+
}
1127+
1128+
const secrets = await k8sApi.listNamespacedSecret(`${username}-dev`);
1129+
let pipelineTokenSecret = secrets?.body.items.find((secret) => secret.metadata.name === `pipeline-secret-${username}-dev`);
1130+
if (!pipelineTokenSecret) {
1131+
pipelineTokenSecret = await Cluster.installPipelineSecretToken(k8sApi, pipelineServiceAccount, username);
1132+
if (!pipelineTokenSecret) {
1133+
return;
1134+
}
1135+
}
1136+
return Buffer.from(pipelineTokenSecret.data.token, 'base64').toString();
1137+
} catch {
1138+
// Ignore
1139+
}
1140+
}
1141+
1142+
static async loginUsingPipelineServiceAccountToken(server: string, proxy: string, username: string, accessToken: string): Promise<string> {
1143+
const kcu = Cluster.prepareSSOInKubeConfig(proxy, username, accessToken);
1144+
const k8sApi = kcu.makeApiClient(CoreV1Api);
1145+
const pipelineToken = await this.getPipelineServiceAccountToken(k8sApi, username);
1146+
if (!pipelineToken) {
1147+
return;
1148+
}
1149+
return Cluster.tokenLogin(server, true, pipelineToken);
1150+
}
10731151
}

src/openshift/sandbox.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface SBSignupResponse {
2828
givenName: string;
2929
status: SBStatus;
3030
username: string;
31+
proxyURL: string;
3132
}
3233

3334
export interface SBResponseData {

src/webview/cluster/app/sandboxView.tsx

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ export default function addSandboxView(): JSX.Element {
5656
const [currentState, setCurrentState] = React.useState({
5757
action: 'sandboxPageDetectAuthSession',
5858
statusInfo: '',
59+
usePipelineToken: false,
5960
consoleDashboard: '',
6061
apiEndpoint: '',
62+
apiEndpointProxy: '',
6163
oauthTokenEndpoint: '',
6264
errorCode: undefined
6365
});
@@ -149,7 +151,9 @@ export default function addSandboxView(): JSX.Element {
149151
action: currentState.action,
150152
consoleDashboard: currentState.consoleDashboard,
151153
statusInfo: currentState.statusInfo,
154+
usePipelineToken: false,
152155
apiEndpoint: '',
156+
apiEndpointProxy: '',
153157
oauthTokenEndpoint: '',
154158
errorCode: undefined
155159
});
@@ -299,8 +303,10 @@ export default function addSandboxView(): JSX.Element {
299303
setCurrentState({
300304
action: 'sandboxPageRequestVerificationCode',
301305
statusInfo: '',
306+
usePipelineToken: false,
302307
consoleDashboard: '',
303308
apiEndpoint: '',
309+
apiEndpointProxy: '',
304310
oauthTokenEndpoint: '',
305311
errorCode: undefined
306312
});
@@ -379,11 +385,27 @@ export default function addSandboxView(): JSX.Element {
379385
const Provisioned = () => {
380386

381387
const handleLoginButton = () => {
382-
postMessage('sandboxLoginUsingDataInClipboard', {apiEndpointUrl: currentState.apiEndpoint, oauthRequestTokenUrl: `${currentState.oauthTokenEndpoint}/request`});
388+
if (!currentState.usePipelineToken) { // Try loging in using a token from the Clipboard
389+
postMessage('sandboxLoginUsingDataInClipboard', {
390+
apiEndpointUrl: currentState.apiEndpoint,
391+
oauthRequestTokenUrl: `${currentState.oauthTokenEndpoint}/request`
392+
});
393+
} else { // Try loging in using a Pipeline Token
394+
postMessage('sandboxLoginUsingPipelineToken', {
395+
apiEndpointUrl: currentState.apiEndpoint,
396+
oauthRequestTokenUrl: `${currentState.oauthTokenEndpoint}/request`,
397+
username: currentState.statusInfo,
398+
apiEndpointProxy: currentState.apiEndpointProxy,
399+
});
400+
}
383401
};
384402

385403
const invalidToken = currentState.errorCode === 'invalidToken';
386-
const loginSandboxTitle = !invalidToken ? 'Login to DevSandbox OpenShift cluster with token from clipboard' : 'Token in clipboard is invalid. Select the Get Token option and copy to clipboard';
404+
const loginSandboxTitle = !invalidToken ?
405+
currentState.usePipelineToken ?
406+
'Login to DevSandbox OpenShift cluster using a service account provided token' :
407+
'Login to DevSandbox OpenShift cluster with token from clipboard' :
408+
'Token in clipboard is invalid. Select the Get Token option and copy to clipboard';
387409

388410
return (
389411
<>
@@ -403,19 +425,29 @@ export default function addSandboxView(): JSX.Element {
403425
</Tooltip>
404426
Your sandbox account has been provisioned and is ready to use.
405427
</Typography>
406-
<Typography variant='caption' color='inherit' display='block' style={{ textAlign:'left', margin: '20px 70px' }}>
407-
Next steps to connect with Developer Sandbox:<br></br>
408-
1. Click on <strong>Get token</strong> button. In the browser, login using <strong>DevSandbox</strong> button.<br></br>
409-
2. Click on <strong>Display token</strong> link and copy token to clipboard.<br></br>
410-
3. Switch back to IDE and press <strong>'Login To DevSandbox'</strong> button. This will login you to DevSandbox with token from clipboard.<br></br>
411-
4. Once successfully logged in, start creating applications and deploy on cluster.
412-
</Typography>
428+
{( !currentState.usePipelineToken ) ? (
429+
<Typography variant='caption' color='inherit' display='block' style={{ textAlign:'left', margin: '20px 70px' }}>
430+
Next steps to connect with Developer Sandbox:<br></br>
431+
1. Click on <strong>Get token</strong> button. In the browser, login using <strong>DevSandbox</strong> button.<br></br>
432+
2. Click on <strong>Display token</strong> link and copy token to clipboard.<br></br>
433+
3. Switch back to IDE and press <strong>'Login To DevSandbox'</strong> button. This will login you to DevSandbox with token from clipboard.<br></br>
434+
4. Once successfully logged in, start creating applications and deploy on cluster.
435+
</Typography>
436+
) : (
437+
<Typography variant='caption' color='inherit' display='block' style={{ textAlign:'left', margin: '20px 70px' }}>
438+
Next steps to connect with Developer Sandbox:<br></br>
439+
1. Press <strong>'Login To DevSandbox'</strong> button. This will login you to DevSandbox using a service account provided token.<br></br>
440+
2. Once successfully logged in, start creating applications and deploy on cluster.
441+
</Typography>
442+
)}
413443
<Tooltip title='Launch your DevSandbox console in browser' placement='bottom'>
414444
<Button variant='contained' className='button' href={currentState.consoleDashboard}>Open Dashboard</Button>
415445
</Tooltip>
416-
<Tooltip title='Open the DevSandbox console page and copy the login token' placement='bottom'>
417-
<Button variant='contained' className='button' href={`${currentState.oauthTokenEndpoint}/request`}>Get token</Button>
418-
</Tooltip>
446+
{( !currentState.usePipelineToken ) && (
447+
<Tooltip title='Open the DevSandbox console page and copy the login token' placement='bottom'>
448+
<Button variant='contained' className='button' href={`${currentState.oauthTokenEndpoint}/request`}>Get token</Button>
449+
</Tooltip>
450+
)}
419451
<Tooltip title={loginSandboxTitle} placement='bottom'>
420452
<div style={{ display: 'inline-block', margin: '8px 0px 8px 0px' }}><Button variant='contained' className='buttonRed' disabled={invalidToken} onClick={handleLoginButton}>Login to DevSandbox</Button></div>
421453
</Tooltip>

src/webview/cluster/clusterViewLoader.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright (c) Red Hat, Inc. All rights reserved.
33
* Licensed under the MIT License. See LICENSE file in the project root for license information.
44
*-----------------------------------------------------------------------------------------------*/
5+
import { CoreV1Api } from '@kubernetes/client-node';
56
import { ChildProcess, spawn } from 'child_process';
67
import * as fs from 'fs';
78
import * as path from 'path';
@@ -116,12 +117,36 @@ async function clusterEditorMessageListener (event: any ): Promise<any> {
116117
} else {
117118
if (signupStatus.status.ready) {
118119
const oauthInfo = await sandboxAPI.getOauthServerInfo(signupStatus.apiEndpoint);
120+
121+
const makeCoreV1ApiClient = ((server: string, proxy: string, username: string, accessToken: string): CoreV1Api => {
122+
const kcu = Cluster.prepareSSOInKubeConfig(proxy, username, accessToken);
123+
const apiClient = new CoreV1Api(proxy);
124+
apiClient.setDefaultAuthentication(kcu);
125+
return apiClient;
126+
});
127+
const pipelineAccountToken = await Cluster.getPipelineServiceAccountToken(
128+
makeCoreV1ApiClient(signupStatus.apiEndpoint, signupStatus.proxyURL,
129+
signupStatus.compliantUsername, (sessionCheck as any).idToken),
130+
signupStatus.compliantUsername);
119131
let errCode = '';
120-
if (!Cluster.validateLoginToken((await vscode.env.clipboard.readText()).trim())) {
121-
errCode = 'invalidToken';
132+
if (!pipelineAccountToken) { // Try loging in using a token from the Clipboard
133+
if (!Cluster.validateLoginToken((await vscode.env.clipboard.readText()).trim())) {
134+
errCode = 'invalidToken';
135+
}
136+
}
137+
await panel.webview.postMessage({
138+
action: 'sandboxPageProvisioned',
139+
statusInfo: signupStatus.compliantUsername,
140+
usePipelineToken: (pipelineAccountToken),
141+
consoleDashboard: signupStatus.consoleURL,
142+
apiEndpoint: signupStatus.apiEndpoint,
143+
apiEndpointProxy: signupStatus.proxyURL,
144+
oauthTokenEndpoint: oauthInfo.token_endpoint,
145+
errorCode: errCode
146+
});
147+
if (!pipelineAccountToken) { // Try loging in using a token from the Clipboard
148+
await pollClipboard(signupStatus);
122149
}
123-
await panel.webview.postMessage({ action: 'sandboxPageProvisioned', statusInfo: signupStatus.username, consoleDashboard: signupStatus.consoleURL, apiEndpoint: signupStatus.apiEndpoint, oauthTokenEndpoint: oauthInfo.token_endpoint, errorCode: errCode });
124-
await pollClipboard(signupStatus);
125150
} else {
126151
// cluster is not ready and the reason is
127152
if (signupStatus.status.verificationRequired) {
@@ -211,6 +236,23 @@ async function clusterEditorMessageListener (event: any ): Promise<any> {
211236
}
212237
break;
213238
}
239+
case 'sandboxLoginUsingPipelineToken': {
240+
const telemetryEventLoginToSandbox = new ExtCommandTelemetryEvent('openshift.explorer.addCluster.sandboxLoginUsingPipelineToken');
241+
try {
242+
const result = await Cluster.loginUsingPipelineServiceAccountToken(
243+
event.payload.apiEndpointUrl,
244+
event.payload.apiEndpointProxy,
245+
event.payload.username,
246+
(sessionCheck as any).idToken
247+
);
248+
if (result) void vscode.window.showInformationMessage(`${result}`);
249+
telemetryEventLoginToSandbox.send();
250+
} catch (err) {
251+
void vscode.window.showErrorMessage(err.message);
252+
telemetryEventLoginToSandbox.sendError('Login into Sandbox Cluster failed.');
253+
}
254+
break;
255+
}
214256
default:
215257
void vscode.window.showErrorMessage(`Unexpected message from webview: '${event.action}'`);
216258
break;
@@ -219,16 +261,27 @@ async function clusterEditorMessageListener (event: any ): Promise<any> {
219261

220262
async function pollClipboard(signupStatus) {
221263
const oauthInfo = await sandboxAPI.getOauthServerInfo(signupStatus.apiEndpoint);
264+
let firstPoll = true;
222265
while (panel) {
223266
const previousContent = (await vscode.env.clipboard.readText()).trim();
224267
await new Promise(r => setTimeout(r, 500));
225268
const currentContent = (await vscode.env.clipboard.readText()).trim();
226-
if (previousContent && previousContent !== currentContent) {
269+
if (firstPoll || (previousContent && previousContent !== currentContent)) {
270+
firstPoll = false;
227271
let errCode = '';
228272
if (!Cluster.validateLoginToken(currentContent)){
229273
errCode = 'invalidToken';
230274
}
231-
void panel.webview.postMessage({action: 'sandboxPageProvisioned', statusInfo: signupStatus.username, consoleDashboard: signupStatus.consoleURL, apiEndpoint: signupStatus.apiEndpoint, oauthTokenEndpoint: oauthInfo.token_endpoint, errorCode: errCode});
275+
void panel.webview.postMessage({
276+
action: 'sandboxPageProvisioned',
277+
statusInfo: signupStatus.username,
278+
usePipelineToken: false,
279+
consoleDashboard: signupStatus.consoleURL,
280+
apiEndpoint: signupStatus.apiEndpoint,
281+
apiEndpointProxy: signupStatus.proxyURL,
282+
oauthTokenEndpoint: oauthInfo.token_endpoint,
283+
errorCode: errCode
284+
});
232285
}
233286
}
234287
}

0 commit comments

Comments
 (0)