Skip to content

Commit

Permalink
feat: optional varfiles (#5996)
Browse files Browse the repository at this point in the history
feat: add optional property for varfiles

Signed-off-by: Manuel Ruck <git@manuelruck.de>
Co-authored-by: Manuel Ruck <git@manuelruck.de>
Co-authored-by: Vladimir Vagaytsev <vladimir.vagaitsev@gmail.com>
  • Loading branch information
3 people committed May 7, 2024
1 parent 06f8bf6 commit ee36cbb
Show file tree
Hide file tree
Showing 34 changed files with 864 additions and 115 deletions.
12 changes: 10 additions & 2 deletions core/src/actions/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
parseActionReference,
createSchema,
unusedApiVersionSchema,
joiArray,
joiVarfile,
} from "../config/common.js"
import { DOCS_BASE_URL } from "../constants.js"
import { dedent, naturalList, stableStringify } from "../util/string.js"
Expand Down Expand Up @@ -225,7 +227,7 @@ export const baseActionConfigSchema = createSchema({
A map of variables scoped to this particular action. These are resolved before any other parts of the action configuration and take precedence over group-scoped variables (if applicable) and project-scoped variables, in that order. They may reference group-scoped and project-scoped variables, and generally can use any template strings normally allowed when resolving the action.
`
),
varfiles: joiSparseArray(joi.posixPath())
varfiles: joiArray(joiVarfile())
.description(
dedent`
Specify a list of paths (relative to the directory where the action is defined) to a file containing variables, that we apply on top of the action-level \`variables\` field, and take precedence over group-level variables (if applicable) and project-level variables, in that order.
Expand All @@ -236,7 +238,13 @@ export const baseActionConfigSchema = createSchema({
To use different varfiles in different environments, you can template in the environment name to the varfile name, e.g. \`varfile: "my-action.\$\{environment.name\}.env\` (this assumes that the corresponding varfiles exist).
If a listed varfile cannot be found, it is ignored.
If a listed varfile cannot be found, throwing an error.
To add optional varfiles, you can use a list item object with a \`path\` and an optional \`optional\` boolean field.
\`\`\`yaml
varfiles:
- path: my-action.env
optional: true
\`\`\`
`
)
.example("my-action.env")
Expand Down
4 changes: 2 additions & 2 deletions core/src/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import type { ValuesType } from "utility-types"
import type { ConfigGraph, ResolvedConfigGraph } from "../graph/config-graph.js"
import type { ActionReference, DeepPrimitiveMap } from "../config/common.js"
import type { ActionReference, DeepPrimitiveMap, Varfile } from "../config/common.js"
import type { ModuleVersion, TreeVersion } from "../vcs/vcs.js"
import type { BuildAction, BuildActionConfig, ExecutedBuildAction, ResolvedBuildAction } from "./build.js"
import type { DeployAction, DeployActionConfig, ExecutedDeployAction, ResolvedDeployAction } from "./deploy.js"
Expand Down Expand Up @@ -91,7 +91,7 @@ export interface BaseActionConfig<K extends ActionKind = ActionKind, T = string,
// -> Templating with ActionConfigContext allowed
variables?: DeepPrimitiveMap
// -> Templating with ActionConfigContext allowed, including in variables defined in the varfiles
varfiles?: string[]
varfiles?: Varfile[]

// Type-specific
spec: Spec
Expand Down
4 changes: 3 additions & 1 deletion core/src/config/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,11 +551,13 @@ export async function loadVarfile({
configRoot,
path,
defaultPath,
optional = false,
}: {
// project root (when resolving project config) or module root (when resolving module config)
configRoot: string
path: string | undefined
defaultPath: string | undefined
optional?: boolean
}): Promise<PrimitiveMap> {
if (!path && !defaultPath) {
throw new ParameterError({
Expand All @@ -565,7 +567,7 @@ export async function loadVarfile({
const resolvedPath = resolve(configRoot, <string>(path || defaultPath))
const exists = await pathExists(resolvedPath)

if (!exists && path && path !== defaultPath) {
if (!exists && path && path !== defaultPath && !optional) {
throw new ConfigurationError({
message: `Could not find varfile at path '${path}'. Absolute path: ${resolvedPath}`,
})
Expand Down
19 changes: 19 additions & 0 deletions core/src/config/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ export interface DeepPrimitiveMap {
[key: string]: Primitive | DeepPrimitiveMap | Primitive[] | DeepPrimitiveMap[]
}

export interface VarfileMap {
path: string
optional?: boolean
}

export type Varfile = VarfileMap | string

export const includeGuideLink = makeDocsLinkPlain(
"using-garden/configuration-overview",
"#including-excluding-files-and-directories"
Expand Down Expand Up @@ -845,6 +852,18 @@ export const joiIdentifierMap = memoize((valueSchema: Joi.Schema) =>
.description("Key/value map. Keys must be valid identifiers.")
)

export const joiVarfile = memoize(() =>
joi
.alternatives(
joi.posixPath().description("Path to a file containing a path."),
joi.object().keys({
path: joi.posixPath().required().description("Path to a file containing a path."),
optional: joi.boolean().description("Whether the varfile is optional."),
})
)
.description("A path to a file containing variables, or an object with a path and optional flag.")
)

export const joiVariablesDescription =
"Keys may contain letters and numbers. Any values are permitted, including arrays and objects of any nesting."

Expand Down
17 changes: 13 additions & 4 deletions core/src/config/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,17 @@ import dedent from "dedent"
import type { ActionConfig } from "../actions/types.js"
import { baseActionConfigSchema } from "../actions/base.js"
import { templateStringLiteral } from "../docs/common.js"
import type { DeepPrimitiveMap } from "./common.js"
import { createSchema, joi, joiSparseArray, joiUserIdentifier, joiVariables, unusedApiVersionSchema } from "./common.js"
import type { DeepPrimitiveMap, Varfile } from "./common.js"
import {
createSchema,
joi,
joiArray,
joiSparseArray,
joiUserIdentifier,
joiVarfile,
joiVariables,
unusedApiVersionSchema,
} from "./common.js"
import { varfileDescription } from "./base.js"

export interface GroupConfig {
Expand All @@ -29,7 +38,7 @@ export interface GroupConfig {

// Variables
variables?: DeepPrimitiveMap
varfiles?: string[]
varfiles?: Varfile[]

// Actions
actions: ActionConfig[]
Expand All @@ -50,7 +59,7 @@ export const groupConfig = createSchema({
variables: joiVariables().default(() => undefined).description(dedent`
A map of variables scoped to the actions in this group. These are resolved before the actions and take precedence over project-scoped variables. They may reference project-scoped variables, and generally use any template strings normally allowed when resolving the action.
`),
varfiles: joiSparseArray(joi.posixPath())
varfiles: joiArray(joiVarfile())
.description(
dedent`
Specify a list of paths (relative to the directory where the group is defined) to a file containing variables, that we apply on top of the group-level \`variables\` field. If you specify multiple paths, they are merged in the order specified, i.e. the last one takes precedence over the previous ones.
Expand Down
4 changes: 2 additions & 2 deletions core/src/graph/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import {
resolveTemplateStrings,
} from "../template-string/template-string.js"
import { dedent, deline, naturalList } from "../util/string.js"
import { mergeVariables } from "./common.js"
import { getVarfileData, mergeVariables } from "./common.js"
import type { ConfigGraph } from "./config-graph.js"
import { MutableConfigGraph } from "./config-graph.js"
import type { ModuleGraph } from "./modules.js"
Expand Down Expand Up @@ -627,7 +627,7 @@ export const preprocessActionConfig = profileAsync(async function preprocessActi
const templateName = config.internal.templateName

// in pre-processing, only use varfiles that are not template strings
const resolvedVarFiles = config.varfiles?.filter((f) => !maybeTemplateString(f))
const resolvedVarFiles = config.varfiles?.filter((f) => !maybeTemplateString(getVarfileData(f).path))
const variables = await mergeVariables({
basePath: config.internal.basePath,
variables: config.variables,
Expand Down
14 changes: 11 additions & 3 deletions core/src/graph/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Profile, profileAsync } from "../util/profiling.js"
import type { ModuleDependencyGraphNode, ModuleDependencyGraphNodeKind, ModuleGraphNodes } from "./modules.js"
import type { ActionKind } from "../plugin/action-types.js"
import { loadVarfile } from "../config/base.js"
import type { DeepPrimitiveMap } from "../config/common.js"
import type { DeepPrimitiveMap, Varfile } from "../config/common.js"
import type { Task } from "../tasks/base.js"
import type { LogMetadata, TaskLogStatus } from "../logger/log-entry.js"

Expand Down Expand Up @@ -121,21 +121,29 @@ interface CycleGraph {
}
}

export const getVarfileData = (varfile: Varfile) => {
const path = typeof varfile === "string" ? varfile : varfile.path
const optional = typeof varfile === "string" ? false : varfile.optional
return { path, optional }
}

export const mergeVariables = profileAsync(async function mergeVariables({
basePath,
variables,
varfiles,
}: {
basePath: string
variables?: DeepPrimitiveMap
varfiles?: string[]
varfiles?: Varfile[]
}) {
const varsByFile = await Promise.all(
(varfiles || []).map((path) => {
(varfiles || []).map((varfile) => {
const { path, optional } = getVarfileData(varfile)
return loadVarfile({
configRoot: basePath,
path,
defaultPath: undefined,
optional,
})
})
)
Expand Down
34 changes: 34 additions & 0 deletions core/test/unit/src/actions/action-configs-to-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,40 @@ describe("actionConfigsToGraph", () => {
expect(vars).to.eql({ projectName: "${project.name}" })
})

it("loads optional varfiles for the action", async () => {
const varfilePath = join(tmpDir.path, "varfile.yml")
await dumpYaml(varfilePath, {
projectName: "${project.name}",
})

const graph = await actionConfigsToGraph({
garden,
log,
groupConfigs: [],
configs: [
{
kind: "Build",
type: "test",
name: "foo",
timeout: DEFAULT_BUILD_TIMEOUT_SEC,
varfiles: [{ path: varfilePath, optional: true }],
internal: {
basePath: tmpDir.path,
},
spec: {},
},
],
moduleGraph: new ModuleGraph([], {}),
actionModes: {},
linkedSources: {},
})

const action = graph.getBuild("foo")
const vars = action["variables"]

expect(vars).to.eql({ projectName: "${project.name}" })
})

it("correctly merges varfile with variables", async () => {
const varfilePath = join(tmpDir.path, "varfile.yml")
await dumpYaml(varfilePath, {
Expand Down
34 changes: 30 additions & 4 deletions docs/reference/action-types/Build/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,17 @@ _NOTE: The default varfile format will change to YAML in Garden v0.13, since YAM

To use different varfiles in different environments, you can template in the environment name to the varfile name, e.g. `varfile: "my-action.\$\{environment.name\}.env` (this assumes that the corresponding varfiles exist).

If a listed varfile cannot be found, it is ignored.
If a listed varfile cannot be found, throwing an error.
To add optional varfiles, you can use a list item object with a `path` and an optional `optional` boolean field.
```yaml
varfiles:
- path: my-action.env
optional: true
```

| Type | Default | Required |
| ------------------ | ------- | -------- |
| `array[posixPath]` | `[]` | No |
| Type | Default | Required |
| --------------------- | ------- | -------- |
| `array[alternatives]` | `[]` | No |

Example:

Expand All @@ -173,6 +179,26 @@ varfiles:
"my-action.env"
```

### `varfiles[].path`

[varfiles](#varfiles) > path

Path to a file containing a path.

| Type | Required |
| ----------- | -------- |
| `posixPath` | Yes |

### `varfiles[].optional`

[varfiles](#varfiles) > optional

Whether the varfile is optional.

| Type | Required |
| --------- | -------- |
| `boolean` | No |

### `kind`

| Type | Allowed Values | Required |
Expand Down
34 changes: 30 additions & 4 deletions docs/reference/action-types/Build/exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,17 @@ _NOTE: The default varfile format will change to YAML in Garden v0.13, since YAM

To use different varfiles in different environments, you can template in the environment name to the varfile name, e.g. `varfile: "my-action.\$\{environment.name\}.env` (this assumes that the corresponding varfiles exist).

If a listed varfile cannot be found, it is ignored.
If a listed varfile cannot be found, throwing an error.
To add optional varfiles, you can use a list item object with a `path` and an optional `optional` boolean field.
```yaml
varfiles:
- path: my-action.env
optional: true
```

| Type | Default | Required |
| ------------------ | ------- | -------- |
| `array[posixPath]` | `[]` | No |
| Type | Default | Required |
| --------------------- | ------- | -------- |
| `array[alternatives]` | `[]` | No |

Example:

Expand All @@ -173,6 +179,26 @@ varfiles:
"my-action.env"
```

### `varfiles[].path`

[varfiles](#varfiles) > path

Path to a file containing a path.

| Type | Required |
| ----------- | -------- |
| `posixPath` | Yes |

### `varfiles[].optional`

[varfiles](#varfiles) > optional

Whether the varfile is optional.

| Type | Required |
| --------- | -------- |
| `boolean` | No |

### `kind`

| Type | Allowed Values | Required |
Expand Down
34 changes: 30 additions & 4 deletions docs/reference/action-types/Build/jib-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,17 @@ _NOTE: The default varfile format will change to YAML in Garden v0.13, since YAM

To use different varfiles in different environments, you can template in the environment name to the varfile name, e.g. `varfile: "my-action.\$\{environment.name\}.env` (this assumes that the corresponding varfiles exist).

If a listed varfile cannot be found, it is ignored.
If a listed varfile cannot be found, throwing an error.
To add optional varfiles, you can use a list item object with a `path` and an optional `optional` boolean field.
```yaml
varfiles:
- path: my-action.env
optional: true
```

| Type | Default | Required |
| ------------------ | ------- | -------- |
| `array[posixPath]` | `[]` | No |
| Type | Default | Required |
| --------------------- | ------- | -------- |
| `array[alternatives]` | `[]` | No |

Example:

Expand All @@ -185,6 +191,26 @@ varfiles:
"my-action.env"
```

### `varfiles[].path`

[varfiles](#varfiles) > path

Path to a file containing a path.

| Type | Required |
| ----------- | -------- |
| `posixPath` | Yes |

### `varfiles[].optional`

[varfiles](#varfiles) > optional

Whether the varfile is optional.

| Type | Required |
| --------- | -------- |
| `boolean` | No |

### `kind`

| Type | Allowed Values | Required |
Expand Down
Loading

0 comments on commit ee36cbb

Please sign in to comment.