feat(scaffolder-backend): add defaultEnvironment config to scaffolder
Signed-off-by: Rogerio Angeliski <angeliski@hotmail.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': minor
|
||||
'@backstage/plugin-scaffolder-node': patch
|
||||
---
|
||||
|
||||
Add `defaultEnvironment` config to scaffolder to enable more flexible and custom templates. Now it's possible enable access to default parameters and secrets in templates, improving security and reducing complexity.
|
||||
@@ -207,6 +207,11 @@ scaffolder:
|
||||
email: scaffolder@backstage.io
|
||||
# Use to customize the default commit message when new components are created
|
||||
defaultCommitMessage: 'Initial commit'
|
||||
defaultEnvironment:
|
||||
parameters:
|
||||
region: eu-west-1
|
||||
secrets:
|
||||
environment: ${NODE_ENV}
|
||||
|
||||
auth:
|
||||
experimentalDynamicClientRegistration:
|
||||
|
||||
@@ -64,6 +64,67 @@ you will not have any templates available to use. These need to be [added to the
|
||||
To get up and running and try out some templates quickly, you can or copy the
|
||||
catalog locations from the [create-app template](https://github.com/backstage/backstage/blob/master/packages/create-app/templates/default-app/app-config.yaml.hbs).
|
||||
|
||||
## Configuration
|
||||
|
||||
### Default Environment
|
||||
|
||||
The scaffolder supports a `defaultEnvironment` configuration that provides default parameters and secrets to all templates. This reduces template complexity and improves security by centralizing common values.
|
||||
|
||||
```yaml
|
||||
scaffolder:
|
||||
defaultEnvironment:
|
||||
parameters:
|
||||
region: eu-west-1
|
||||
organizationName: acme-corp
|
||||
defaultRegistry: registry.acme-corp.com
|
||||
secrets:
|
||||
AWS_ACCESS_KEY: ${AWS_ACCESS_KEY}
|
||||
GITHUB_TOKEN: ${GITHUB_TOKEN}
|
||||
DOCKER_REGISTRY_TOKEN: ${DOCKER_REGISTRY_TOKEN}
|
||||
```
|
||||
|
||||
#### Default parameters
|
||||
|
||||
Default parameters are accessible via `${{ environment.parameters.* }}` in templates. Default parameters are isolated in their own context to avoid naming conflicts.
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- title: Fill in some steps
|
||||
required:
|
||||
- organizationName
|
||||
properties:
|
||||
organizationName:
|
||||
title: organizationName
|
||||
type: string
|
||||
description: Unique name of the organization
|
||||
ui:autofocus: true
|
||||
ui:options:
|
||||
rows: 5
|
||||
|
||||
steps:
|
||||
- id: deploy
|
||||
name: Deploy Application
|
||||
action: aws:deploy
|
||||
input:
|
||||
region: ${{ environment.parameters.region }} # Resolves to defaultEnvironment.parameters.region
|
||||
organization: ${{ parameters.organizationName }} # Resolves to frontend input value
|
||||
otherOrganization: ${{ environment.parameters.organizationName }} # Resolves to defaultEnvironment.parameters.organizationName
|
||||
```
|
||||
|
||||
#### Secrets
|
||||
|
||||
Default secrets are resolved from environment variables and accessible via `${{ environment.secrets.* }}` in template actions. Secrets are only available during action execution, not in frontend forms.
|
||||
|
||||
```yaml
|
||||
- id: deploy
|
||||
name: Deploy with credentials
|
||||
action: aws:deploy
|
||||
input:
|
||||
accessKey: ${{ environment.secrets.AWS_ACCESS_KEY }} # Resolves to defaultEnvironment.secrets.AWS_ACCESS_KEY
|
||||
```
|
||||
|
||||
**Security Note:** Secrets are automatically masked in logs and are only available to backend actions, never exposed to the frontend.
|
||||
|
||||
## Audit Events
|
||||
|
||||
The Scaffolder backend emits audit events for various operations. Events are grouped logically by `eventId`, with `subEventId` providing further distinction when needed.
|
||||
|
||||
+21
@@ -108,5 +108,26 @@ export interface Config {
|
||||
auditor?: {
|
||||
taskParameterMaxLength?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default environment variables and secrets available to all templates.
|
||||
*/
|
||||
defaultEnvironment?: {
|
||||
/**
|
||||
* Default parameters accessible via ${{ environment.parameters.* }} in templates.
|
||||
*/
|
||||
parameters?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Secret values from environment variables accessible via ${{ environment.secrets.* }} in templates.
|
||||
* Values should reference environment variables like ${SECRET_NAME}.
|
||||
* @visibility secret
|
||||
*/
|
||||
secrets?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { resolveDefaultEnvironment } from './defaultEnvironment';
|
||||
|
||||
describe('defaultEnvironment', () => {
|
||||
describe('resolveDefaultEnvironment', () => {
|
||||
it('should return empty when no defaultEnvironment config is provided', () => {
|
||||
const config = new ConfigReader({});
|
||||
const result = resolveDefaultEnvironment(config);
|
||||
|
||||
expect(result).toEqual({
|
||||
parameters: {},
|
||||
secrets: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve parameters and secrets from config', () => {
|
||||
const config = new ConfigReader({
|
||||
scaffolder: {
|
||||
defaultEnvironment: {
|
||||
parameters: {
|
||||
region: 'us-east-1',
|
||||
organizationName: 'acme-corp',
|
||||
version: '1.0.0',
|
||||
},
|
||||
secrets: {
|
||||
AWS_ACCESS_KEY: 'test-secret-value',
|
||||
DATABASE_PASSWORD: 'db-password',
|
||||
LITERAL_SECRET: 'literal-value',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = resolveDefaultEnvironment(config);
|
||||
|
||||
expect(result).toEqual({
|
||||
parameters: {
|
||||
region: 'us-east-1',
|
||||
organizationName: 'acme-corp',
|
||||
version: '1.0.0',
|
||||
},
|
||||
secrets: {
|
||||
AWS_ACCESS_KEY: 'test-secret-value',
|
||||
DATABASE_PASSWORD: 'db-password',
|
||||
LITERAL_SECRET: 'literal-value',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle only parameters without secrets', () => {
|
||||
const config = new ConfigReader({
|
||||
scaffolder: {
|
||||
defaultEnvironment: {
|
||||
parameters: {
|
||||
region: 'eu-west-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = resolveDefaultEnvironment(config);
|
||||
|
||||
expect(result).toEqual({
|
||||
parameters: {
|
||||
region: 'eu-west-1',
|
||||
},
|
||||
secrets: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
|
||||
export interface DefaultEnvironmentConfig {
|
||||
parameters?: Record<string, any>;
|
||||
secrets?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ResolvedDefaultEnvironment {
|
||||
parameters: JsonObject;
|
||||
secrets: Record<string, string>;
|
||||
}
|
||||
|
||||
export function resolveDefaultEnvironment(
|
||||
config: Config,
|
||||
): ResolvedDefaultEnvironment {
|
||||
const defaultEnvConfig = config.getOptionalConfig(
|
||||
'scaffolder.defaultEnvironment',
|
||||
);
|
||||
if (!defaultEnvConfig) {
|
||||
return {
|
||||
parameters: {},
|
||||
secrets: {},
|
||||
};
|
||||
}
|
||||
|
||||
const parameters: JsonObject = {};
|
||||
const secrets: Record<string, string> = {};
|
||||
|
||||
const parametersConfig = defaultEnvConfig.getOptionalConfig('parameters');
|
||||
if (parametersConfig) {
|
||||
for (const paramKey of parametersConfig.keys()) {
|
||||
const paramValue = parametersConfig.getString(paramKey);
|
||||
parameters[paramKey] = paramValue;
|
||||
}
|
||||
}
|
||||
|
||||
const secretsConfig = defaultEnvConfig.getOptionalConfig('secrets');
|
||||
if (secretsConfig) {
|
||||
for (const secretKey of secretsConfig.keys()) {
|
||||
const secretValue = secretsConfig.getString(secretKey);
|
||||
secrets[secretKey] = secretValue;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
parameters,
|
||||
secrets,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import type { UserEntity } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
|
||||
import {
|
||||
@@ -79,6 +80,7 @@ export type TemplateTesterCreateOptions = {
|
||||
additionalTemplateFilters?: Record<string, TemplateFilter>;
|
||||
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
|
||||
permissions?: PermissionEvaluator;
|
||||
config?: Config;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -105,6 +107,7 @@ export function createDryRunner(options: TemplateTesterCreateOptions) {
|
||||
},
|
||||
}),
|
||||
]),
|
||||
config: options.config,
|
||||
});
|
||||
|
||||
// Extracting contentsPath and dryRunId from the baseUrl
|
||||
|
||||
@@ -53,6 +53,16 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
scaffolder: {
|
||||
defaultEnvironment: {
|
||||
parameters: {
|
||||
region: 'us-east-1',
|
||||
},
|
||||
secrets: {
|
||||
AWS_ACCESS_KEY: 'test-secret-value',
|
||||
},
|
||||
},
|
||||
},
|
||||
integrations: {
|
||||
github: [{ host: 'github.com', token: 'token' }],
|
||||
},
|
||||
@@ -212,12 +222,26 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
{ result: AuthorizeResult.ALLOW },
|
||||
]);
|
||||
|
||||
const config = new ConfigReader({
|
||||
scaffolder: {
|
||||
defaultEnvironment: {
|
||||
parameters: {
|
||||
region: 'us-east-1',
|
||||
},
|
||||
secrets: {
|
||||
AWS_ACCESS_KEY: 'test-secret-value',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
runner = new NunjucksWorkflowRunner({
|
||||
actionRegistry,
|
||||
integrations,
|
||||
workingDirectory: mockDir.path,
|
||||
logger,
|
||||
permissions: mockedPermissionApi,
|
||||
config,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -494,6 +518,7 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
action: 'jest-mock-action',
|
||||
input: {
|
||||
foo: '${{parameters.input | lower }}',
|
||||
region: '${{environment.parameters.region}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -505,7 +530,9 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
await runner.execute(task);
|
||||
|
||||
expect(fakeActionHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ input: { foo: 'backstage' } }),
|
||||
expect.objectContaining({
|
||||
input: { foo: 'backstage', region: 'us-east-1' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -777,6 +804,49 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
expectTaskLog('info: ***');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('should redact secrets that are passed in the environment', async () => {
|
||||
actionRegistry.register({
|
||||
id: 'log-secret',
|
||||
description: 'Mock action for testing',
|
||||
supportsDryRun: true,
|
||||
handler: async ctx => {
|
||||
ctx.logger.info(ctx.input.secret);
|
||||
},
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['secret'],
|
||||
properties: {
|
||||
secret: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const task = createMockTaskWithSpec(
|
||||
{
|
||||
steps: [
|
||||
{
|
||||
id: 'test',
|
||||
name: 'name',
|
||||
action: 'log-secret',
|
||||
input: {
|
||||
secret: '${{ environment.secrets.AWS_ACCESS_KEY }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ secret: 'my-secret-value' },
|
||||
);
|
||||
|
||||
await runner.execute(task);
|
||||
|
||||
expectTaskLog('info: ***');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('should redact meta fields properly', async () => {
|
||||
actionRegistry.register({
|
||||
@@ -1083,7 +1153,9 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
await runner.execute(task);
|
||||
|
||||
expect(fakeActionHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ secrets: { foo: 'bar' } }),
|
||||
expect.objectContaining({
|
||||
secrets: { foo: 'bar' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1097,6 +1169,7 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
action: 'jest-mock-action',
|
||||
input: {
|
||||
b: '${{ secrets.foo }}',
|
||||
aws_key: '${{ environment.secrets.AWS_ACCESS_KEY }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -1107,7 +1180,41 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
await runner.execute(task);
|
||||
|
||||
expect(fakeActionHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ input: { b: 'bar' } }),
|
||||
expect.objectContaining({
|
||||
input: { b: 'bar', aws_key: 'test-secret-value' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should separate task secrets from environment secrets', async () => {
|
||||
const task = createMockTaskWithSpec(
|
||||
{
|
||||
steps: [
|
||||
{
|
||||
id: 'test',
|
||||
name: 'name',
|
||||
action: 'jest-mock-action',
|
||||
input: {
|
||||
b: '${{ secrets.foo }}',
|
||||
aws_key: '${{ secrets.AWS_ACCESS_KEY }}',
|
||||
env_aws_key: '${{ environment.secrets.AWS_ACCESS_KEY }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ foo: 'bar', AWS_ACCESS_KEY: 'another-value-from-task' },
|
||||
);
|
||||
|
||||
await runner.execute(task);
|
||||
|
||||
expect(fakeActionHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: {
|
||||
b: 'bar',
|
||||
aws_key: 'another-value-from-task',
|
||||
env_aws_key: 'test-secret-value',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1126,6 +1233,7 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
],
|
||||
output: {
|
||||
b: '${{ secrets.foo }}',
|
||||
c: '${{ environment.secrets.AWS_ACCESS_KEY }}',
|
||||
},
|
||||
},
|
||||
{ foo: 'bar' },
|
||||
@@ -1134,6 +1242,7 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
const executedTask = await runner.execute(task);
|
||||
|
||||
expect(executedTask.output.b).toBeUndefined();
|
||||
expect(executedTask.output.c).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ import { createConditionAuthorizer } from '@backstage/plugin-permission-node';
|
||||
import { actionExecutePermission } from '@backstage/plugin-scaffolder-common/alpha';
|
||||
import {
|
||||
TaskContext,
|
||||
TaskSecrets,
|
||||
TemplateAction,
|
||||
TemplateFilter,
|
||||
TemplateGlobal,
|
||||
@@ -64,6 +65,8 @@ import {
|
||||
CheckpointState,
|
||||
CheckpointContext,
|
||||
} from '@backstage/plugin-scaffolder-node/alpha';
|
||||
import { Config } from '@backstage/config';
|
||||
import { resolveDefaultEnvironment } from '../../lib/defaultEnvironment';
|
||||
|
||||
type NunjucksWorkflowRunnerOptions = {
|
||||
workingDirectory: string;
|
||||
@@ -74,10 +77,12 @@ type NunjucksWorkflowRunnerOptions = {
|
||||
additionalTemplateFilters?: Record<string, TemplateFilter>;
|
||||
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
|
||||
permissions?: PermissionsService;
|
||||
config?: Config;
|
||||
};
|
||||
|
||||
type TemplateContext = {
|
||||
parameters: JsonObject;
|
||||
environment: JsonObject;
|
||||
EXPERIMENTAL_recovery?: TaskRecovery;
|
||||
steps: {
|
||||
[stepName: string]: { output: { [outputName: string]: JsonValue } };
|
||||
@@ -103,10 +108,12 @@ const createStepLogger = ({
|
||||
task,
|
||||
step,
|
||||
rootLogger,
|
||||
secretsForRedaction,
|
||||
}: {
|
||||
task: TaskContext;
|
||||
step: TaskStep;
|
||||
rootLogger: LoggerService;
|
||||
secretsForRedaction?: string[];
|
||||
}) => {
|
||||
const taskLogger = WinstonLogger.create({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
@@ -118,6 +125,7 @@ const createStepLogger = ({
|
||||
});
|
||||
|
||||
taskLogger.addRedactions(Object.values(task.secrets ?? {}));
|
||||
taskLogger.addRedactions(Object.values(secretsForRedaction ?? {}));
|
||||
|
||||
return { taskLogger };
|
||||
};
|
||||
@@ -128,6 +136,10 @@ const isActionAuthorized = createConditionAuthorizer(
|
||||
|
||||
export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
private readonly defaultTemplateFilters: Record<string, TemplateFilter>;
|
||||
private environment: {
|
||||
parameters: JsonObject;
|
||||
secrets?: Record<string, string>;
|
||||
} = { parameters: {}, secrets: {} };
|
||||
|
||||
constructor(private readonly options: NunjucksWorkflowRunnerOptions) {
|
||||
this.defaultTemplateFilters = convertFiltersToRecord(
|
||||
@@ -139,6 +151,24 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
|
||||
private readonly tracker = scaffoldingTracker();
|
||||
|
||||
async getEnvironmentConfig(): Promise<{
|
||||
parameters: JsonObject;
|
||||
secrets?: TaskSecrets;
|
||||
}> {
|
||||
if (this.options.config) {
|
||||
const defaultEnvironment = resolveDefaultEnvironment(this.options.config);
|
||||
return {
|
||||
parameters: defaultEnvironment.parameters,
|
||||
secrets: defaultEnvironment.secrets,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
parameters: {},
|
||||
secrets: {},
|
||||
};
|
||||
}
|
||||
|
||||
private isSingleTemplateString(input: string) {
|
||||
const { parser, nodes } = nunjucks as unknown as {
|
||||
parser: {
|
||||
@@ -250,18 +280,32 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
task,
|
||||
step,
|
||||
rootLogger: this.options.logger,
|
||||
secretsForRedaction: this.environment?.secrets
|
||||
? Object.values(this.environment.secrets)
|
||||
: [],
|
||||
});
|
||||
|
||||
if (task.isDryRun) {
|
||||
const redactedSecrets = Object.fromEntries(
|
||||
Object.entries(task.secrets ?? {}).map(secret => [secret[0], '***']),
|
||||
);
|
||||
|
||||
const redactedEnvironmentSecrets = Object.fromEntries(
|
||||
Object.entries(this.environment?.secrets ?? {}).map(secret => [
|
||||
secret[0],
|
||||
'***',
|
||||
]),
|
||||
);
|
||||
const debugInput =
|
||||
(step.input &&
|
||||
this.render(
|
||||
step.input,
|
||||
{
|
||||
...context,
|
||||
environment: {
|
||||
parameters: this.environment?.parameters || {},
|
||||
secrets: redactedEnvironmentSecrets,
|
||||
},
|
||||
secrets: redactedSecrets,
|
||||
},
|
||||
renderTemplate,
|
||||
@@ -296,7 +340,14 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
step.each &&
|
||||
this.render(
|
||||
step.each,
|
||||
{ ...context, secrets: task.secrets ?? {} },
|
||||
{
|
||||
...context,
|
||||
environment: {
|
||||
parameters: this.environment?.parameters || {},
|
||||
secrets: this.environment?.secrets ?? {},
|
||||
},
|
||||
secrets: task?.secrets ?? {},
|
||||
},
|
||||
renderTemplate,
|
||||
);
|
||||
|
||||
@@ -318,7 +369,15 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
input: step.input
|
||||
? this.render(
|
||||
step.input,
|
||||
{ ...context, secrets: task.secrets ?? {}, ...i },
|
||||
{
|
||||
...context,
|
||||
environment: {
|
||||
parameters: this.environment?.parameters ?? {},
|
||||
secrets: this.environment?.secrets ?? {},
|
||||
},
|
||||
secrets: task.secrets ?? {},
|
||||
...i,
|
||||
},
|
||||
renderTemplate,
|
||||
)
|
||||
: {},
|
||||
@@ -481,6 +540,8 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
const { additionalTemplateFilters, additionalTemplateGlobals } =
|
||||
this.options;
|
||||
|
||||
this.environment = await this.getEnvironmentConfig();
|
||||
|
||||
const renderTemplate = await SecureTemplater.loadRenderer({
|
||||
templateFilters: {
|
||||
...this.defaultTemplateFilters,
|
||||
@@ -497,6 +558,10 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
|
||||
const context: TemplateContext = {
|
||||
parameters: task.spec.parameters,
|
||||
environment: {
|
||||
parameters: this.environment?.parameters || {},
|
||||
secrets: {},
|
||||
},
|
||||
steps: {},
|
||||
user: task.spec.user,
|
||||
context: {
|
||||
|
||||
@@ -103,6 +103,22 @@ describe('StorageTaskBroker', () => {
|
||||
expect(task.secrets).toEqual(fakeSecrets);
|
||||
}, 10000);
|
||||
|
||||
it('should return secrets with priority over defaults', async () => {
|
||||
const broker = new StorageTaskBroker(storage, logger);
|
||||
await broker.dispatch(emptyTaskWithFakeSecretsSpec);
|
||||
const task = await broker.claim();
|
||||
|
||||
expect(task.secrets).toEqual(fakeSecrets);
|
||||
}, 10000);
|
||||
|
||||
it('should return all secrets', async () => {
|
||||
const broker = new StorageTaskBroker(storage, logger);
|
||||
await broker.dispatch(emptyTaskWithFakeSecretsSpec);
|
||||
const task = await broker.claim();
|
||||
|
||||
expect(task.secrets).toEqual({ ...fakeSecrets });
|
||||
}, 10000);
|
||||
|
||||
it('should complete a task', async () => {
|
||||
const broker = new StorageTaskBroker(storage, logger);
|
||||
const dispatchResult = await broker.dispatch(emptyTaskSpec);
|
||||
@@ -232,7 +248,7 @@ describe('StorageTaskBroker', () => {
|
||||
id: taskId,
|
||||
}),
|
||||
]),
|
||||
totalTasks: 13,
|
||||
totalTasks: 15,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -160,6 +160,10 @@ export type WorkflowResponse = { output: { [key: string]: JsonValue } };
|
||||
|
||||
export interface WorkflowRunner {
|
||||
execute(task: TaskContext): Promise<WorkflowResponse>;
|
||||
getEnvironmentConfig?(): Promise<{
|
||||
parameters: JsonObject;
|
||||
secrets?: TaskSecrets;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type TaskTrackType = {
|
||||
|
||||
@@ -368,6 +368,7 @@ export async function createRouter(
|
||||
auditor,
|
||||
workingDirectory,
|
||||
permissions,
|
||||
config,
|
||||
...templateExtensions,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user