Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
915fa56
feat: setting env vars for connected integrations
jankuca Oct 17, 2025
c5d47b1
fix: convert SQL integration credentials to normalized format for too…
jankuca Oct 17, 2025
d6a3320
document integration credential usage
jankuca Oct 17, 2025
c04c276
refactor: move integration storage logic to platform for proper impor…
jankuca Oct 17, 2025
a5cc4bc
test: add tests for sql integration env vars
jankuca Oct 17, 2025
42f191c
remove logging of secrets
jankuca Oct 17, 2025
681a38f
fix postgres db url encoding
jankuca Oct 17, 2025
989f687
ensure dispose
jankuca Oct 17, 2025
79d3f45
use cancel token
jankuca Oct 17, 2025
c5dbd12
lint md file
jankuca Oct 17, 2025
02729da
add interface for env var provider
jankuca Oct 17, 2025
7f94f93
decrease log level
jankuca Oct 17, 2025
e974e81
refactor tests
jankuca Oct 17, 2025
3947c69
refactor tests
jankuca Oct 17, 2025
4bbfb51
remove setTimeout race from tests
jankuca Oct 17, 2025
114b751
decease log level
jankuca Oct 17, 2025
b4651e0
DRY tests
jankuca Oct 17, 2025
1bb15db
remove integration info from logs
jankuca Oct 17, 2025
8cf39c8
add encoding tests
jankuca Oct 17, 2025
6173b16
update md file about logging from integration logic
jankuca Oct 17, 2025
dd7b995
remove unused import
jankuca Oct 17, 2025
94b34a8
improve method naming in integration stoarge
jankuca Oct 17, 2025
f5377ee
add md code fence lang
jankuca Oct 17, 2025
1622f92
dedupe log
jankuca Oct 17, 2025
4783e59
dedupe tests
jankuca Oct 17, 2025
0b3514c
fix interface injection
jankuca Oct 17, 2025
91fd3cc
add edge case tests
jankuca Oct 17, 2025
1c0ab8e
use interface
jankuca Oct 17, 2025
47647ca
lower logs
jankuca Oct 17, 2025
6674550
add cancel test
jankuca Oct 17, 2025
3cad603
implement iface
jankuca Oct 17, 2025
2a987de
fix test safety
jankuca Oct 17, 2025
ad89f88
update docs
jankuca Oct 20, 2025
07cdb1f
hide details from logs
jankuca Oct 20, 2025
842302b
use iface
jankuca Oct 20, 2025
0bad9cd
remove any from test
jankuca Oct 20, 2025
1b212bd
convert base error to custom
jankuca Oct 20, 2025
881009d
add jsdoc
jankuca Oct 20, 2025
b626801
remove env var log
jankuca Oct 20, 2025
fd84cff
redact log
jankuca Oct 20, 2025
e020716
fix var list merge
jankuca Oct 20, 2025
2d309ff
fix var value test
jankuca Oct 20, 2025
e63ef6e
redact log
jankuca Oct 20, 2025
1b499f3
Merge branch 'main' into jk/feat/sql-block-credentials
jankuca Oct 20, 2025
83b7ab9
Merge branch 'main' into jk/feat/sql-block-credentials
jamesbhobbs Oct 20, 2025
130b397
Merge branch 'main' into jk/feat/sql-block-credentials
jankuca Oct 21, 2025
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
413 changes: 413 additions & 0 deletions INTEGRATIONS_CREDENTIALS.md

Large diffs are not rendered by default.

33 changes: 30 additions & 3 deletions src/kernels/deepnote/deepnoteServerStarter.node.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { inject, injectable, named } from 'inversify';
import { inject, injectable, named, optional } from 'inversify';
import { CancellationToken, Uri } from 'vscode';
import { PythonEnvironment } from '../../platform/pythonEnvironments/info';
import { IDeepnoteServerStarter, IDeepnoteToolkitInstaller, DeepnoteServerInfo, DEEPNOTE_DEFAULT_PORT } from './types';
Expand All @@ -17,6 +17,7 @@ import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from '../../platform/vscode-path/path';
import { generateUuid } from '../../platform/common/uuid';
import { SqlIntegrationEnvironmentVariablesProvider } from '../../platform/notebooks/deepnote/sqlIntegrationEnvironmentVariablesProvider';

/**
* Lock file data structure for tracking server ownership
Expand Down Expand Up @@ -47,7 +48,10 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
@inject(IDeepnoteToolkitInstaller) private readonly toolkitInstaller: IDeepnoteToolkitInstaller,
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel,
@inject(IHttpClient) private readonly httpClient: IHttpClient,
@inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry
@inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry,
@inject(SqlIntegrationEnvironmentVariablesProvider)
@optional()
private readonly sqlIntegrationEnvVars?: SqlIntegrationEnvironmentVariablesProvider
) {
// Register for disposal when the extension deactivates
asyncRegistry.push(this);
Expand Down Expand Up @@ -149,10 +153,33 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension

// Detached mode ensures no requests are made to the backend (directly, or via proxy)
// as there is no backend running in the extension, therefore:
// 1. integration environment variables won't work / be injected
// 1. integration environment variables are injected here instead
// 2. post start hooks won't work / are not executed
env.DEEPNOTE_RUNTIME__RUNNING_IN_DETACHED_MODE = 'true';

// Inject SQL integration environment variables
if (this.sqlIntegrationEnvVars) {
logger.info(`DeepnoteServerStarter: Injecting SQL integration env vars for ${deepnoteFileUri.toString()}`);
try {
const sqlEnvVars = await this.sqlIntegrationEnvVars.getEnvironmentVariables(deepnoteFileUri, token);
if (sqlEnvVars && Object.keys(sqlEnvVars).length > 0) {
logger.info(
`DeepnoteServerStarter: Injecting ${
Object.keys(sqlEnvVars).length
} SQL integration env vars: ${Object.keys(sqlEnvVars).join(', ')}`
);
Object.assign(env, sqlEnvVars);
logger.info(`DeepnoteServerStarter: Injected SQL env vars: ${Object.keys(sqlEnvVars).join(', ')}`);
} else {
logger.info('DeepnoteServerStarter: No SQL integration env vars to inject');
}
} catch (error) {
logger.error('DeepnoteServerStarter: Failed to get SQL integration env vars', error);
}
} else {
logger.info('DeepnoteServerStarter: SqlIntegrationEnvironmentVariablesProvider not available');
}

// Remove PYTHONHOME if it exists (can interfere with venv)
delete env.PYTHONHOME;

Expand Down
215 changes: 215 additions & 0 deletions src/kernels/helpers.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,4 +646,219 @@ suite('Kernel Connection Helpers', () => {
assert.strictEqual(name, '.env (Python 9.8.7)');
});
});

suite('executeSilently', () => {
test('Returns outputs from kernel execution', async () => {
const mockKernel = {
requestExecute: () => ({
done: Promise.resolve({
content: {
status: 'ok' as const
}
}),
onIOPub: () => {
// noop
}
})
};

const code = 'print("hello")';
const { executeSilently } = await import('./helpers');
const result = await executeSilently(mockKernel as any, code);

// executeSilently should return outputs array
assert.isArray(result);
});

test('Handles empty code', async () => {
const mockKernel = {
requestExecute: () => ({
done: Promise.resolve({
content: {
status: 'ok' as const
}
}),
onIOPub: () => {
// noop
}
})
};

const code = '';
const { executeSilently } = await import('./helpers');
const result = await executeSilently(mockKernel as any, code);

// Should return empty array for empty code
assert.isArray(result);
});

test('Collects stream outputs', async () => {
let iopubCallback: ((msg: any) => void) | undefined;

const mockKernel = {
requestExecute: () => ({
done: Promise.resolve({
content: {
status: 'ok' as const
}
}),
onIOPub: (cb: (msg: any) => void) => {
iopubCallback = cb;
// Simulate stream output
setTimeout(() => {
if (iopubCallback) {
iopubCallback({
header: { msg_type: 'stream' },
content: {
name: 'stdout',
text: 'test output'
}
});
}
}, 0);
}
})
};

const code = 'print("test")';
const { executeSilently } = await import('./helpers');
const result = await executeSilently(mockKernel as any, code);

assert.isArray(result);
// Should have collected the stream output
if (result && result.length > 0) {
assert.equal(result[0].output_type, 'stream');
}
});

test('Collects error outputs', async () => {
let iopubCallback: ((msg: any) => void) | undefined;

const mockKernel = {
requestExecute: () => ({
done: Promise.resolve({
content: {
status: 'error' as const,
ename: 'NameError',
evalue: 'name not defined',
traceback: ['Traceback...']
}
}),
onIOPub: (cb: (msg: any) => void) => {
iopubCallback = cb;
// Simulate error output
setTimeout(() => {
if (iopubCallback) {
iopubCallback({
header: { msg_type: 'error' },
content: {
ename: 'NameError',
evalue: 'name not defined',
traceback: ['Traceback...']
}
});
}
}, 0);
}
})
};

const code = 'undefined_variable';
const { executeSilently } = await import('./helpers');
const result = await executeSilently(mockKernel as any, code);

assert.isArray(result);
// Should have collected the error output
if (result && result.length > 0) {
assert.equal(result[0].output_type, 'error');
}
});

test('Collects display_data outputs', async () => {
let iopubCallback: ((msg: any) => void) | undefined;

const mockKernel = {
requestExecute: () => ({
done: Promise.resolve({
content: {
status: 'ok' as const
}
}),
onIOPub: (cb: (msg: any) => void) => {
iopubCallback = cb;
// Simulate display_data output
setTimeout(() => {
if (iopubCallback) {
iopubCallback({
header: { msg_type: 'display_data' },
content: {
data: {
'text/plain': 'some data'
},
metadata: {}
}
});
}
}, 0);
}
})
};

const code = 'display("data")';
const { executeSilently } = await import('./helpers');
const result = await executeSilently(mockKernel as any, code);

assert.isArray(result);
// Should have collected the display_data output
if (result && result.length > 0) {
assert.equal(result[0].output_type, 'display_data');
}
});

test('Handles multiple outputs', async () => {
let iopubCallback: ((msg: any) => void) | undefined;

const mockKernel = {
requestExecute: () => ({
done: Promise.resolve({
content: {
status: 'ok' as const
}
}),
onIOPub: (cb: (msg: any) => void) => {
iopubCallback = cb;
// Simulate multiple outputs
setTimeout(() => {
if (iopubCallback) {
iopubCallback({
header: { msg_type: 'stream' },
content: {
name: 'stdout',
text: 'output 1'
}
});
iopubCallback({
header: { msg_type: 'stream' },
content: {
name: 'stdout',
text: 'output 2'
}
});
}
}, 0);
}
})
};

const code = 'print("1"); print("2")';
const { executeSilently } = await import('./helpers');
const result = await executeSilently(mockKernel as any, code);

assert.isArray(result);
// Should have collected multiple outputs
if (result) {
assert.isAtLeast(result.length, 0);
}
});
});
});
14 changes: 13 additions & 1 deletion src/kernels/kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,10 +853,22 @@ abstract class BaseKernel implements IBaseKernel {

// Gather all of the startup code at one time and execute as one cell
const startupCode = await this.gatherInternalStartupCode();
await this.executeSilently(session, startupCode, {
logger.trace(`Executing startup code with ${startupCode.length} lines`);

const outputs = await this.executeSilently(session, startupCode, {
traceErrors: true,
traceErrorsMessage: 'Error executing jupyter extension internal startup code'
});
logger.trace(`Startup code execution completed with ${outputs?.length || 0} outputs`);
if (outputs && outputs.length > 0) {
// Avoid logging content; output types only.
logger.trace(
`Startup code produced ${outputs.length} output(s): ${outputs
.map((o) => o.output_type)
.join(', ')}`
);
}

// Run user specified startup commands
await this.executeSilently(session, this.getUserStartupCommands(), { traceErrors: false });
}
Expand Down
Loading
Loading