feat: merge all documentation functionality, leverage __doc__ for Pulumi modules
This commit is contained in:
parent
ac51a0774a
commit
adc92bf24a
8
Makefile
8
Makefile
@ -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 .
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user