diff --git a/docs/examples/artifact2.py b/docs/examples/artifact2.py new file mode 100644 index 00000000..b0c9aa82 --- /dev/null +++ b/docs/examples/artifact2.py @@ -0,0 +1,22 @@ +import unfurl +import tosca +from tosca import ToscaOutputs, Attribute, Eval +class MyArtifact(unfurl.artifacts.ShellExecutable): + file: str = "myscript.sh" + contrived_key: str + outputsTemplate = Eval("{{ stdout | from_json | subelements(SELF.contrived_key)}}") + + class Outputs(ToscaOutputs): + a_output: str = Attribute() + + def execute(self, arg1: str, arg2: int) -> Outputs: + return MyArtifact.Outputs() + +class MyNode(tosca.nodes.Root): + prop1: str + prop2: int + + def configure(self) -> MyArtifact.Outputs: + return MyArtifact(contrived_key=self.prop1).execute("hello", arg2=self.prop2) + +my_node = MyNode(prop1="foo", prop2=1) diff --git a/docs/examples/artifact2.yaml b/docs/examples/artifact2.yaml new file mode 100644 index 00000000..fcd13cf1 --- /dev/null +++ b/docs/examples/artifact2.yaml @@ -0,0 +1,67 @@ +artifact_types: + MyArtifact: + derived_from: unfurl.artifacts.ShellExecutable + properties: + file: + type: string + default: myscript.sh + contrived_key: + type: string + outputsTemplate: + type: string + default: '{{ stdout | from_json | subelements(SELF.contrived_key)}}' + interfaces: + Executable: + type: unfurl.interfaces.Executable + operations: + execute: + metadata: + output_key: + - Outputs + inputs: + arg1: + type: string + arg2: + type: integer + outputs: + a_output: + type: string +node_types: + MyNode: + derived_from: tosca.nodes.Root + properties: + prop1: + type: string + prop2: + type: integer + interfaces: + Standard: + operations: + configure: + metadata: + output_key: + - Outputs + arguments: + - arg1 + - arg2 + inputs: + arg1: hello + arg2: + eval: .::prop2 + outputs: + a_output: + type: string + implementation: + primary: + type: MyArtifact + properties: + contrived_key: + eval: .parent::.::prop1 + file: myscript.sh +topology_template: + node_templates: + my_node: + type: MyNode + properties: + prop1: foo + prop2: 1 diff --git a/tests/examples/artifact1.py b/tests/examples/artifact1.py new file mode 100644 index 00000000..f8522ca7 --- /dev/null +++ b/tests/examples/artifact1.py @@ -0,0 +1,50 @@ +# Generated by tosca.yaml2python from docs/examples/artifact1.yaml at 2025-01-14T17:23:28 overwrite not modified (change to "overwrite ok" to allow) + +import unfurl +from typing import List, Dict, Any, Tuple, Union, Sequence +import tosca +from tosca import ArtifactEntity, Eval, Node, operation + + + +@operation(name="execute") +def _terraform_execute(self, bar=1): + ... +# terraform_execute = operation(name="execute") + +def _make_terraform() -> unfurl.artifacts.TerraformModule: + terraform = unfurl.artifacts.TerraformModule( + "terraform", + resultTemplate=Eval( + { + "attributes": { + "output_attribute": "{{ '.name' | eval }} node " + "{{arguments.foo}}:{{arguments.bar}}" + } + } + ), + file="missing.tf", + contents='resource "null_resource" "null" {\n}\n', + ) + # manual: + terraform.set_operation(_terraform_execute) + terraform._Executable_default_inputs = dict(foo="hello") # type: ignore[attr-defined] + return terraform + + +configurator_artifacts: Node = unfurl.nodes.LocalRepository( + "configurator-artifacts", +) +configurator_artifacts.terraform = _make_terraform() # type: ignore[attr-defined] + + +@operation(name="configure") +def test_configure(self, **kw): + return self.find_artifact("terraform").execute() + + +test: Node = tosca.nodes.Root( + "test", +) +test.set_operation(test_configure, "configure") + diff --git a/tests/examples/artifact1.yaml b/tests/examples/artifact1.yaml new file mode 100644 index 00000000..9d86bc50 --- /dev/null +++ b/tests/examples/artifact1.yaml @@ -0,0 +1,41 @@ +kind: Ensemble +apiVersion: unfurl/v1alpha1 +spec: + service_template: + tosca_definitions_version: tosca_simple_unfurl_1_0_0 + topology_template: + node_templates: + configurator-artifacts: + type: unfurl.nodes.LocalRepository + artifacts: + terraform: + type: unfurl.artifacts.TerraformModule + properties: + resultTemplate: + attributes: + output_attribute: "{{ '.name' | eval }} node {{arguments.foo}}:{{arguments.bar}}" + interfaces: + Executable: + type: unfurl.interfaces.Executable + inputs: + foo: hello + operations: + execute: + inputs: + bar: + type: integer + default: 1 + file: missing.tf + contents: "resource \"null_resource\" \"null\" {\n}\n" + metadata: + module: service_template + test: + type: tosca.nodes.Root + interfaces: + Standard: + operations: + configure: + implementation: + primary: terraform + metadata: + module: service_template \ No newline at end of file diff --git a/tests/examples/dsl_artifacts.py b/tests/examples/dsl_artifacts.py new file mode 100644 index 00000000..e7fc3545 --- /dev/null +++ b/tests/examples/dsl_artifacts.py @@ -0,0 +1,75 @@ +from abc import abstractmethod +from typing import Dict +import unfurl +import tosca +from tosca import ToscaInputs, ToscaOutputs, Attribute, placeholder + +class KubernetesClusterInputs(ToscaInputs): + do_region: str = "nyc3" + doks_k8s_version: str ="1.30" + +class KubernetesClusterOutputs(ToscaOutputs): + do_id: str = Attribute() # needed so can be KubernetesClusterOutputs inherited + +class ClusterOp(unfurl.artifacts.Executable): + file: str = "kubernetes" + + # inputs defined here are set as the inputs for operations that set this artifact as their implementation + # args, retval set input, output definitions + def execute(self, inputs: KubernetesClusterInputs) -> KubernetesClusterOutputs: + # If an artifact of this type is use an operation's implementation + # the inputs defined here will be set as the inputs definition for the operation + return placeholder(KubernetesClusterOutputs) + +class MoreInputs(ToscaInputs): + nodes: int = 4 + + +class CustomClusterOp(ClusterOp): + # customizing an artifact should make sure execute is compatible with base artifact's artifact + # this means you can't change existing parameters, only add new ones with default values + + # only add new inputs definitions to this types interface, since the base types inputs will be merged with these + def execute(self, inputs: KubernetesClusterInputs, more_inputs: MoreInputs = MoreInputs()) -> KubernetesClusterOutputs: + self.set_inputs(inputs, more_inputs) + return placeholder(KubernetesClusterOutputs) + +class ClusterTerraform(unfurl.artifacts.TerraformModule, ClusterOp): + # need to merge properties with inputs for configurator + file: str = "kubernetes" + + # def execute(self, inputs: KubernetesClusterInputs) -> KubernetesClusterOutputs: + # ToscaInputs._get_inputs(self) # type: ignore + # return None # type: ignore + # # print("Cluster.execute!!", inputs) + # # return configurator(self.className)(self).execute(inputs) + +class DOCluster(tosca.nodes.Root, KubernetesClusterInputs, KubernetesClusterOutputs): + clusterconfig: "ClusterOp" = ClusterTerraform() + + my_property: str = "default" + + def configure(self, **kw) -> KubernetesClusterOutputs: + return self.clusterconfig.execute(self) + + +class ExtraClusterOp(ClusterOp): + # properties are merged with configurator inputs at runtime + extra: str = tosca.Property(options=tosca.InputOption) + + def execute(self, inputs: KubernetesClusterInputs, extra: str = tosca.CONSTRAINED) -> KubernetesClusterOutputs: + # self.set_inputs(inputs, extra=self.extra) + return KubernetesClusterOutputs() + +class CustomClusterTerraform(unfurl.artifacts.TerraformModule, ExtraClusterOp): + file: str = "my_custom_kubernetes_tf_module" + +mycluster = DOCluster(clusterconfig=CustomClusterTerraform(extra="extra", + contents = """resource "null_resource" "null" {} + output "do_id" { + value = "ABC" + } + """)) + + + diff --git a/tests/examples/dsl_configurator.py b/tests/examples/dsl_configurator.py index 95fe0af3..120f123f 100644 --- a/tests/examples/dsl_configurator.py +++ b/tests/examples/dsl_configurator.py @@ -45,7 +45,7 @@ def create(self, **kw: Any) -> Callable[[Any], Any]: @operation(outputs=dict(test_output="computed")) def delete(self, **kw: Any) -> TemplateConfigurator: - render = self._context # type: ignore + render = self._context # type: ignore # raise error to force render done = DoneDict(outputs=dict(test_output=Eval("{{'set output'}}"))) return TemplateConfigurator(TemplateInputs(run="test me", done=done)) diff --git a/tests/examples/dsl_relationships.py b/tests/examples/dsl_relationships.py index 38cc9335..cf263eba 100644 --- a/tests/examples/dsl_relationships.py +++ b/tests/examples/dsl_relationships.py @@ -18,7 +18,7 @@ class Volume(tosca.nodes.Root): disk_label: str disk_size: tosca.Size = 100 * tosca.GB -class VolumeAttachment(tosca.relationships.AttachesTo): +class VolumeAttachment(tosca.relationships.AttachesTo): # type: ignore[override] _target: Volume class VolumeMountArtifact(tosca.artifacts.Root): diff --git a/tests/test_artifacts.py b/tests/test_artifacts.py index 10a99ec6..63030ae8 100644 --- a/tests/test_artifacts.py +++ b/tests/test_artifacts.py @@ -4,8 +4,10 @@ from click.testing import CliRunner, Result from .utils import DEFAULT_STEPS, isolated_lifecycle, lifecycle -from unfurl.yamlmanifest import YamlManifest -from unfurl.job import Runner, run_job +from unfurl.yamlmanifest import YamlManifest, save_task +from unfurl.job import Runner, run_job, JobOptions +from tosca.python2yaml import PythonToYaml, python_src_to_yaml_obj +from unfurl.yamlloader import ImportResolver, load_yaml, yaml ensemble = """ apiVersion: unfurl/v1alpha1 @@ -32,15 +34,19 @@ def test_localhost(): localhost = runner.manifest.get_root_resource().find_resource("localhost") assert localhost if localhost.attributes["os_type"] == "Darwin": - assert ( - localhost.attributes["package_manager"] == "homebrew" - ), localhost.attributes + assert localhost.attributes["package_manager"] == "homebrew", ( + localhost.attributes + ) else: # assume running on a linus - localhost.attributes["package_manager"] in [ - "apt", - "yum", - "rpm", - ], localhost.attributes["package_manager"] + ( + localhost.attributes["package_manager"] + in [ + "apt", + "yum", + "rpm", + ], + localhost.attributes["package_manager"], + ) assert localhost.attributes["architecture"] assert localhost.attributes["architecture"] == localhost.query( ".capabilities::architecture" @@ -82,7 +88,9 @@ def test_lifecycle(): def test_lifecycle_no_home(): - env = dict(UNFURL_HOME="") # no home (not needed, this is the default for isolated_lifecycle) + env = dict( + UNFURL_HOME="" + ) # no home (not needed, this is the default for isolated_lifecycle) src_path = str(Path(__file__).parent / "examples" / "build_artifact-ensemble.yaml") for job in isolated_lifecycle(src_path, env=env): # verify that the build artifact got installed as part of an job request @@ -118,7 +126,10 @@ def test_lifecycle_no_home(): def test_target_and_intent(): src_path = str(Path(__file__).parent / "examples" / "deploy_artifact.yaml") job = run_job(src_path, dict(skip_save=True)) - assert job.get_outputs()["outputVar"].strip() == "Artifact: deploy_artifact intent deploy contents of deploy_artifact parent: configuration" + assert ( + job.get_outputs()["outputVar"].strip() + == "Artifact: deploy_artifact intent deploy contents of deploy_artifact parent: configuration" + ) collection_ensemble = """ @@ -154,11 +165,10 @@ def test_target_and_intent(): changes: [] """ + def test_collection_artifact(): cli_runner = CliRunner() - with cli_runner.isolated_filesystem( - os.getenv("UNFURL_TEST_TMPDIR") - ) as tmp_path: + with cli_runner.isolated_filesystem(os.getenv("UNFURL_TEST_TMPDIR")) as tmp_path: os.environ["ANSIBLE_COLLECTIONS_PATH"] = tmp_path runner = Runner(YamlManifest(collection_ensemble)) run1 = runner.run() @@ -180,3 +190,67 @@ def test_collection_artifact(): if sys.version_info[1] > 8: # mdellweg.filter.repr result assert summary["tasks"][0]["output"]["run"] == ["'test'"], summary + + +def _to_yaml(python_src: str): + namespace: dict = {} + tosca_tpl = python_src_to_yaml_obj(python_src, namespace) + # yaml.dump(tosca_tpl, sys.stdout) + return tosca_tpl + + +def _get_python_manifest(pyfile, yamlfile=None): + basepath = os.path.join(os.path.dirname(__file__), "examples/") + with open(os.path.join(basepath, pyfile)) as f: + python_src = f.read() + py_tpl = _to_yaml(python_src) + manifest_tpl = dict( + apiVersion="unfurl/v1alpha1", + kind="Ensemble", + spec=dict(service_template=py_tpl), + ) + yaml.dump(manifest_tpl, sys.stdout) + if yamlfile: + with open(os.path.join(basepath, yamlfile)) as f: + yaml_src = f.read() + yaml_tpl = yaml.load(yaml_src) + assert yaml_tpl == manifest_tpl + + return manifest_tpl + + +def test_artifact_dsl(): + manifest_tpl = _get_python_manifest("dsl_artifacts.py") + manifest = YamlManifest(manifest_tpl) + job = Runner(manifest) + job = job.run(JobOptions(skip_save=True)) + assert job + assert len(job.workDone) == 1, len(job.workDone) + task = list(job.workDone.values())[0] + assert task._arguments() == { + "do_region": "nyc3", + "doks_k8s_version": "1.30", + "extra": "extra", + } + assert task.configSpec.outputs + mycluster = manifest.rootResource.find_instance("mycluster") + assert mycluster.attributes["do_id"] == "ABC" + assert mycluster.artifacts["clusterconfig"].file == "my_custom_kubernetes_tf_module" + + +def test_artifact_syntax(): + manifest_tpl = _get_python_manifest("artifact1.py", "artifact1.yaml") + manifest = YamlManifest(manifest_tpl) + job = Runner(manifest) + job = job.run(JobOptions(skip_save=True)) + assert job + assert len(job.workDone) == 1, len(job.workDone) + task = list(job.workDone.values())[0] + assert ( + save_task(task)["digestKeys"] + == "arguments,main,::test::.artifacts::configurator-artifacts--terraform::main,::test::.artifacts::configurator-artifacts--terraform::contents" + ) + assert ( + manifest.rootResource.find_instance("test").attributes["output_attribute"] + == "test node hello:1" + ) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 7f16f814..e5c4d493 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -178,12 +178,12 @@ def test_constraints(): @unittest.skipIf("slow" in os.getenv("UNFURL_TEST_SKIP", ""), "UNFURL_TEST_SKIP set") @pytest.mark.parametrize( - "path", ["constraints.py", "dsl_configurator.py", "dsl_relationships.py"] + "path", ["constraints.py", "dsl_configurator.py", "dsl_relationships.py", "dsl_artifacts.py"] ) def test_mypy(path): # assert mypy ok basepath = os.path.join(os.path.dirname(__file__), "examples", path) - assert_no_mypy_errors(basepath, "--disable-error-code=override") + assert_no_mypy_errors(basepath) #, "--disable-error-code=override") @unittest.skipIf("slow" in os.getenv("UNFURL_TEST_SKIP", ""), "UNFURL_TEST_SKIP set") diff --git a/tests/test_docker.py b/tests/test_docker.py index d4475f7d..cd1b127e 100644 --- a/tests/test_docker.py +++ b/tests/test_docker.py @@ -171,17 +171,6 @@ def test_login(self): assert registry and isinstance(registry, toscaparser.repositories.Repository) assert not run1.unexpectedAbort, run1.unexpectedAbort.get_stack_trace() - def test_environment(self): - src_path = str(Path(__file__).parent / "examples" / "docker-ensemble.yaml") - jobs = list( - isolated_lifecycle( - src_path, - steps=[Step("plan", Status.ok)], - ) - ) - env = jobs[0].rootResource.find_instance("container1").attributes['container']['environment'] - assert env == {'FOO': '1', 'BAR': '1', 'PASSWORD': 'test'} - def test_lifecycle(self): # note: this tests dynamically skipping an operation (start) because the previous one (create) # sets the state state @@ -193,3 +182,15 @@ def test_lifecycle(self): env = dict(UNFURL_HOME="./unfurl_home") ) ) + +def test_environment(): + # test that $APP_ synthetic environment variables work + src_path = str(Path(__file__).parent / "examples" / "docker-ensemble.yaml") + jobs = list( + isolated_lifecycle( + src_path, + steps=[Step("plan", Status.ok)], + ) + ) + env = jobs[0].rootResource.find_instance("container1").attributes['container']['environment'] + assert env == {'FOO': '1', 'BAR': '1', 'PASSWORD': 'test'} diff --git a/tests/test_docs.py b/tests/test_docs.py index 9e0a2d0b..2b237b43 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -14,7 +14,7 @@ from unfurl.yamlloader import YamlConfig from unfurl.spec import ToscaSpec from tosca import global_state -from unfurl.testing import CliRunner, run_cmd +from unfurl.testing import CliRunner, assert_no_mypy_errors, run_cmd basedir = os.path.join(os.path.dirname(__file__), "..", "docs", "examples") @@ -135,3 +135,12 @@ def test_quickstart(): run_cmd(runner, "plan production") if "slow" not in os.getenv("UNFURL_TEST_SKIP", ""): run_cmd(runner, "deploy --dryrun --approve development") + +@pytest.mark.skipif("slow" in os.getenv("UNFURL_TEST_SKIP", ""), reason="UNFURL_TEST_SKIP set") +@pytest.mark.parametrize( + "path", ["artifact2.py", "tosca-interfaces.py"] +) +def test_mypy(path): + # assert mypy ok + basepath = os.path.join(os.path.dirname(__file__), "..", "docs", "examples", path) + assert_no_mypy_errors(basepath) # , "--disable-error-code=override") diff --git a/tests/test_dsl.py b/tests/test_dsl.py index da0b4e90..65e2cfa0 100644 --- a/tests/test_dsl.py +++ b/tests/test_dsl.py @@ -119,9 +119,10 @@ def test_builtin_generation(): src_yaml[section], yaml_src[section], skipkeys=("description", "required") ) diffs.pop("unfurl.interfaces.Install", None) - diffs.pop( - "tosca.nodes.Root", None - ) # XXX sometimes present due to test race condition? + for sectiontype in ["nodes", "relationships", "groups"]: + diffs.pop( + f"tosca.{sectiontype}.Root", None + ) # adds "type" to Standard interface diffs.pop( "tosca.nodes.SoftwareComponent", None ) # !namespace attributes might get added by other tests @@ -421,7 +422,6 @@ def test_example_helloworld(): src_tpl["topology_template"]["node_templates"]["db_server"]["interfaces"][ "Standard" ] = { - "type": "tosca.interfaces.node.lifecycle.Standard", "operations": {"configure": {"implementation": "safe_mode"}}, } assert src_tpl == tosca_tpl @@ -508,18 +508,44 @@ def test_example_template(): } assert wordpress_db["interfaces"] == { "Standard": { - "type": "tosca.interfaces.node.lifecycle.Standard", "operations": { "create": { "implementation": {"primary": "db_create.sh"}, - "inputs": {"db_data": {"get_artifact": ["SELF", "db_content"]} - }, + "inputs": {"db_data": {"get_artifact": ["SELF", "db_content"]}}, + "metadata": { + "arguments": [ + "db_data", + ], + }, } }, } } +custom_interface_python = """ +import unfurl +import tosca +class MyCustomInterface(tosca.interfaces.Root): + class Inputs(tosca.ToscaInputs): + location: str + version: int = 0 + + my_operation = tosca.operation() + "an abstract operation, subclass needs to implement" + +class Example(tosca.nodes.Root, MyCustomInterface): + shellScript = tosca.artifacts.ImplementationBash(file="example.sh") + prop1: str + host: tosca.nodes.Compute + + def my_operation(self): + return self.shellScript.execute( + MyCustomInterface.Inputs(location=self.prop1), + host=self.host.public_address, + ) +""" + custom_interface_yaml = """ tosca_definitions_version: tosca_simple_unfurl_1_0_0 node_types: @@ -547,6 +573,8 @@ def test_example_template(): eval: .::prop1 host: eval: .::.targets::host::public_address + metadata: + arguments: [location, host] interface_types: MyCustomInterface: derived_from: tosca.interfaces.Root @@ -558,36 +586,14 @@ def test_example_template(): default: 0 operations: my_operation: - description: description of my_operation + description: an abstract operation, subclass needs to implement topology_template: {} """ def test_custom_interface(): - class MyCustomInterface(tosca.interfaces.Root): - class Inputs(tosca.ToscaInputs): - location: str - version: int = 0 - - def my_operation(self): - "description of my_operation" - ... # an abstract operation, subclass needs to implement - - class Example(tosca.nodes.Root, MyCustomInterface): - shellScript = tosca.artifacts.ImplementationBash(file="example.sh") - prop1: str - host: tosca.nodes.Compute - - def my_operation(self): - return self.shellScript.execute( - MyCustomInterface.Inputs(location=self.prop1), - host=self.host.public_address, - ) - - __name__ = "tests.test_dsl" - converter = PythonToYaml(locals()) - yaml_dict = converter.module2yaml() - # yaml.dump(yaml_dict, sys.stdout) + yaml_dict = _to_yaml(custom_interface_python, False) + yaml.dump(yaml_dict, sys.stdout) tosca_yaml = load_yaml(yaml, custom_interface_yaml) assert yaml_dict == tosca_yaml @@ -595,8 +601,8 @@ def my_operation(self): def test_generator_operation(): import unfurl.configurators.shell from unfurl.configurator import TaskView - class Node(tosca.nodes.Root): + class Node(tosca.nodes.Root): def foo(self, ctx: TaskView): result = yield unfurl.configurators.shell.ShellConfigurator() if result.result.success: @@ -612,6 +618,7 @@ def configure(self, **kw): yaml_dict = converter.module2yaml() yaml.dump(yaml_dict, sys.stdout) + def test_bad_field(): class BadNode(tosca.nodes.Root): a_property: str = tosca.Property() @@ -627,7 +634,7 @@ class BadNode(tosca.nodes.Root): def test_class_init() -> None: class Example(tosca.nodes.Root): - shellScript: tosca.artifacts.Root = tosca.artifacts.Root(file="example.sh") + shellScript: tosca.artifacts.Root = tosca.CONSTRAINED prop1: Optional[str] = tosca.CONSTRAINED host: tosca.nodes.Compute = tosca.CONSTRAINED self_reference: "Example" = tosca.CONSTRAINED @@ -636,6 +643,7 @@ class Example(tosca.nodes.Root): def _class_init(cls) -> None: cls.self_reference.prop1 = cls.prop1 or "" cls.host.public_address = cls.prop1 or "" + cls.shellScript = tosca.artifacts.Root(file="example.sh", intent=cls.prop1) with pytest.raises(ValueError) as e_info1: cls.host.host = cls.host.host @@ -647,7 +655,8 @@ def _class_init(cls) -> None: cls.set_to_property_source(cls.host, cls.prop1) def create(self, **kw) -> tosca.artifacts.Root: - return self.shellScript.execute(input1=self.prop1) + self.shellScript.execute(input1=self.prop1) + return self.shellScript # print( str(inspect.signature(Example.__init__)) ) @@ -661,6 +670,23 @@ def create(self, **kw) -> tosca.artifacts.Root: converter = PythonToYaml(locals()) yaml_dict = converter.module2yaml() # yaml.dump(yaml_dict, sys.stdout) + assert yaml_dict["topology_template"] == { + "node_templates": { + "my_compute": {"type": "tosca.nodes.Compute"}, + "my_template": { + "type": "Example", + "artifacts": { + "shellScript": { + "type": "tosca.artifacts.Root", + "file": "example.sh", + "intent": {"eval": ".parent::prop1"}, + } + }, + "requirements": [{"host": "my_compute"}], + "metadata": {"module": "tests.test_dsl"}, + }, + } + } assert yaml_dict["node_types"] == { "Example": { "derived_from": "tosca.nodes.Root", @@ -674,7 +700,13 @@ def create(self, **kw) -> tosca.artifacts.Root: } }, "artifacts": { - "shellScript": {"file": "example.sh", "type": "tosca.artifacts.Root"} + "shellScript": { + "file": "example.sh", + "type": "tosca.artifacts.Root", + "intent": { + "eval": ".parent::prop1", + }, + } }, "requirements": [ { @@ -703,6 +735,11 @@ def create(self, **kw) -> tosca.artifacts.Root: "create": { "implementation": {"primary": "shellScript"}, "inputs": {"input1": {"eval": ".::prop1"}}, + "metadata": { + "arguments": [ + "input1", + ], + }, } } } @@ -725,6 +762,11 @@ def create(self, **kw) -> tosca.artifacts.Root: prop1: type: string default: '' + map: + type: map + default: {} + entry_schema: + type: integer topology_template: node_templates: test: @@ -734,17 +776,19 @@ def create(self, **kw) -> tosca.artifacts.Root: properties: data: prop1: test + map: {} """ def test_datatype(): import tosca - from tosca import DataType + from tosca import DataType, DEFAULT with tosca.set_evaluation_mode("parse"): class MyDataType(DataType): prop1: str = "" + map: Dict[str, int] = DEFAULT class Example(tosca.nodes.Root): data: MyDataType @@ -837,7 +881,11 @@ class pcls(tosca.InstanceProxy): generic_envvars = unfurl.datatypes.EnvironmentVariables(DBASE="aaaa", URL=True) assert generic_envvars.to_yaml() == {"DBASE": "aaaa", "URL": True} - assert OpenDataEntity(a=1, b="b").extend(c="c").to_yaml() == {"a": 1, "b": "b", "c": "c"} + assert OpenDataEntity(a=1, b="b").extend(c="c").to_yaml() == { + "a": 1, + "b": "b", + "c": "c", + } assert Namespace.MyDataType(name="foo").to_yaml() == {"name": "foo"} # make sure DockerContainer is an OpenDataEntity unfurl_datatypes_DockerContainer().extend(labels=dict(foo="bar")) diff --git a/tox.ini b/tox.ini index b431e74e..167b292c 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ setenv = ; XXX set HOME to isolated path ; can also set TOX_TESTENV_PASSENV="ENV_VAR1 ENV_VAR1" -passenv=HOME CI GITHUB_* RUNNER_* ANSIBLE_VERBOSITY ANSIBLE_DEBUG UNFURL_WORKDIR UNFURL_LOGGING UNFURL_RAISE_LOGGING_EXCEPTIONS UNFURL_LOG_TRUNCATE UNFURL_TEST_* ANDROID_* TRAVIS* DOCKER_* +passenv=HOME CI GITHUB_* RUNNER_* ANSIBLE_VERBOSITY ANSIBLE_DEBUG UNFURL_WORKDIR UNFURL_LOGGING UNFURL_RAISE_LOGGING_EXCEPTIONS UNFURL_LOG_TRUNCATE UNFURL_TEST_* ANDROID_* TRAVIS* DOCKER_* UNFURL_TMPDIR basepython = py38: python3.8 py39: python3.9