Skip to content
Closed
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
26 changes: 26 additions & 0 deletions packages/plugins/apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A Vite plugin that builds a deployable Datadog Apps package. Publishing is owned
<!-- #toc -->
- [Configuration](#configuration)
- [Development server authentication](#development-server-authentication)
- [Custom Credentials for local execution](#custom-credentials-for-local-execution)
- [Package output](#package-output)
- [apps.enable](#appsenable)
- [apps.include](#appsinclude)
Expand Down Expand Up @@ -45,6 +46,31 @@ passes it to the dev server via `DD_OAUTH_ACCESS_TOKEN`. When no credentials are
configured, backend function execution is unavailable and the dev server tells
you to start it with `datadog-apps dev`.

## Custom Credentials for local execution

During `vite dev`, backend functions read Custom Credentials from `process.env`, under the same
names the deployed app uses. Set them in the shell that starts the dev server, or in Vite's
`.env`, `.env.local`, `.env.[mode]` and `.env.[mode].local` files. A shell variable takes
precedence over a file.

- Don't give a credential Vite's `envPrefix` (`VITE_` by default): Vite exposes those keys to
the frontend, so they are not loaded for backend functions.
- `DD_*` and `DATADOG_*` keys in these files are ignored. Set the authentication variables above
in the shell, or start the dev server with `datadog-apps dev`.
- Keys that change Node or process behavior are ignored too, and left to the shell: `NODE_*`,
`LD_*`, `DYLD_*`, `OPENSSL_*`, proxy, DNS resolver and CA certificate settings, `TZ`, `PATH`,
`PATHEXT`, `COMSPEC`, `SystemRoot`, `windir`, `ESBUILD_BINARY_PATH`, `UV_THREADPOOL_SIZE`,
`UV_USE_IO_URING`, home and temp directories.
- `dev:verify`, which runs backend functions in the cloud, doesn't load these files, and neither
does a Vitest run or a dev server in `test` mode.
- After a dev server restart, `vite.config`, other plugins' `config` and `configureServer` hooks
and `${VAR}` references in `envPrefix` keys still see the values loaded before the restart.
- Don't pass `process.env` to Vite's `define`, as in `define: { 'process.env': process.env }`:
it would inline the loaded values, backend-only credentials included, into the frontend
bundle, notably after a restart, while the previous load is still in `process.env`.
- Keep files that hold real secrets, such as `.env.local`, out of version control.
- Values from these files are never bundled into the deployed backend functions.

## Package output

A production `vite build` writes `datadog-app-assets.zip` beside the Vite output. The ZIP contains `frontend/`, `backend/`, and `manifest.json`. The app's identity is resolved by `@datadog/apps-cli` at deploy time.
Expand Down
69 changes: 68 additions & 1 deletion packages/plugins/apps/src/vite/build-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

import { outputFileSync, rmSync } from '@dd/core/helpers/fs';
import { getTempWorkingDir } from '@dd/tests/_jest/helpers/env';
import { build } from 'vite';
import { build, loadEnv } from 'vite';

import { getBaseBackendBuildConfig } from './build-config';
import { dropEnvFileCredentials, loadEnvFileCredentials } from './dotenv-credentials';

const isAlwaysActive = () => true;

describe('getBaseBackendBuildConfig', () => {
test('bundles a backend function that imports a real Node builtin module with a working import, not a browser-external stub', async () => {
Expand Down Expand Up @@ -242,4 +245,68 @@ describe('getBaseBackendBuildConfig', () => {
rmSync(workingDir);
}
});

test('Should keep a .env credential the dev server copied into process.env as a runtime read', async () => {
const seed = `build-config-dotenv-loaded-${Date.now()}`;
const workingDir = getTempWorkingDir(seed);
const unprefixedValue = 'sk_unprefixed_should_stay_a_runtime_read';

try {
outputFileSync(`${workingDir}/.env`, `QA_TEST_LOADED_SECRET=${unprefixedValue}\n`);
loadEnvFileCredentials(
loadEnv,
{
root: workingDir,
mode: 'development',
envDir: workingDir,
},
isAlwaysActive,
);
expect(process.env.QA_TEST_LOADED_SECRET).toBe(unprefixedValue);

const absolutePath = `${workingDir}/src/readsLoadedEnv.backend.ts`;
outputFileSync(
absolutePath,
`
export async function readsLoadedEnv() {
return process.env.QA_TEST_LOADED_SECRET;
}
`,
);

const virtualId = 'virtual:dd-backend-test:readsLoadedEnv';
const virtualContent = `import { readsLoadedEnv } from ${JSON.stringify(absolutePath)};\nexport async function main($) { return await readsLoadedEnv(); }`;
const baseConfig = getBaseBackendBuildConfig(
workingDir,
{ [virtualId]: virtualContent },
[],
);

const result = await build({
...baseConfig,
build: {
...baseConfig.build,
write: false,
rollupOptions: {
...baseConfig.build.rollupOptions,
input: virtualId,
output: baseConfig.build.rollupOptions.output,
},
},
});

const output = Array.isArray(result) ? result[0] : result;
if (!('output' in output)) {
throw new Error('Unexpected vite.build result');
}
const chunk = output.output[0];
const code = chunk.type === 'chunk' ? chunk.code : '';

expect(code).toContain('process.env.QA_TEST_LOADED_SECRET');
expect(code).not.toContain(unprefixedValue);
} finally {
dropEnvFileCredentials();
rmSync(workingDir);
}
});
});
10 changes: 5 additions & 5 deletions packages/plugins/apps/src/vite/dev-server.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import nock from 'nock';
import os from 'os';
import path from 'path';
import { parseAst } from 'rollup/parseAst';
import { build, createServer, type Plugin, type ViteDevServer } from 'vite';
import { build, createServer, loadEnv, type Plugin, type ViteDevServer } from 'vite';

import { extractConnectionIdsFromModuleGraph } from '../backend/ast-parsing/extract-connection-ids-from-module-graph';
import { encodeQueryName } from '../backend/encodeQueryName';
Expand Down Expand Up @@ -150,7 +150,7 @@ describe('Dev Server Middleware — real end-to-end local execution', () => {
const appsPlugin: Plugin = {
name: 'dd-apps-test',
...getVitePlugin({
bundler: { build },
bundler: { build, loadEnv },
context: getContextMock({ buildRoot: FIXTURE_ROOT }),
options: {
include: [],
Expand Down Expand Up @@ -510,7 +510,7 @@ describe('Dev Server Middleware — editing files between local executions', ()
const appsPlugin: Plugin = {
name: 'dd-apps-test',
...getVitePlugin({
bundler: { build },
bundler: { build, loadEnv },
context,
options: { include: [], longPolling: mockLongPolling },
}),
Expand Down Expand Up @@ -794,7 +794,7 @@ describe('Dev Server Middleware — SSR import warmup warnings', () => {
const appsPlugin: Plugin = {
name: 'dd-apps-test',
...getVitePlugin({
bundler: { build },
bundler: { build, loadEnv },
context,
options: { include: [], longPolling: mockLongPolling },
}),
Expand Down Expand Up @@ -892,7 +892,7 @@ describe('Dev Server Middleware — dynamic imports inside a backend function',
const appsPlugin: Plugin = {
name: 'dd-apps-test',
...getVitePlugin({
bundler: { build },
bundler: { build, loadEnv },
context: getContextMock({ buildRoot: root }),
options: { include: [], longPolling: mockLongPolling },
}),
Expand Down
Loading
Loading