Some commands - such as link or upgrade - have extra documenation on a per-plugin basis. Rather than make some sort of weird templating logic in the help output generation, that documentation is added directly to the repository and then injected at generation time.
539 lines
18 KiB
Python
Executable File
539 lines
18 KiB
Python
Executable File
#!/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'[](https://github.com/dokku/dokku-{service}/actions/workflows/ci.yml?query=branch%3Amaster)',
|
|
f'[](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",
|
|
"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 pull` calls",
|
|
"",
|
|
f"If you wish to disable the `docker 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 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()
|