Skip to content

Commit

Permalink
chore: introduce apiVersion: garden.io/v2 (#6820)
Browse files Browse the repository at this point in the history
* docs: fix typo

* refactor: named constant for supported api versions

* refactor: extract local var + unwrap unnecessary else-block

* docs: stub for 0.14 migration guide

* chore: introduce `apiVersion: garden.io/v2`

* chore: fix lint errors

* chore: emit warning if the old multi-valued `dotIgnoreFiles` config is explicitly defined

Print a warning even if the list of files is empty.

* chore: helper function to handle deprecated feature usages

* chore: propagate `projectApiVersion` to the `PluginContext`

* chore: use new helper to report the deprecated usages of 0.13

* refactor: extract named const for default `apiVersion`

* chore: add more TODOs for 0.14

* chore: allow empty feature descriptions and hints in `reportDeprecatedFeatureUsage`

* chore: update warning message

* refactor: rename function

* docs: update Bonsai migration guide

Add note on the `cluster-init` command of the Kubernetes plugin.

* docs: update Garden 0.14 migration guide

* chore: add TODO-comment for 0.14

* chore: fix error message

* test: fix some assertions

* test: move test case to another file

A validation error is thrown from `resolveProjectConfig` if the `apiVersion` is not correct.

* chore: update warnings and docs to reflect that 0.14 has not been released yet and align hints with the reference docs

* chore: fix lint errors

* chore: use link style in the error message too

* docs: re-generate docs

* chore: highlight config field names and values in warnings and error messages

* chore: fix typo

* test: fix assertion

* chore: fix lint errors

* docs: display deprecation message in the docs

* chore: slightly adjust the look of deprecation warnings

* docs: re-generate docs

* chore: autogenerate the migration guide

* chore: generate deprecation docs

* chore: add FORCE_COLOR=true to the generate-docs script to fix CI

* chore: attempt to fix ci

* chore: fix ci

---------

Co-authored-by: Steffen Neubauer <steffen@garden.io>
  • Loading branch information
vvagaytsev and stefreak authored Feb 5, 2025
1 parent e274c0a commit cd0fbcd
Show file tree
Hide file tree
Showing 32 changed files with 400 additions and 134 deletions.
5 changes: 1 addition & 4 deletions core/src/build-staging/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,7 @@ export function cloneFile(
if (fromStats.target?.isFile()) {
// For compatibility with older versions of garden that would allow symlink targets outside the root, if the target file existed, we only emit a warning here.
// TODO(0.14): Throw an error here
emitNonRepeatableWarning(
log,
outOfBoundsMessage + `\n\nWARNING: This will become an error in an upcoming release of Garden.`
)
emitNonRepeatableWarning(log, outOfBoundsMessage + `\n\nWARNING: This will become an error in Garden 0.14.`)
// For compatibility with older versions of Garden, copy the target file instead of reproducing the symlink
// TODO(0.14): Only reproduce the symlink. The target file will be copied in another call to `cloneFile`.
fromStats = fromStats.target
Expand Down
53 changes: 29 additions & 24 deletions core/src/config/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { omit, isPlainObject } from "lodash-es"
import type { BuildDependencyConfig, ModuleConfig } from "./module.js"
import { coreModuleSpecSchema, baseModuleSchemaKeys } from "./module.js"
import { ConfigurationError, FilesystemError, isErrnoException, ParameterError } from "../exceptions.js"
import { DEFAULT_BUILD_TIMEOUT_SEC, GardenApiVersion } from "../constants.js"
import { DEFAULT_BUILD_TIMEOUT_SEC, defaultGardenApiVersion, GardenApiVersion } from "../constants.js"
import type { ProjectConfig } from "../config/project.js"
import { validateWithPath } from "./validation.js"
import { defaultDotIgnoreFile, listDirectory } from "../util/fs.js"
Expand All @@ -30,14 +30,15 @@ import { isUnresolved } from "../template/templated-strings.js"
import type { Log } from "../logger/log-entry.js"
import type { Document, DocumentOptions } from "yaml"
import { parseAllDocuments } from "yaml"
import { dedent, deline } from "../util/string.js"
import { dedent } from "../util/string.js"
import { makeDocsLinkStyled } from "../docs/common.js"
import { profileAsync } from "../util/profiling.js"
import { readFile } from "fs/promises"
import { LRUCache } from "lru-cache"
import { parseTemplateCollection } from "../template/templated-collections.js"
import { evaluate } from "../template/evaluate.js"
import { GenericContext } from "./template-contexts/base.js"
import { reportDeprecatedFeatureUsage } from "../util/deprecations.js"

export const configTemplateKind = "ConfigTemplate"
export const renderTemplateKind = "RenderTemplate"
Expand Down Expand Up @@ -370,15 +371,17 @@ function handleDotIgnoreFiles(log: Log, projectSpec: ProjectConfig) {
return projectSpec
}

reportDeprecatedFeatureUsage({
apiVersion: projectSpec.apiVersion,
log,
deprecation: "dotIgnoreFiles",
})

if (dotIgnoreFiles.length === 0) {
return { ...projectSpec, dotIgnoreFile: defaultDotIgnoreFile }
}

if (dotIgnoreFiles.length === 1) {
emitNonRepeatableWarning(
log,
deline`Multi-valued project configuration field \`dotIgnoreFiles\` is deprecated in 0.13 and will be removed in 0.14. Please use single-valued \`dotIgnoreFile\` instead.`
)
return { ...projectSpec, dotIgnoreFile: dotIgnoreFiles[0] }
}

Expand All @@ -393,10 +396,11 @@ function handleProjectModules(log: Log, projectSpec: ProjectConfig): ProjectConf
// Field 'modules' was intentionally removed from the internal interface `ProjectConfig`,
// but it still can be presented in the runtime if the old config format is used.
if (projectSpec["modules"]) {
emitNonRepeatableWarning(
reportDeprecatedFeatureUsage({
apiVersion: projectSpec.apiVersion,
log,
"Project configuration field `modules` is deprecated in 0.13 and will be removed in 0.14. Please use the `scan` field instead."
)
deprecation: "projectConfigModules",
})
let scanConfig = projectSpec.scan || {}
for (const key of ["include", "exclude"]) {
if (projectSpec["modules"][key]) {
Expand All @@ -416,35 +420,36 @@ function handleProjectModules(log: Log, projectSpec: ProjectConfig): ProjectConf
return projectSpec
}

function handleMissingApiVersion(log: Log, projectSpec: ProjectConfig): ProjectConfig {
function handleApiVersion(log: Log, projectSpec: ProjectConfig): ProjectConfig {
const projectApiVersion = projectSpec.apiVersion

// We conservatively set the apiVersion to be compatible with 0.12.
if (projectSpec["apiVersion"] === undefined) {
if (projectApiVersion === undefined) {
emitNonRepeatableWarning(
log,
`"apiVersion" is missing in the Project config. Assuming "${
GardenApiVersion.v0
defaultGardenApiVersion
}" for backwards compatibility with 0.12. The "apiVersion"-field is mandatory when using the new action Kind-configs. A detailed migration guide is available at ${makeDocsLinkStyled("guides/migrating-to-bonsai")}`
)

return { ...projectSpec, apiVersion: GardenApiVersion.v0 }
} else {
if (projectSpec["apiVersion"] === GardenApiVersion.v0) {
emitNonRepeatableWarning(
log,
`Project is configured with \`apiVersion: ${GardenApiVersion.v0}\`, running with backwards compatibility.`
)
} else if (projectSpec["apiVersion"] !== GardenApiVersion.v1) {
throw new ConfigurationError({
message: `Project configuration with \`apiVersion: ${projectSpec["apiVersion"]}\` is not supported. Valid values are ${GardenApiVersion.v1} or ${GardenApiVersion.v0}.`,
})
}
}

if (projectApiVersion === GardenApiVersion.v0) {
reportDeprecatedFeatureUsage({
apiVersion: projectApiVersion,
log,
deprecation: "apiVersionV0",
})
}

// TODO(0.14): print a warning if apiVersion: garden.io/v1 is used

return projectSpec
}

const bonsaiDeprecatedConfigHandlers: DeprecatedConfigHandler[] = [
handleMissingApiVersion,
handleApiVersion,
handleDotIgnoreFiles,
handleProjectModules,
]
Expand Down
41 changes: 25 additions & 16 deletions core/src/config/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { ConfigurationError, InternalError, ParameterError, ValidationError } fr
import { memoize } from "lodash-es"
import { providerConfigBaseSchema } from "./provider.js"
import type { GitScanMode } from "../constants.js"
import { supportedApiVersions } from "../constants.js"
import { DOCS_BASE_URL, GardenApiVersion, defaultGitScanMode, gitScanModes } from "../constants.js"
import { defaultDotIgnoreFile } from "../util/fs.js"
import type { CommandInfo } from "../plugin-context.js"
Expand All @@ -45,6 +46,7 @@ import { deepResolveContext } from "./template-contexts/base.js"
import { LazyMergePatch } from "../template/lazy-merge.js"
import { isArray, isPlainObject } from "../util/objects.js"
import { VariablesContext } from "./template-contexts/variables.js"
import { makeDeprecationMessage } from "../util/deprecations.js"

export const defaultProjectVarfilePath = "garden.env"
export const defaultEnvVarfilePath = (environmentName: string) => `garden.${environmentName}.env`
Expand Down Expand Up @@ -226,6 +228,27 @@ export interface ProjectConfig extends BaseGardenResource {
variables: DeepPrimitiveMap
}

export const projectApiVersionSchema = memoize(() =>
joi.string().valid(...supportedApiVersions).description(dedent`
The Garden apiVersion for this project.
The value ${GardenApiVersion.v0} is the default for backwards compatibility with
Garden Acorn (0.12) when not explicitly specified.
Configuring ${GardenApiVersion.v1} explicitly in your project configuration allows
you to start using the new Action configs introduced in Garden Bonsai (0.13).
Note that the value ${GardenApiVersion.v1} will break compatibility of your project
with Garden Acorn (0.12).
EXPERIMENTAL: Configuring ${GardenApiVersion.v2} explicitly in your project configuration
activates the breaking changes introduced in Garden 0.14.
The list of breaking changes is not final yet, so use this setting at your own risk.
Please refer to [the deprecations guide](${DOCS_BASE_URL}/guides/deprecations) for more information.
`)
)

export const projectNameSchema = memoize(() =>
joiIdentifier().required().description("The name of the project.").example("my-sweet-project")
)
Expand Down Expand Up @@ -302,18 +325,7 @@ export const projectSchema = createSchema({
"Configuration for a Garden project. This should be specified in the garden.yml file in your project root.",
required: true,
keys: () => ({
apiVersion: joi.string().valid(GardenApiVersion.v0, GardenApiVersion.v1).description(dedent`
The Garden apiVersion for this project.
The value ${GardenApiVersion.v0} is the default for backwards compatibility with
Garden Acorn (0.12) when not explicitly specified.
Configuring ${GardenApiVersion.v1} explicitly in your project configuration allows
you to start using the new Action configs introduced in Garden Bonsai (0.13).
Note that the value ${GardenApiVersion.v1} will break compatibility of your project
with Garden Acorn (0.12).
`),
apiVersion: projectApiVersionSchema(),
kind: joi.string().default("Project").valid("Project").description("Indicate what kind of config this is."),
path: projectRootSchema().meta({ internal: true }),
configPath: joi.string().meta({ internal: true }).description("The path to the project config file."),
Expand Down Expand Up @@ -356,13 +368,10 @@ export const projectSchema = createSchema({
.description(
deline`
Specify a filename that should be used as ".ignore" file across the project, using the same syntax and semantics as \`.gitignore\` files. By default, patterns matched in \`.gardenignore\` files, found anywhere in the project, are ignored when scanning for actions and action sources.
Note: This field has been deprecated in 0.13 in favor of the \`dotIgnoreFile\` field, and as of 0.13 only one filename is allowed here. If a single filename is specified, the conversion is done automatically. If multiple filenames are provided, an error will be thrown.
Otherwise, an error will be thrown.
`
)
.meta({
deprecated: "Please use `dotIgnoreFile` instead.",
deprecated: makeDeprecationMessage({ deprecation: "dotIgnoreFiles" }),
})
.example([".gitignore"]),
dotIgnoreFile: joi
Expand Down
11 changes: 11 additions & 0 deletions core/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ export const DEFAULT_PORT_PROTOCOL = "TCP"
export enum GardenApiVersion {
v0 = "garden.io/v0",
v1 = "garden.io/v1",
v2 = "garden.io/v2",
}

// TODO(0.14): bump this to v1 (or v2?)
// Update the comments and log messages in the placed where it's used.
export const defaultGardenApiVersion = GardenApiVersion.v0

export const supportedApiVersions: string[] = Object.values(GardenApiVersion).map((v) => v as string)

export function gardenApiSupportsActions(apiVersion: GardenApiVersion): boolean {
return apiVersion !== GardenApiVersion.v0
}

export const DEFAULT_BUILD_TIMEOUT_SEC = 600
Expand Down
1 change: 1 addition & 0 deletions core/src/docs/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export abstract class BaseKeyDescription<T = any> {
abstract description?: string
abstract example?: T
abstract deprecated: boolean
abstract deprecationMessage: string | undefined
abstract experimental: boolean
abstract required: boolean

Expand Down
5 changes: 3 additions & 2 deletions core/src/docs/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { BaseKeyDescription, NormalizeOptions } from "./common.js"
import { indent, renderMarkdownTable, convertMarkdownLinks, flattenSchema, isArrayType } from "./common.js"
import { JoiKeyDescription } from "./joi-schema.js"
import { safeDumpYaml } from "../util/serialization.js"
import stripAnsi from "strip-ansi"

export const TEMPLATES_DIR = resolve(STATIC_DIR, "docs", "templates")
const partialTemplatePath = resolve(TEMPLATES_DIR, "config-partial.hbs")
Expand Down Expand Up @@ -86,8 +87,8 @@ function makeMarkdownDescription(description: BaseKeyDescription, { showRequired

let deprecatedDescription = "This field will be removed in a future release."

if (description.deprecated && isString(description.deprecated)) {
deprecatedDescription = description.deprecated + " " + deprecatedDescription
if (description.deprecated && isString(description.deprecationMessage)) {
deprecatedDescription = stripAnsi(description.deprecationMessage)
}

return {
Expand Down
41 changes: 41 additions & 0 deletions core/src/docs/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { GardenApiVersion } from "../constants.js"
import { actionKinds } from "../actions/types.js"

import { fileURLToPath } from "node:url"
import dedent from "dedent"
import { getDeprecations } from "../util/deprecations.js"

const moduleDirName = dirname(fileURLToPath(import.meta.url))
/* eslint-disable no-console */
Expand All @@ -47,6 +49,8 @@ export async function generateDocs(targetDir: string, getPlugins: () => (GardenP
writeTemplateStringReferenceDocs(docsRoot)
console.log("Generating table of contents...")
await writeTableOfContents(docsRoot, "README.md")
console.log("Updating the deprecation guide...")
await updateDeprecationGuide(docsRoot, "guides/deprecations.md")
}

export async function writeConfigReferenceDocs(
Expand Down Expand Up @@ -199,3 +203,40 @@ export async function writeConfigReferenceDocs(
await renderConfigTemplate("config-template", renderConfigReference(configTemplateSchema()))
await renderConfigTemplate("render-template", renderConfigReference(renderTemplateConfigSchema()))
}

async function updateDeprecationGuide(docsRoot: string, deprecationGuideFilename: string) {
const guide = resolve(docsRoot, deprecationGuideFilename)
const contents = (await readFile(guide)).toString()
const humanGenerated = contents.split("## Breaking changes")[0]

// apply style for docs, using backticks instead of ansi codes
const deprecations = getDeprecations((s) => `\`${s}\``)

const contexts = new Set<string>()
for (const [_, { contextDesc }] of Object.entries(deprecations)) {
contexts.add(contextDesc)
}

const breakingChanges: string[] = []

for (const context of contexts) {
breakingChanges.push(`### ${context}`)

const matchingDeprecations = Object.entries(deprecations).filter(([_, { contextDesc }]) => contextDesc === context)
for (const [id, { hint, featureDesc }] of matchingDeprecations) {
breakingChanges.push(`#### <a id="${id}">${featureDesc}</a>`)
breakingChanges.push(hint)
}
}

await writeFile(
guide,
dedent`
${humanGenerated.trimEnd()}
## Breaking changes
${breakingChanges.join("\n\n")}
`
)
}
4 changes: 4 additions & 0 deletions core/src/docs/joi-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class JoiKeyDescription extends BaseKeyDescription {
private joiDescription: JoiDescription

override deprecated: boolean
override deprecationMessage: string | undefined
override example?: any
override experimental: boolean
override required: boolean
Expand Down Expand Up @@ -53,6 +54,9 @@ export class JoiKeyDescription extends BaseKeyDescription {
const metas: any = extend({}, ...(joiDescription.metas || []))

this.deprecated = joiDescription.parent?.deprecated || !!metas.deprecated
if (typeof metas.deprecated === "string") {
this.deprecationMessage = metas.deprecated
}
this.description = joiDescription.flags?.description
this.experimental = joiDescription.parent?.experimental || !!metas.experimental
this.internal = joiDescription.parent?.internal || !!metas.internal
Expand Down
1 change: 1 addition & 0 deletions core/src/docs/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class JsonKeyDescription<T = any> extends BaseKeyDescription<T> {
override type: string
override example?: T | undefined
override deprecated: boolean
override deprecationMessage: undefined // not used in json schema
override experimental: boolean
override required: boolean

Expand Down
7 changes: 4 additions & 3 deletions core/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
gardenEnv,
SUPPORTED_ARCHITECTURES,
GardenApiVersion,
gardenApiSupportsActions,
} from "./constants.js"
import type { Log } from "./logger/log-entry.js"
import { EventBus } from "./events/events.js"
Expand Down Expand Up @@ -308,7 +309,7 @@ export class Garden {
public readonly production: boolean
public readonly projectRoot: string
public readonly projectName: string
public readonly projectApiVersion: string
public readonly projectApiVersion: GardenApiVersion
public readonly environmentName: string
/**
* The resolved default namespace as defined in the Project config for the current environment.
Expand Down Expand Up @@ -1517,11 +1518,11 @@ export class Garden {

// Verify that the project apiVersion is defined as compatible with action kinds
// This is only available with apiVersion `garden.io/v1` or newer.
if (actionConfigs.length && this.projectApiVersion !== GardenApiVersion.v1) {
if (actionConfigs.length && !gardenApiSupportsActions(this.projectApiVersion)) {
throw new ConfigurationError({
message: `Action kinds are only supported in project configurations with "apiVersion: ${
GardenApiVersion.v1
}". A detailed migration guide is available at ${makeDocsLinkStyled("guides/migrating-to-bonsai")}`,
}" or higher. A detailed migration guide is available at ${makeDocsLinkStyled("guides/migrating-to-bonsai")}`,
})
}

Expand Down
4 changes: 4 additions & 0 deletions core/src/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import type { Garden } from "./garden.js"
import type { SourceConfig } from "./config/project.js"
import { projectApiVersionSchema } from "./config/project.js"
import { projectNameSchema, projectSourcesSchema, environmentNameSchema } from "./config/project.js"
import type { Provider, BaseProviderConfig } from "./config/provider.js"
import { providerSchema } from "./config/provider.js"
Expand All @@ -29,6 +30,7 @@ import { deepEvaluate } from "./template/evaluate.js"

export type WrappedFromGarden = Pick<
Garden,
| "projectApiVersion"
| "projectName"
| "projectRoot"
| "gardenDirPath"
Expand Down Expand Up @@ -89,6 +91,7 @@ export const pluginContextSchema = createSchema({
.default(false)
.description("Indicate if the current environment is a production environment.")
.example(true),
projectApiVersion: projectApiVersionSchema(),
projectName: projectNameSchema(),
projectId: joi.string().optional().description("The unique ID of the current project."),
projectRoot: joi.string().description("The absolute path of the project root."),
Expand Down Expand Up @@ -228,6 +231,7 @@ export async function createPluginContext({
namespace: garden.namespace,
gardenDirPath: garden.gardenDirPath,
log: garden.log,
projectApiVersion: garden.projectApiVersion,
projectName: garden.projectName,
projectRoot: garden.projectRoot,
projectSources: garden.getProjectSources(),
Expand Down
Loading

0 comments on commit cd0fbcd

Please sign in to comment.