diff --git a/Makefile b/Makefile index 7d327935c1..ed6053b777 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,6 @@ PWD = $(shell pwd) UID = $(shell id -u) GID = $(shell id -g) -NPM_VERSION = $(shell python -m scripts.generate_semver) PY_SOURCES = authentik tests scripts lifecycle .github DOCKER_IMAGE ?= "authentik:test" @@ -116,49 +115,21 @@ gen-diff: ## (Release) generate the changelog diff between the current schema a sed -i 's/}/}/g' diff.md npx prettier --write diff.md -gen-clean-ts: ## Remove generated API client for Typescript - rm -rf ./${GEN_API_TS}/ - rm -rf ./web/node_modules/@goauthentik/api/ +gen-client-ts: ## Build and install the authentik API for Typescript into the authentik UI Application + ./scripts/gen-client-ts.mjs -gen-clean-go: ## Remove generated API client for Go - rm -rf ./${GEN_API_GO}/ + npm i --prefix ${GEN_API_TS} -gen-clean-py: ## Remove generated API client for Python - rm -rf ./${GEN_API_PY}/ + cd ./${GEN_API_TS} && npm link + cd ./web && npm link @goauthentik/api -gen-clean: gen-clean-ts gen-clean-go gen-clean-py ## Remove generated API clients +gen-client-py: ## Build and install the authentik API for Python + ./scripts/gen-client-py.mjs -gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescript into the authentik UI Application - docker run \ - --rm -v ${PWD}:/local \ - --user ${UID}:${GID} \ - docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \ - -i /local/schema.yml \ - -g typescript-fetch \ - -o /local/${GEN_API_TS} \ - -c /local/scripts/api-ts-config.yaml \ - --additional-properties=npmVersion=${NPM_VERSION} \ - --git-repo-id authentik \ - --git-user-id goauthentik - mkdir -p web/node_modules/@goauthentik/api - cd ./${GEN_API_TS} && npm i - \cp -rf ./${GEN_API_TS}/* web/node_modules/@goauthentik/api - -gen-client-py: gen-clean-py ## Build and install the authentik API for Python - docker run \ - --rm -v ${PWD}:/local \ - --user ${UID}:${GID} \ - docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \ - -i /local/schema.yml \ - -g python \ - -o /local/${GEN_API_PY} \ - -c /local/scripts/api-py-config.yaml \ - --additional-properties=packageVersion=${NPM_VERSION} \ - --git-repo-id authentik \ - --git-user-id goauthentik pip install ./${GEN_API_PY} -gen-client-go: gen-clean-go ## Build and install the authentik API for Golang +gen-client-go: ## Build and install the authentik API for Golang + rm -rf ./${GEN_API_GO}/ mkdir -p ./${GEN_API_GO} ./${GEN_API_GO}/templates wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O ./${GEN_API_GO}/config.yaml wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O ./${GEN_API_GO}/templates/README.mustache diff --git a/scripts/gen-client-py.mjs b/scripts/gen-client-py.mjs new file mode 100755 index 0000000000..b0baeed955 --- /dev/null +++ b/scripts/gen-client-py.mjs @@ -0,0 +1,19 @@ +#!/usr/bin/env node +/** + * @file Generates the authentik API client for Python. + */ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { generateOpenAPIClient } from "./openapi-generator.mjs"; + +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); + +const repoRoot = resolve(scriptDirectory, ".."); + +generateOpenAPIClient({ + cwd: repoRoot, + outputDirectory: resolve(repoRoot, "gen-py-api"), + generatorName: "python", + config: resolve(scriptDirectory, "api-py-config.yaml"), +}); diff --git a/scripts/gen-client-ts.mjs b/scripts/gen-client-ts.mjs new file mode 100755 index 0000000000..18a698f88b --- /dev/null +++ b/scripts/gen-client-ts.mjs @@ -0,0 +1,22 @@ +#!/usr/bin/env node +/** + * @file Generates the authentik API client for TypeScript. + */ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import PackageJSON from "../package.json" with { type: "json" }; +import { generateOpenAPIClient } from "./openapi-generator.mjs"; + +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); + +const repoRoot = resolve(scriptDirectory, ".."); +const npmVersion = [PackageJSON.version, Date.now()].join("-"); + +generateOpenAPIClient({ + cwd: repoRoot, + outputDirectory: resolve(repoRoot, "gen-ts-api"), + generatorName: "typescript-fetch", + config: resolve(scriptDirectory, "api-ts-config.yaml"), + commandArgs: [`--additional-properties=npmVersion=${npmVersion}`], +}); diff --git a/scripts/generate_semver.py b/scripts/generate_semver.py deleted file mode 100755 index 7156662b2a..0000000000 --- a/scripts/generate_semver.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -""" -Generates a Semantic Versioning identifier, suffixed with a timestamp. -""" - -from time import time - -from authentik import __version__ as package_version - -""" -See: https://semver.org/#spec-item-9 (Pre-release spec) -""" -pre_release_timestamp = int(time()) - -print(f"{package_version}-{pre_release_timestamp}") diff --git a/scripts/openapi-generator.mjs b/scripts/openapi-generator.mjs new file mode 100644 index 0000000000..e23940f3f2 --- /dev/null +++ b/scripts/openapi-generator.mjs @@ -0,0 +1,100 @@ +/** + * @file OpenAPI generator utilities. + */ +import { execFileSync, execSync } from "node:child_process"; +import { existsSync, rmSync } from "node:fs"; +import { userInfo } from "node:os"; +import { join, relative, resolve } from "node:path"; + +const OPENAPI_CONTAINER_IMAGE = "docker.io/openapitools/openapi-generator-cli:v7.11.0"; + +/** + * Checks if a command exists in the PATH. + * + * @template {string} T + * @param {T} command + * @returns {T | null} + */ +function commandExists(command) { + if (execSync(`command -v ${command} || echo ''`).toString().trim()) { + return command; + } + + return null; +} + +/** + * Given a path relative to the current working directory, + * resolves it to a path relative to the local volume. + * + * @param {string} cwd + * @param {...string} pathSegments + */ +function resolveLocalPath(cwd, ...pathSegments) { + return resolve("/local", relative(cwd, join(...pathSegments))); +} + +/** + * @typedef {object} GenerateOpenAPIClientOptions + * @property {string} cwd The working directory to run the generator in. + * @property {string} outputDirectory The path to the output directory. + * @property {string} generatorName The name of the generator. + * @property {string} config The path to the generator configuration. + * @property {string} [inputSpec] The path to the OpenAPI specification. + * @property {Array} [commandArgs] Additional arguments to pass to the generator. + */ + +/** + * Generates an OpenAPI client using the `openapi-generator-cli` Docker image. + * + * @param {GenerateOpenAPIClientOptions} options + * @see {@link https://openapi-generator.tech/docs/usage} + */ +export function generateOpenAPIClient({ + cwd, + outputDirectory, + generatorName, + config, + inputSpec = resolve(cwd, "schema.yml"), + commandArgs = [], +}) { + if (existsSync(outputDirectory)) { + console.log(`Removing existing generated API client from ${outputDirectory}`); + + rmSync(outputDirectory, { recursive: true, force: true }); + } + + const containerEngine = commandExists("docker") || commandExists("podman"); + + if (!containerEngine) { + throw new Error("Container engine not found. Is Docker or Podman available in the PATH?"); + } + + const { gid, uid } = userInfo(); + + const args = [ + "run", + [`--user`, `${uid}:${gid}`], + `--rm`, + [`-v`, `${cwd}:/local`], + OPENAPI_CONTAINER_IMAGE, + "generate", + ["--input-spec", resolveLocalPath(cwd, inputSpec)], + [`--generator-name`, generatorName], + ["--config", resolveLocalPath(cwd, config)], + ["--git-repo-id", `authentik`], + ["--git-user-id", `goauthentik`], + ["--output", resolveLocalPath(cwd, outputDirectory)], + + ...commandArgs, + ]; + + console.debug(`Running command: ${containerEngine}`, args); + + execFileSync(containerEngine, args.flat(), { + cwd, + stdio: "inherit", + }); + + console.log(`Generated API client to ${outputDirectory}`); +}