feat: merge all documentation functionality, leverage __doc__ for Pulumi modules

This commit is contained in:
Stefan Reimer 2022-06-30 17:42:49 +02:00
parent ac51a0774a
commit adc92bf24a
3 changed files with 121 additions and 105 deletions

View File

@ -26,11 +26,11 @@ clean:
pybuild:
hatchling build
test_upload: $(PACKAGE_FILE)
twine upload --repository-url https://test.pypi.org/legacy/ dist/cloudbender-*.whl
test_upload: pybuild
twine upload --repository-url https://test.pypi.org/legacy/ --non-interactive dist/cloudbender-*.whl
upload: $(PACKAGE_FILE)
twine upload --repository-url https://upload.pypi.org/legacy/ dist/cloudbender-*.whl
upload: pybuild
twine upload -r pypi --non-interactive dist/cloudbender-*.whl
build:
podman build --rm -t $(REPOSITORY):$(TAG) -t $(REPOSITORY):latest .

View File

@ -145,12 +145,12 @@ def outputs(cb, stack_names, multi, include, values):
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
@click.option("--graph", is_flag=True, help="Create Dot Graph file")
@click.pass_obj
def create_docs(cb, stack_names, multi, graph):
"""Parses all documentation fragments out of rendered templates creating docs/*.md file"""
def docs(cb, stack_names, multi, graph):
"""Outputs docs for stack(s). For Pulumi stacks prints out docstring. For CloudFormation templates render a markdown file. Same idea as helm-docs."""
stacks = _find_stacks(cb, stack_names, multi)
for s in stacks:
s.create_docs(graph=graph)
s.docs(graph=graph)
@click.command()
@ -183,19 +183,14 @@ def refresh(cb, stack_name):
@click.argument("stack_name")
@click.argument("function", default="")
@click.argument("args", nargs=-1)
@click.option(
"--listall",
is_flag=True,
help="List all available execute functions for this stack",
)
@click.pass_obj
def execute(cb, stack_name, function, args, listall=False):
def execute(cb, stack_name, function, args):
"""Executes custom Python function within an existing stack context"""
stacks = _find_stacks(cb, [stack_name])
for s in stacks:
if s.mode == "pulumi":
s.execute(function, args, listall)
s.execute(function, args)
else:
logger.info(
"{} uses Cloudformation, no exec feature available.".format(s.stackname)
@ -455,7 +450,7 @@ cli.add_command(delete)
cli.add_command(clean)
cli.add_command(create_change_set)
cli.add_command(outputs)
cli.add_command(create_docs)
cli.add_command(docs)
cli.add_command(refresh)
cli.add_command(preview)
cli.add_command(set_config)

View File

@ -574,84 +574,111 @@ class Stack(object):
)
)
def create_docs(self, template=False, graph=False):
@pulumi_ws
def docs(self, template=False, graph=False):
"""Read rendered template, parse documentation fragments, eg. parameter description
and create a mardown doc file for the stack
same idea as eg. helm-docs for values.yaml
and create a mardown doc file for the stack. Same idea as helm-docs for the values.yaml
"""
try:
self.read_template_file()
except FileNotFoundError:
return
if self.mode == "pulumi":
if vars(self._pulumi_code)["__doc__"]:
print(vars(self._pulumi_code)["__doc__"])
else:
print("No template documentation found.")
# collect all __doc__ from available _execute_ functions
_help = ""
for k in vars(self._pulumi_code).keys():
if k.startswith("_execute_"):
docstring = vars(self._pulumi_code)[k].__doc__
_help = _help + "## {}\n{}\n".format(
k.lstrip("_execute_"), docstring
)
if _help:
print(f"# Available `execute` functions: \n\n{_help}")
if not template:
doc_template = importlib.resources.read_text(templates, "stack-doc.md")
jenv = JinjaEnv()
template = jenv.from_string(doc_template)
data = {}
else:
doc_template = template
data["name"] = self.stackname
data["description"] = self.cfn_data["Description"]
data["dependencies"] = self.dependencies
if "Parameters" in self.cfn_data:
data["parameters"] = self.cfn_data["Parameters"]
set_parameters = self.resolve_parameters()
for p in set_parameters:
data["parameters"][p]["value"] = set_parameters[p]
if "Outputs" in self.cfn_data:
data["outputs"] = self.cfn_data["Outputs"]
# Check for existing outputs yaml, if found add current value column and set header to timestamp from outputs file
output_file = os.path.join(
self.ctx["outputs_path"], self.rel_path, self.stackname + ".yaml"
)
try:
with open(output_file, "r") as yaml_contents:
outputs = yaml.safe_load(yaml_contents.read())
for p in outputs["Outputs"]:
data["outputs"][p]["last_value"] = outputs["Outputs"][p]
data["timestamp"] = outputs["TimeStamp"]
except (FileNotFoundError, KeyError, TypeError):
pass
self.read_template_file()
except FileNotFoundError:
return
doc_file = os.path.join(
self.ctx["docs_path"], self.rel_path, self.stackname + ".md"
)
ensure_dir(os.path.join(self.ctx["docs_path"], self.rel_path))
if not template:
doc_template = importlib.resources.read_text(templates, "stack-doc.md")
jenv = JinjaEnv()
template = jenv.from_string(doc_template)
data = {}
else:
doc_template = template
with open(doc_file, "w") as doc_contents:
doc_contents.write(template.render(**data))
logger.info("Wrote documentation for %s to %s", self.stackname, doc_file)
data["name"] = self.stackname
data["description"] = self.cfn_data["Description"]
data["dependencies"] = self.dependencies
# Write Graph in Dot format
if graph:
filename = os.path.join(
self.ctx["template_path"], self.rel_path, self.stackname + ".yaml"
)
if "Parameters" in self.cfn_data:
data["parameters"] = self.cfn_data["Parameters"]
set_parameters = self.resolve_parameters()
for p in set_parameters:
data["parameters"][p]["value"] = set_parameters[p]
lint_args = ["--template", filename]
(args, filenames, formatter) = cfnlint.core.get_args_filenames(lint_args)
(template, rules, matches) = cfnlint.core.get_template_rules(filename, args)
template_obj = cfnlint.template.Template(filename, template, [self.region])
if "Outputs" in self.cfn_data:
data["outputs"] = self.cfn_data["Outputs"]
path = os.path.join(
self.ctx["docs_path"], self.rel_path, self.stackname + ".dot"
)
g = cfnlint.graph.Graph(template_obj)
try:
g.to_dot(path)
logger.info("DOT representation of the graph written to %s", path)
except ImportError:
logger.error(
"Could not write the graph in DOT format. Please install either `pygraphviz` or `pydot` modules."
# Check for existing outputs yaml, if found add current value column and set header to timestamp from outputs file
output_file = os.path.join(
self.ctx["outputs_path"], self.rel_path, self.stackname + ".yaml"
)
try:
with open(output_file, "r") as yaml_contents:
outputs = yaml.safe_load(yaml_contents.read())
for p in outputs["Outputs"]:
data["outputs"][p]["last_value"] = outputs["Outputs"][p]
data["timestamp"] = outputs["TimeStamp"]
except (FileNotFoundError, KeyError, TypeError):
pass
doc_file = os.path.join(
self.ctx["docs_path"], self.rel_path, self.stackname + ".md"
)
ensure_dir(os.path.join(self.ctx["docs_path"], self.rel_path))
with open(doc_file, "w") as doc_contents:
doc_contents.write(template.render(**data))
logger.info(
"Wrote documentation for %s to %s", self.stackname, doc_file
)
# Write Graph in Dot format
if graph:
filename = os.path.join(
self.ctx["template_path"], self.rel_path, self.stackname + ".yaml"
)
lint_args = ["--template", filename]
(args, filenames, formatter) = cfnlint.core.get_args_filenames(
lint_args
)
(template, rules, matches) = cfnlint.core.get_template_rules(
filename, args
)
template_obj = cfnlint.template.Template(
filename, template, [self.region]
)
path = os.path.join(
self.ctx["docs_path"], self.rel_path, self.stackname + ".dot"
)
g = cfnlint.graph.Graph(template_obj)
try:
g.to_dot(path)
logger.info("DOT representation of the graph written to %s", path)
except ImportError:
logger.error(
"Could not write the graph in DOT format. Please install either `pygraphviz` or `pydot` modules."
)
def resolve_parameters(self):
"""Renders parameters for the stack based on the source template and the environment configuration"""
@ -867,35 +894,29 @@ class Stack(object):
return
@pulumi_ws
def execute(self, function, args, listall=False):
"""Executes custom Python function within a Pulumi stack"""
def execute(self, function, args):
"""
Executes custom Python function within a Pulumi stack
# call all available functions and output built in help
if listall:
for k in vars(self._pulumi_code).keys():
if k.startswith("_execute_"):
docstring = vars(self._pulumi_code)[k](docstring=True)
print("{}: {}".format(k.lstrip("_execute_"), docstring))
These functions are executed within the stack environment and are provided with all stack input parameters as well as current outputs.
Think of "docker exec" into an existing container...
"""
if not function:
logger.error("No function specified !")
return
exec_function = f"_execute_{function}"
if exec_function in vars(self._pulumi_code):
pulumi_stack = self._get_pulumi_stack()
vars(self._pulumi_code)[exec_function](
config=pulumi_stack.get_all_config(),
outputs=pulumi_stack.outputs(),
args=args,
)
else:
if not function:
logger.error("No function specified !")
return
exec_function = f"_execute_{function}"
if exec_function in vars(self._pulumi_code):
pulumi_stack = self._get_pulumi_stack()
vars(self._pulumi_code)[exec_function](
config=pulumi_stack.get_all_config(),
outputs=pulumi_stack.outputs(),
args=args,
)
else:
logger.error(
"{} is not defined in {}".format(function, self._pulumi_code)
)
logger.error("{} is not defined in {}".format(function, self._pulumi_code))
@pulumi_ws
def assimilate(self):