diff --git a/model/_init.cf b/model/_init.cf index 55535a9..d548434 100644 --- a/model/_init.cf +++ b/model/_init.cf @@ -641,3 +641,27 @@ ResourceSet.resources [0:] -- std::Resource index ResourceSet(name) implement ResourceSet using std::none + + +typedef value_reference as string matching std::is_valid_value_reference(self) + + +entity EnvironmentReference: + """ A reference to an environment variable. + + :attr variable_name: The name of the environment variable to reference. + :attr value: The value from the environment + """ + string variable_name + string value +end + + +implementation env_ref for EnvironmentReference: + self.value = std::get_value_reference_attribute( + reference=std::create_environment_reference(self.variable_name), + name="value", + ) +end + +implement EnvironmentReference using env_ref diff --git a/plugins/__init__.py b/plugins/__init__.py index f806564..f134f2a 100644 --- a/plugins/__init__.py +++ b/plugins/__init__.py @@ -28,7 +28,8 @@ from copy import copy from itertools import chain from operator import attrgetter -from typing import Any, Optional, Tuple +from typing import Any, Optional, Tuple, Literal +from dataclasses import dataclass import jinja2 import pydantic @@ -45,7 +46,7 @@ from inmanta.export import dependency_manager, unknown_parameters from inmanta.module import Project from inmanta.plugins import Context, deprecated, plugin - +from inmanta import references @plugin def unique_file( @@ -1264,3 +1265,52 @@ def ip_address_from_interface( :param ip_interface: The interface from where we will extract the ip address """ return str(ipaddress.ip_interface(ip_interface).ip) + + +@plugin +def is_valid_value_reference(value: "string") -> "bool": + """ Validate if value is a valid value reference + """ + return isinstance(value, references.ValueReference) + + +@plugin +def get_value_reference_attribute(reference: "std::value_reference", name: "string") -> "string": + """ Reference an attribute in a secret reference + """ + # TODO: we can add type checking here so that we can only reference attribute that exist on the + # secret that reference returns + return references.ValueReferenceAttributeString.create(reference, name) + + +@dataclass +class EnvironmentValue: + value: str + + +class EnvironmentVariableReference(references.ValueReferenceModel): + # TODO: this one should also refer to EnvironmentVariable so that plugins like get_value_reference_attribute can do more type checking + reference_type: Literal["std::EnvironmentReference"] = "std::EnvironmentReference" + variable_name: str + """ The variable name to fetch the value from + """ + +class EnvironmentVariableReferenceResolver(references.Resolver[EnvironmentVariableReference, EnvironmentValue]): + def __init__(self, reference: EnvironmentVariableReference) -> None: + self._reference: EnvironmentVariableReference = reference + + def fetch(self) -> EnvironmentValue: + """ Fetch the value + """ + return EnvironmentValue(value=os.getenv(self._reference.variable_name)) + + +@plugin +def create_environment_reference(variable_name: "string") -> "std::value_reference": + """Create a reference to an enviroment variable + """ + return references.ValueReference.create( + reference=EnvironmentVariableReference( + variable_name=variable_name, + ) + ) diff --git a/plugins/resources.py b/plugins/resources.py index 6834632..37adeb5 100644 --- a/plugins/resources.py +++ b/plugins/resources.py @@ -67,29 +67,6 @@ def generate_content(content_list, seperator): return seperator.join([x[1] for x in sort_list]) + seperator -def store_file(exporter, obj): - content = obj.content - if isinstance(content, Unknown): - return content - - if "FileMarker" in content.__class__.__name__: - with open(content.filename, "rb") as fd: - content = fd.read() - - if len(obj.prefix_content) > 0: - content = ( - generate_content(obj.prefix_content, obj.content_seperator) - + obj.content_seperator - + content - ) - if len(obj.suffix_content) > 0: - content += obj.content_seperator + generate_content( - obj.suffix_content, obj.content_seperator - ) - - return exporter.upload_file(content) - - @resource("std::Service", agent="host.name", id_attribute="name") class Service(Resource): """ @@ -105,8 +82,34 @@ class File(PurgeableResource): A file on a filesystem """ - fields = ("path", "owner", "hash", "group", "permissions", "reload") - map = {"hash": store_file, "permissions": lambda y, x: int(x.mode)} + fields = ("path", "owner", "content", "group", "permissions", "reload") + + @staticmethod + def get_permissions(_, instance) -> int: + return int(instance.mode) + + @staticmethod + def get_hash(exporter, obj): + content = obj.content + if isinstance(content, Unknown): + return content + + if "FileMarker" in content.__class__.__name__: + with open(content.filename, "rb") as fd: + content = fd.read() + + if len(obj.prefix_content) > 0: + content = ( + generate_content(obj.prefix_content, obj.content_seperator) + + obj.content_seperator + + content + ) + if len(obj.suffix_content) > 0: + content += obj.content_seperator + generate_content( + obj.suffix_content, obj.content_seperator + ) + + return exporter.upload_file(content) @resource("std::Directory", agent="host.name", id_attribute="path") @@ -192,13 +195,8 @@ def read_resource(self, ctx: HandlerContext, resource: PurgeableResource) -> Non if not self._io.file_exists(resource.path): raise ResourcePurged() - resource.hash = self._io.hash_file(resource.path) - # upload the previous version for back up and for generating a diff! - content = self._io.read_binary(resource.path) - - if not self.stat_file(resource.hash): - self.upload_file(resource.hash, content) + resource.content = self._io.read_binary(resource.path).decode() for key, value in self._io.file_stat(resource.path).items(): setattr(resource, key, value) @@ -209,13 +207,7 @@ def create_resource(self, ctx: HandlerContext, resource: PurgeableResource) -> N f"Cannot create file {resource.path}, because it already exists." ) - data = self.get_file(resource.hash) - if hash_file(data) != resource.hash: - raise Exception( - "File hash was %s expected %s" % (resource.hash, hash_file(data)) - ) - - self._io.put(resource.path, data) + self._io.put(resource.path, resource.content.encode()) self._io.chmod(resource.path, str(resource.permissions)) self._io.chown(resource.path, resource.owner, resource.group) ctx.set_created() @@ -233,19 +225,15 @@ def update_resource( f"Cannot update file {resource.path} because it doesn't exist" ) - if "hash" in changes: - data = self.get_file(resource.hash) - if hash_file(data) != resource.hash: - raise Exception( - "File hash was %s expected %s" % (resource.hash, hash_file(data)) - ) - self._io.put(resource.path, data) + if "content" in changes: + self._io.put(resource.path, resource.content.encode()) if "permissions" in changes: self._io.chmod(resource.path, str(resource.permissions)) if "owner" in changes or "group" in changes: self._io.chown(resource.path, resource.owner, resource.group) + ctx.set_updated() diff --git a/tests/test_references.py b/tests/test_references.py new file mode 100644 index 0000000..e209f10 --- /dev/null +++ b/tests/test_references.py @@ -0,0 +1,23 @@ + +import os + +def test_reference(project) -> None: + """ Test the use of reference + """ + project.compile(""" +env_ref = std::EnvironmentReference(variable_name="USER") + +file = std::ConfigFile( + host=std::Host(name="test", os=std::linux), + owner="bart", + group="bart", + path="/tmp/reference", + content=env_ref.value +) +""") + + project.deploy_all().assert_all() + + + with open("/tmp/reference", "r") as fd: + assert fd.read() == os.getenv("USER")