#!/usr/bin/env python3 # -*- coding: utf-8 -*- from __future__ import print_function import os import re def compile(service, version, variable, alias, image, scheme, ports, sponsors, options, unimplemented, dokku_version): prefix = "\n\n".join([ header(service), description(service, image, version), ]) if len(sponsors) > 0: prefix += "\n\n" prefix += sponsors_section(service, sponsors) return ( "\n\n".join( [ prefix, requirements_section(dokku_version), installation_section(service, dokku_version), commands_section(service, variable, alias, image, scheme, ports, unimplemented), usage_section(service, variable, alias, image, scheme, ports, options, unimplemented), ] ) .replace("\n\n\n\n\n", "\n") .replace("\n\n\n\n", "\n") .replace("\n\n\n", "\n\n") ) def header(service): return " ".join( [ f"# dokku {service}", f'[![Build Status](https://img.shields.io/github/actions/workflow/status/dokku/dokku-{service}/ci.yml?branch=master&style=flat-square "Build Status")](https://github.com/dokku/dokku-{service}/actions/workflows/ci.yml?query=branch%3Amaster)', f'[![IRC Network](https://img.shields.io/badge/irc-libera-blue.svg?style=flat-square "IRC Libera")](https://webchat.libera.chat/?channels=dokku)', ] ) def description(service, full_image, version): base = "_" image = full_image if "/" in full_image: base = "r/" + full_image.split("/")[0] image = full_image.split("/")[1] return f"Official {service} plugin for dokku. Currently defaults to installing [{full_image} {version}](https://hub.docker.com/{base}/{image}/)." def sponsors_section(service, sponsors): if len(sponsors) == 0: return "" sponsor_data = ["## Sponsors", "", f"The {service} plugin was generously sponsored by the following:", ""] sponsor_data.extend([f"- [{s}](https://github.com/{s})" for s in sponsors]) return "\n".join( sponsor_data ) def requirements_section(dokku_version): return "\n".join( ["## Requirements", "", f"- dokku {dokku_version}", "- docker 1.8.x",] ) def installation_section(service, dokku_version): return "\n".join( [ "## Installation", "", "```shell", f"# on {dokku_version}", f"sudo dokku plugin:install https://github.com/dokku/dokku-{service}.git {service}", "```", ] ) def commands_section(service, variable, alias, image, scheme, ports, unimplemented): content = [ "## Commands", "", "```", ] subcommands = os.listdir("subcommands") subcommands.sort() command_list = [] descriptions = [] for filename in subcommands: if filename in unimplemented: continue data = command_data(filename, service, variable, alias, image, scheme, ports) description = data["description"] arguments = data["arguments_string"] command_list.append(f"{service}:{filename} {arguments}") descriptions.append(description) maxlen = max(map(len, command_list)) if maxlen > 50: maxlen = 50 for command, description in zip(command_list, descriptions): space_count = maxlen - len(command) content.append("{0}{1} # {2}".format(command, " " * space_count, description)) content.append("```") return "\n".join(content) def usage_section(service, variable, alias, image, scheme, ports, options, unimplemented): return "\n\n".join( [ "## Usage", f"Help for any commands can be displayed by specifying the command as an argument to {service}:help. Plugin help output in conjunction with any files in the `docs/` folder is used to generate the plugin documentation. Please consult the `{service}:help` command for any undocumented commands.", usage_intro(service, variable, alias, image, scheme, ports, options, unimplemented), usage_lifecycle(service, variable, alias, image, scheme, ports, options, unimplemented), usage_automation(service, variable, alias, image, scheme, ports, options, unimplemented), usage_data_management(service, variable, alias, image, scheme, ports, options, unimplemented), usage_backup(service, variable, alias, image, scheme, ports, options, unimplemented), usage_docker_pull(service, variable, alias, image, scheme, ports, options, unimplemented), ] ) def usage_intro(service, variable, alias, image, scheme, ports, options, unimplemented): commands = ["create", "info", "list", "logs", "link", "unlink"] content = ["### Basic Usage"] return fetch_commands_content( service, variable, alias, image, scheme, ports, options, unimplemented, commands, content ) def usage_lifecycle(service, variable, alias, image, scheme, ports, options, unimplemented): commands = [ "connect", "enter", "expose", "unexpose", "promote", "start", "stop", "pause", "restart", "upgrade", ] content = [ "### Service Lifecycle", "", "The lifecycle of each service can be managed through the following commands:", "", ] return fetch_commands_content( service, variable, alias, image, scheme, ports, options, unimplemented, commands, content ) def usage_automation(service, variable, alias, image, scheme, ports, options, unimplemented): commands = ["app-links", "clone", "exists", "linked", "links"] content = [ "### Service Automation", "", "Service scripting can be executed using the following commands:", "", ] return fetch_commands_content( service, variable, alias, image, scheme, ports, options, unimplemented, commands, content ) def usage_data_management(service, variable, alias, image, scheme, ports, options, unimplemented): commands = ["import", "export"] content = [ "### Data Management", "", "The underlying service data can be imported and exported with the following commands:", "", ] return fetch_commands_content( service, variable, alias, image, scheme, ports, options, unimplemented, commands, content ) def usage_backup(service, variable, alias, image, scheme, ports, options, unimplemented): commands = [ "backup-auth", "backup-deauth", "backup", "backup-set-encryption", "backup-unset-encryption", "backup-schedule", "backup-schedule-cat", "backup-unschedule", ] content = [ "### Backups", "", "Datastore backups are supported via AWS S3 and S3 compatible services like [minio](https://github.com/minio/minio).", "", "You may skip the `backup-auth` step if your dokku install is running within EC2 and has access to the bucket via an IAM profile. In that case, use the `--use-iam` option with the `backup` command.", "", "Backups can be performed using the backup commands:", "", ] return fetch_commands_content( service, variable, alias, image, scheme, ports, options, unimplemented, commands, content ) def usage_docker_pull(service, variable, alias, image, scheme, ports, options, unimplemented): service_prefix = service.upper() return "\n".join( [ "### Disabling `docker image pull` calls", "", f"If you wish to disable the `docker image pull` calls that the plugin triggers, you may set the `{service_prefix}_DISABLE_PULL` environment variable to `true`. Once disabled, you will need to pull the service image you wish to deploy as shown in the `stderr` output.", "", "Please ensure the proper images are in place when `docker image pull` is disabled.", ] ) def fetch_commands_content( service, variable, alias, image, scheme, ports, options, unimplemented, commands, content ): i = 0 for command in commands: output = command_help(command, service, variable, alias, image, scheme, ports, options, unimplemented) if output == "": continue content.append(output) i += 1 if i == 0: return "" return "\n".join(content) def parse_args(line): line = line.strip() arguments = [] for arg in re.findall("([A-Z_]+)", line): arg = arg.replace("_", "-").lower() if arg.endswith("optional-flag"): arg = arg.replace("-optional-flag", "") arguments.append(f"[--{arg}]") elif arg.endswith("-flag"): if arg == "info-flag": arguments.append(f"[--single-info-flag]") else: arg = arg.replace("-flag", "") first_letter = arg[0] arguments.append(f"[-{first_letter}|--{arg}]") elif arg.endswith("-flags-list"): arg = arg.replace("-list", "") arguments.append(f"[--{arg}...]") elif arg.endswith("list"): arg = arg.replace("-list", "") arguments.append(f"<{arg}...>") else: arguments.append(f"<{arg}>") return " ".join(arguments) def command_help(command, service, variable, alias, image, scheme, ports, options, unimplemented): if command in unimplemented: return "" data = command_data(command, service, variable, alias, image, scheme, ports) content = [ f"### {data['description']}", "", "```shell", "# usage", f"dokku {service}:{command} {data['arguments_string']}", "```", ] # if len(data["arguments"]) > 0: # content.append("") # content.append("arguments:") # content.append("") # for argument in data["arguments"]: # content.append(f"- {argument}") if len(data["flags"]) > 0: content.append("") content.append("flags:") content.append("") for flag in data["flags"]: if "--config-options" in flag and options != "": flag = f"{flag} (default: `{options}`)" content.append(f"- {flag}") if len(data["examples"]) > 0: content.append("") content.append(data["examples"]) doc_file = os.path.join("docs", f"{command}.md") if os.path.isfile(doc_file): content.append("") with open(doc_file) as f: content.append(f.read()) return "\n" + "\n".join(content) def command_data(command, service, variable, alias, image, scheme, ports): description = None arguments = [] arguments_string = "" example_lines = [] flags = [] with open(os.path.join("subcommands", command)) as f: for line in f.readlines(): line = line.strip() line = line.replace("$PLUGIN_SERVICE", service) line = line.replace("$PLUGIN_COMMAND_PREFIX", service) line = line.replace("${PLUGIN_COMMAND_PREFIX}", service) line = line.replace("${PLUGIN_VARIABLE}", variable) line = line.replace("${PLUGIN_DEFAULT_ALIAS}", alias) line = line.replace("${PLUGIN_IMAGE}", image) line = line.replace("${PLUGIN_SCHEME}", scheme) line = line.replace("${PLUGIN_DATASTORE_PORTS[0]}", ports[0]) line = line.replace("${PLUGIN_DATASTORE_PORTS[@]}", " ".join(ports)) if "declare desc" in line: description = re.search('"(.+)"', line).group(1) elif "$1" in line: arguments_string = parse_args(line) elif line.startswith("#A "): argument = line.replace("#A ", "") parts = [a.strip() for a in argument.split(",", 1)] arguments.append(f"`{parts[0]}`: {parts[1]}") elif line.startswith("#F "): flag = line.replace("#F ", "") parts = [a.strip() for a in flag.split(",", 1)] flags.append(f"`{parts[0]}`: {parts[1]}") elif line.startswith("#E "): example_lines.append(line.replace("#E ", "")) examples = [] sentence_lines = [] command_lines = [] codeblock_lines = [] blockquote_lines = [] for line in example_lines: if line.startswith("export") or line.startswith("dokku"): if len(blockquote_lines) > 0: examples.append("\n" + process_blockquote(blockquote_lines)) blockquote_lines = [] if len(codeblock_lines) > 0: examples.append("\n" + process_codeblock(codeblock_lines)) codeblock_lines = [] if len(sentence_lines) > 0: examples.append("\n" + process_sentence(sentence_lines)) sentence_lines = [] command_lines.append(line) elif line.startswith(" "): if len(blockquote_lines) > 0: examples.append("\n" + process_blockquote(blockquote_lines)) blockquote_lines = [] if len(command_lines) > 0: examples.append("\n" + process_command(command_lines)) command_lines = [] if len(sentence_lines) > 0: examples.append("\n" + process_sentence(sentence_lines)) sentence_lines = [] codeblock_lines.append(line.strip()) elif line.startswith(">"): if len(codeblock_lines) > 0: examples.append("\n" + process_codeblock(codeblock_lines)) codeblock_lines = [] if len(command_lines) > 0: examples.append("\n" + process_command(command_lines)) command_lines = [] if len(sentence_lines) > 0: examples.append("\n" + process_sentence(sentence_lines)) sentence_lines = [] blockquote_lines.append(line) else: if len(blockquote_lines) > 0: examples.append("\n" + process_blockquote(blockquote_lines)) blockquote_lines = [] if len(codeblock_lines) > 0: examples.append("\n" + process_codeblock(codeblock_lines)) codeblock_lines = [] if len(command_lines) > 0: examples.append("\n" + process_command(command_lines)) command_lines = [] sentence_lines.append(line) if len(blockquote_lines) > 0: examples.append("\n" + process_blockquote(blockquote_lines)) blockquote_lines = [] if len(codeblock_lines) > 0: examples.append("\n" + process_codeblock(codeblock_lines)) codeblock_lines = [] if len(command_lines) > 0: examples.append("\n" + process_command(command_lines)) command_lines = [] if len(sentence_lines) > 0: examples.append("\n" + process_sentence(sentence_lines)) sentence_lines = [] return { "description": description, "arguments_string": arguments_string, "arguments": arguments, "flags": flags, "examples": "\n".join(examples).strip(), } def process_sentence(sentence_lines): sentence_lines = " ".join(sentence_lines) sentences = ". ".join( upperfirst(i.strip()) for i in sentence_lines.split(". ") ).strip() if not sentences.endswith(".") and not sentences.endswith(":"): sentences += ":" text = [] for sentence in sentences.split(". "): parts = [] for word in sentence.strip().split(" "): if word.isupper() and len(word) > 1: for ending in [':', '.']: if word.endswith(ending): word = '`{0}`{1}'.format(word[:-1], ending) else: word = '`{0}`'.format(word) parts.append(word) text.append(" ".join(parts)) text = ". ".join(text) # some cleanup text = text.replace("(0.0.0.0)", "(`0.0.0.0`)") text = text.replace("'", "`") text = text.replace("`s", "'s") text = text.replace("``", "`") text = text.strip(" ") return text def upperfirst(x): return x[:1].upper() + x[1:] def process_blockquote(blockquote_lines): return "\n".join(blockquote_lines) def process_command(command_lines): command_lines = "\n".join(command_lines) return f"```shell\n{command_lines}\n```" def process_codeblock(codeblock_lines): codeblock_lines = "\n".join(codeblock_lines) return f"```\n{codeblock_lines}\n```" def main(): service = None version = None variable = None image = None alias = None options = None unimplemented = [] with open("Dockerfile") as f: for line in f.readlines(): if "FROM " in line: image, version = line.split(" ")[1].split(":") image = image.strip() version = version.strip() with open("config") as f: for line in f.readlines(): if "PLUGIN_COMMAND_PREFIX=" in line: service = re.search('"(.+)"', line).group(1) if "PLUGIN_DEFAULT_ALIAS=" in line: alias = re.search('"(.+)"', line).group(1) if "PLUGIN_VARIABLE=" in line: variable = re.search('"(.+)"', line).group(1) if "PLUGIN_SCHEME=" in line: scheme = re.search('"(.+)"', line).group(1) if "PLUGIN_DATASTORE_PORTS=" in line: ports = re.search("\((.+)\)", line).group(1).split(" ") if "PLUGIN_UNIMPLEMENTED_SUBCOMMANDS=" in line: match = re.search("\((.+)\)", line) if match is not None: unimplemented = [s.strip('"') for s in match.group(1).split(" ")] with open("config") as f: for line in f.readlines(): if f"{variable}_CONFIG_OPTIONS" in line: match = re.search('"(.+)"', line) if match is not None: options = match.group(1) sponsors = [] with open("plugin.toml") as f: for line in f.readlines(): if line.startswith("sponsors"): sponsors = re.search("\[([\"\w\s,_-]+)\]", line).group(1) sponsors = [s.strip("\"") for s in sponsors.split(",")] text = compile(service, version, variable, alias, image, scheme, ports, sponsors, options, unimplemented, "0.19.x+") base_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) readme_file = os.path.join(base_path, "README.md") with open(readme_file, "w") as f: f.write(text + "\n") if __name__ == "__main__": main()