#!/usr/bin/env node 'use strict' /* eslint-disable no-sync, no-console */ const fs = require('fs') const path = require('path') const mustache = require('mustache') const zlib = require('zlib') const { Client } = require('..') function getOperationGroup (operation) { const kubernetesAction = operation['x-kubernetes-action'] const group = { get: 'read', list: 'read', watch: 'read', watchlist: 'read', delete: 'write', deletecollection: 'write', patch: 'write', post: 'write', put: 'write', connect: 'proxy' }[kubernetesAction] return group || 'misc' } function _walk (component, kinds) { if (component.children.length) { component.children.forEach(child => { _walk(component[child], kinds) }) } if (component.template) { _walk(component(`{${component.template}}`), kinds) } const { pathItemObject = null } = component if (pathItemObject) { ['get', 'delete', 'options', 'patch', 'post', 'put'] .filter(key => key in pathItemObject) .forEach(key => { const operation = { ...component.pathItemObject[key], path: component.getPath(), method: key } if (component.pathItemObject.parameters) { operation.parameters = operation.parameters || [] operation.parameters = operation.parameters.concat(component.pathItemObject.parameters) } const { kind = null } = operation['x-kubernetes-group-version-kind'] || { kind: 'Cluster' } kinds[kind] = kinds[kind] || [] // // kubernetes-client aliases some kinds (e.g., pods -> [pods, pod, po]. Skip aliases. // if (!kinds[kind].find(existingOperation => existingOperation.operationId === operation.operationId)) { kinds[kind].push(operation) } }) } } /** * Setup data for mustache rendering/viewing. * @param {object} client - kubernetes-client object. * @returns {object} Object mapping kind (e.g., Deployment) to groups (e.g., read or write) * of OpenAPI operations. */ function setup (client) { const kinds = {} _walk(client, kinds) const kindsToGroups = Object.keys(kinds).reduce((acc, kindKey) => { acc[kindKey] = kinds[kindKey].reduce((kind, operation) => { const operationGroup = getOperationGroup(operation) kind[operationGroup] = kind[operationGroup] || [] let hasQueryParameters = false let hasPathParameters = false let hasBodyParameters = false const parameters = operation.parameters || [] parameters.forEach(parameter => { if (parameter.in === 'query') { parameter.isQueryParameter = true hasQueryParameters = true } else if (parameter.in === 'path') { parameter.isPathParameter = true hasPathParameters = true } else if (parameter.in === 'body') { parameter.isBodyParameter = true hasBodyParameters = true } }) operation.hasQueryParameters = hasQueryParameters operation.hasPathParameters = hasPathParameters operation.hasBodyParameters = hasBodyParameters kind[operationGroup].push(operation) return kind }, {}) return acc }, {}) return kindsToGroups } function kindFilePath (kind) { return `${kind}.md` } function generateClient ({ kindsToGroups, output }) { const view = { kinds: Object.keys(kindsToGroups).map(kind => { return Object.assign({ kind }, kindsToGroups[kind]) }), kindTarget: function () { if (output) return kindFilePath(this.kind) return `#${this.kind}` } } const source = mustache.render( fs.readFileSync(path.join(__dirname, 'templates/markdown-client.mustache')).toString(), view ) if (output) { const filePath = path.join(output, 'README.md') fs.writeFileSync(filePath, source) } else { console.log(source) } } function generateKind ({ kind, output }) { const view = { kindKey: kind.kindKey, groups: Object.keys(kind.groups).map(group => ({ groupKey: group, operations: kind.groups[group] })), /** * Replace newline characters in rendered text with
s. * @returns {function} function */ markdownBreaks: function () { return function (text, render) { return render(text) .replace(/\r\n/g, '
') .replace(/\n/g, '
') } }, /** * When in a method section, return the full kubernetes-client name. * @returns {function} function */ jsName: function () { return function () { const leadingAndTrailingSlashes = /(^\/)|(\/$)/g const jsName = this.path .replace(leadingAndTrailingSlashes, '') .replace(/\/{/g, '(') // replace /{ with ( .replace(/}\//g, ').') // replace }/ with ). .replace(/}/g, ')') // replace } with ) .replace(/\//g, '.') // replace / with . return `${jsName}.${this.method.toLowerCase()}` } } } const partials = { group: fs.readFileSync(path.join(__dirname, 'templates/markdown-group.mustache')).toString(), operation: fs.readFileSync(path.join(__dirname, 'templates/markdown-operation.mustache')).toString() } const source = mustache.render( fs.readFileSync(path.join(__dirname, 'templates/markdown-kind.mustache')).toString(), view, partials ) if (output) { const filePath = path.join(output, kindFilePath(kind.kindKey)) fs.writeFileSync(filePath, source) } else { console.log(source) } } function generate (input, output) { let raw = fs.readFileSync(input) if (input.endsWith('.gz')) { raw = zlib.gunzipSync(raw) } const spec = JSON.parse(raw) const client = new Client({ spec, backend: {} }) const kindsToGroups = setup(client) try { fs.mkdirSync(output) } catch (err) { if (err.code !== 'EEXIST') throw err } generateClient({ kindsToGroups, output }) Object.entries(kindsToGroups).forEach(([kindKey, groups]) => { generateKind({ kind: { kindKey, groups }, output }) }) } function main (args) { if (args.builtins) { const specs = './lib/specs' fs.readdirSync(specs).forEach(filename => { const versionRegExp = /swagger-(.+)\.json.gz/ const match = filename.match(versionRegExp) if (!match) { console.log(`Skipping ${filename}`) return } const version = match[1] const output = `./docs/${version}` generate(path.join(specs, filename), output) }) } if (args.spec) { generate(args.spec, args.output) } } const argv = require('yargs') .usage('Usage: $0 [options]') .option('spec', { alias: 's', describe: 'Swagger / OpenAPI specification' }) .option('output', { alias: 'o', describe: 'Markdown output file' }) .option('builtins', { describe: 'Generate Markdown for builtin specifications' }) .strict() .help() .argv main(argv)