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: pybuild:
hatchling build hatchling build
test_upload: $(PACKAGE_FILE) test_upload: pybuild
twine upload --repository-url https://test.pypi.org/legacy/ dist/cloudbender-*.whl twine upload --repository-url https://test.pypi.org/legacy/ --non-interactive dist/cloudbender-*.whl
upload: $(PACKAGE_FILE) upload: pybuild
twine upload --repository-url https://upload.pypi.org/legacy/ dist/cloudbender-*.whl twine upload -r pypi --non-interactive dist/cloudbender-*.whl
build: build:
podman build --rm -t $(REPOSITORY):$(TAG) -t $(REPOSITORY):latest . 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("--multi", is_flag=True, help="Allow more than one stack to match")
@click.option("--graph", is_flag=True, help="Create Dot Graph file") @click.option("--graph", is_flag=True, help="Create Dot Graph file")
@click.pass_obj @click.pass_obj
def create_docs(cb, stack_names, multi, graph): def docs(cb, stack_names, multi, graph):
"""Parses all documentation fragments out of rendered templates creating docs/*.md file""" """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) stacks = _find_stacks(cb, stack_names, multi)
for s in stacks: for s in stacks:
s.create_docs(graph=graph) s.docs(graph=graph)
@click.command() @click.command()
@ -183,19 +183,14 @@ def refresh(cb, stack_name):
@click.argument("stack_name") @click.argument("stack_name")
@click.argument("function", default="") @click.argument("function", default="")
@click.argument("args", nargs=-1) @click.argument("args", nargs=-1)
@click.option(
"--listall",
is_flag=True,
help="List all available execute functions for this stack",
)
@click.pass_obj @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""" """Executes custom Python function within an existing stack context"""
stacks = _find_stacks(cb, [stack_name]) stacks = _find_stacks(cb, [stack_name])
for s in stacks: for s in stacks:
if s.mode == "pulumi": if s.mode == "pulumi":
s.execute(function, args, listall) s.execute(function, args)
else: else:
logger.info( logger.info(
"{} uses Cloudformation, no exec feature available.".format(s.stackname) "{} uses Cloudformation, no exec feature available.".format(s.stackname)
@ -455,7 +450,7 @@ cli.add_command(delete)
cli.add_command(clean) cli.add_command(clean)
cli.add_command(create_change_set) cli.add_command(create_change_set)
cli.add_command(outputs) cli.add_command(outputs)
cli.add_command(create_docs) cli.add_command(docs)
cli.add_command(refresh) cli.add_command(refresh)
cli.add_command(preview) cli.add_command(preview)
cli.add_command(set_config) 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 """Read rendered template, parse documentation fragments, eg. parameter description
and create a mardown doc file for the stack and create a mardown doc file for the stack. Same idea as helm-docs for the values.yaml
same idea as eg. helm-docs for values.yaml
""" """
try: if self.mode == "pulumi":
self.read_template_file() if vars(self._pulumi_code)["__doc__"]:
except FileNotFoundError: print(vars(self._pulumi_code)["__doc__"])
return 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: 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: try:
with open(output_file, "r") as yaml_contents: self.read_template_file()
outputs = yaml.safe_load(yaml_contents.read()) except FileNotFoundError:
for p in outputs["Outputs"]: return
data["outputs"][p]["last_value"] = outputs["Outputs"][p]
data["timestamp"] = outputs["TimeStamp"]
except (FileNotFoundError, KeyError, TypeError):
pass
doc_file = os.path.join( if not template:
self.ctx["docs_path"], self.rel_path, self.stackname + ".md" doc_template = importlib.resources.read_text(templates, "stack-doc.md")
) jenv = JinjaEnv()
ensure_dir(os.path.join(self.ctx["docs_path"], self.rel_path)) template = jenv.from_string(doc_template)
data = {}
else:
doc_template = template
with open(doc_file, "w") as doc_contents: data["name"] = self.stackname
doc_contents.write(template.render(**data)) data["description"] = self.cfn_data["Description"]
logger.info("Wrote documentation for %s to %s", self.stackname, doc_file) data["dependencies"] = self.dependencies
# Write Graph in Dot format if "Parameters" in self.cfn_data:
if graph: data["parameters"] = self.cfn_data["Parameters"]
filename = os.path.join( set_parameters = self.resolve_parameters()
self.ctx["template_path"], self.rel_path, self.stackname + ".yaml" for p in set_parameters:
) data["parameters"][p]["value"] = set_parameters[p]
lint_args = ["--template", filename] if "Outputs" in self.cfn_data:
(args, filenames, formatter) = cfnlint.core.get_args_filenames(lint_args) data["outputs"] = self.cfn_data["Outputs"]
(template, rules, matches) = cfnlint.core.get_template_rules(filename, args)
template_obj = cfnlint.template.Template(filename, template, [self.region])
path = os.path.join( # Check for existing outputs yaml, if found add current value column and set header to timestamp from outputs file
self.ctx["docs_path"], self.rel_path, self.stackname + ".dot" output_file = os.path.join(
) self.ctx["outputs_path"], self.rel_path, self.stackname + ".yaml"
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."
) )
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): def resolve_parameters(self):
"""Renders parameters for the stack based on the source template and the environment configuration""" """Renders parameters for the stack based on the source template and the environment configuration"""
@ -867,35 +894,29 @@ class Stack(object):
return return
@pulumi_ws @pulumi_ws
def execute(self, function, args, listall=False): def execute(self, function, args):
"""Executes custom Python function within a Pulumi stack""" """
Executes custom Python function within a Pulumi stack
# call all available functions and output built in help These functions are executed within the stack environment and are provided with all stack input parameters as well as current outputs.
if listall: Think of "docker exec" into an existing container...
for k in vars(self._pulumi_code).keys():
if k.startswith("_execute_"): """
docstring = vars(self._pulumi_code)[k](docstring=True) if not function:
print("{}: {}".format(k.lstrip("_execute_"), docstring)) logger.error("No function specified !")
return 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: else:
if not function: logger.error("{} is not defined in {}".format(function, self._pulumi_code))
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)
)
@pulumi_ws @pulumi_ws
def assimilate(self): def assimilate(self):