From f03f5804c6a3d8ef646f984cbd74915f723a7f4a Mon Sep 17 00:00:00 2001 From: John Chilton Date: Wed, 29 Mar 2023 08:44:06 -0400 Subject: [PATCH] Object Store creation templates. --- .../ObjectStore/Templates/CreateForm.vue | 54 +++++ .../Templates/CreateUserObjectStore.vue | 68 ++++++ .../ObjectStore/Templates/SelectTemplate.vue | 52 +++++ client/src/components/ObjectStore/services.ts | 10 + client/src/entry/analysis/router.js | 5 + lib/galaxy/app.py | 5 + lib/galaxy/config/schemas/config_schema.yml | 15 ++ lib/galaxy/model/__init__.py | 17 ++ ...3c93d66a_add_user_defined_object_stores.py | 50 +++++ lib/galaxy/objectstore/templates/__init__.py | 7 + lib/galaxy/objectstore/templates/manager.py | 75 +++++++ lib/galaxy/objectstore/templates/models.py | 200 ++++++++++++++++++ lib/galaxy/webapps/galaxy/api/object_store.py | 16 ++ lib/galaxy/webapps/galaxy/buildapp.py | 1 + .../unit/objectstore/test_template_manager.py | 18 ++ test/unit/objectstore/test_template_models.py | 132 ++++++++++++ 16 files changed, 725 insertions(+) create mode 100644 client/src/components/ObjectStore/Templates/CreateForm.vue create mode 100644 client/src/components/ObjectStore/Templates/CreateUserObjectStore.vue create mode 100644 client/src/components/ObjectStore/Templates/SelectTemplate.vue create mode 100644 lib/galaxy/model/migrations/alembic/versions_gxy/c14a3c93d66a_add_user_defined_object_stores.py create mode 100644 lib/galaxy/objectstore/templates/__init__.py create mode 100644 lib/galaxy/objectstore/templates/manager.py create mode 100644 lib/galaxy/objectstore/templates/models.py create mode 100644 test/unit/objectstore/test_template_manager.py create mode 100644 test/unit/objectstore/test_template_models.py diff --git a/client/src/components/ObjectStore/Templates/CreateForm.vue b/client/src/components/ObjectStore/Templates/CreateForm.vue new file mode 100644 index 000000000000..a6bb342c7f72 --- /dev/null +++ b/client/src/components/ObjectStore/Templates/CreateForm.vue @@ -0,0 +1,54 @@ + + \ No newline at end of file diff --git a/client/src/components/ObjectStore/Templates/CreateUserObjectStore.vue b/client/src/components/ObjectStore/Templates/CreateUserObjectStore.vue new file mode 100644 index 000000000000..304e39da6d74 --- /dev/null +++ b/client/src/components/ObjectStore/Templates/CreateUserObjectStore.vue @@ -0,0 +1,68 @@ + + diff --git a/client/src/components/ObjectStore/Templates/SelectTemplate.vue b/client/src/components/ObjectStore/Templates/SelectTemplate.vue new file mode 100644 index 000000000000..b33966794e75 --- /dev/null +++ b/client/src/components/ObjectStore/Templates/SelectTemplate.vue @@ -0,0 +1,52 @@ + + + diff --git a/client/src/components/ObjectStore/services.ts b/client/src/components/ObjectStore/services.ts index 23c0f4e3b3fc..92f2ed8bfab7 100644 --- a/client/src/components/ObjectStore/services.ts +++ b/client/src/components/ObjectStore/services.ts @@ -1,8 +1,18 @@ +import type { components } from "@/schema"; import { fetcher } from "@/schema/fetcher"; const getObjectStores = fetcher.path("/api/object_stores").method("get").create(); +const getObjectStoreTemplates = fetcher.path("/api/object_store_templates").method("get").create(); + +export type ObjectStoreTemplateSummaries = components["schemas"]["ObjectStoreTemplateSummaries"]; +export type ObjectStoreTemplateSummary = components["schemas"]["ObjectStoreTemplateSummary"]; export async function getSelectableObjectStores() { const { data } = await getObjectStores({ selectable: true }); return data; } + +export async function getTemplates(): Promise { + const { data } = await getObjectStoreTemplates({}); + return data; +} diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index 35f234452422..354139aae102 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -60,6 +60,7 @@ import VisualizationPublished from "components/Visualizations/VisualizationPubli import WorkflowImport from "components/Workflow/WorkflowImport"; import WorkflowList from "components/Workflow/WorkflowList"; import WorkflowPublished from "components/Workflow/WorkflowPublished"; +import CreateUserObjectStore from "components/ObjectStore/Templates/CreateUserObjectStore"; import { APIKey } from "components/User/APIKey"; import { CloudAuth } from "components/User/CloudAuth"; import { ExternalIdentities } from "components/User/ExternalIdentities"; @@ -271,6 +272,10 @@ export function getRouter(Galaxy) { termsUrl: Galaxy.config.terms_url, }, }, + { + path: "object_store/create", + component: CreateUserObjectStore, + }, { path: "pages/create", component: FormGeneric, diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index edea14e22c3c..12c33bc7a04f 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -81,6 +81,7 @@ BaseObjectStore, build_object_store_from_config, ) +from galaxy.objectstore.templates import ConfiguredObjectStoreTemplates from galaxy.queue_worker import ( GalaxyQueueWorker, send_local_control_task, @@ -549,6 +550,10 @@ def __init__(self, configure_logging=True, use_converters=True, use_display_appl ConfiguredFileSources, ConfiguredFileSources.from_app_config(self.config) ) + self.object_store_templates = self._register_singleton( + ConfiguredObjectStoreTemplates, ConfiguredObjectStoreTemplates.from_app_config(self.config) + ) + self.vault = self._register_singleton(Vault, VaultFactory.from_app(self)) # type: ignore[type-abstract] # Load security policy. self.security_agent = self.model.security_agent diff --git a/lib/galaxy/config/schemas/config_schema.yml b/lib/galaxy/config/schemas/config_schema.yml index ccc0955444b7..2f0303607064 100644 --- a/lib/galaxy/config/schemas/config_schema.yml +++ b/lib/galaxy/config/schemas/config_schema.yml @@ -551,6 +551,21 @@ mapping: desc: | FileSource plugins described embedded into Galaxy's config. + object_store_templates_config_file: + type: str + default: object_store_templates.yml + path_resolves_to: config_dir + required: false + desc: | + Configured Object Store templates configuration file. + + object_store_templates: + type: seq + sequence: + - type: any + desc: | + Configured Object Store templates embedded into Galaxy's config. + enable_mulled_containers: type: bool default: true diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 39b2aedd152d..9d15dcf5c743 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -10065,6 +10065,23 @@ def __init__(self, name=None, value=None): self.value = value +class UserObjectStore(Base, RepresentById): + __tablename__ = "user_object_store" + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("galaxy_user.id"), index=True) + create_time = Column(DateTime, default=now) + update_time = Column(DateTime, default=now, onupdate=now, index=True) + name = Column(String(255), index=True) + object_store_template_id = Column(String(255), index=True) + object_store_template_version = Column(Integer, index=True) + # Maybe just use the id/version and don't record the definition + object_store_template_definition = Column(JSONType) + # Maybe convert these maps to association tables encoding maps... + object_store_template_variables = Column(JSONType) + object_store_template_secrets = Column(JSONType) + + class UserAction(Base, RepresentById): __tablename__ = "user_action" diff --git a/lib/galaxy/model/migrations/alembic/versions_gxy/c14a3c93d66a_add_user_defined_object_stores.py b/lib/galaxy/model/migrations/alembic/versions_gxy/c14a3c93d66a_add_user_defined_object_stores.py new file mode 100644 index 000000000000..63eb4e39fd53 --- /dev/null +++ b/lib/galaxy/model/migrations/alembic/versions_gxy/c14a3c93d66a_add_user_defined_object_stores.py @@ -0,0 +1,50 @@ +"""add user defined object stores + +Revision ID: c14a3c93d66a +Revises: 460d0ecd1dd8 +Create Date: 2023-04-01 17:25:37.553039 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy import ( + Column, + DateTime, + ForeignKey, + Integer, + String, +) + +from galaxy.model.custom_types import JSONType + +# revision identifiers, used by Alembic. +revision = "c14a3c93d66a" +down_revision = "460d0ecd1dd8" +branch_labels = None +depends_on = None + + +# database object names used in this revision +table_name = "user_object_store" + + +def upgrade(): + op.create_table( + table_name, + Column("id", Integer, primary_key=True), + Column("user_id", Integer, ForeignKey("galaxy_user.id"), nullable=False), + Column("name", String(255), index=True), + Column("create_time", DateTime), + Column("update_time", DateTime), + Column("template_id", String(255), index=True), + Column("template_version", Integer, index=True), + # Maybe just use the id/version and don't record the definition + Column("template_definition", JSONType), + # Maybe convert these maps to association tables encoding maps... + Column("template_variables", JSONType), + Column("template_secrets", JSONType), + ) + + +def downgrade(): + op.drop_table(table_name) diff --git a/lib/galaxy/objectstore/templates/__init__.py b/lib/galaxy/objectstore/templates/__init__.py new file mode 100644 index 000000000000..3859884bb1f7 --- /dev/null +++ b/lib/galaxy/objectstore/templates/__init__.py @@ -0,0 +1,7 @@ +from .manager import ConfiguredObjectStoreTemplates +from .models import ObjectStoreTemplateSummaries + +__all__ = ( + "ConfiguredObjectStoreTemplates", + "ObjectStoreTemplateSummaries", +) diff --git a/lib/galaxy/objectstore/templates/manager.py b/lib/galaxy/objectstore/templates/manager.py new file mode 100644 index 000000000000..43fe9e4efa3f --- /dev/null +++ b/lib/galaxy/objectstore/templates/manager.py @@ -0,0 +1,75 @@ +import os +from typing import ( + Any, + Dict, + List, + Optional, +) + +from typing_extensions import Protocol +from yaml import safe_load + +from .models import ( + ObjectStoreTemplateCatalog, + ObjectStoreTemplateSummaries, +) + +RawTemplateConfig = Dict[str, Any] + + +class AppConfigProtocol(Protocol): + object_store_templates: Optional[List[RawTemplateConfig]] + object_store_templates_config_file: Optional[str] + + +class ConfiguredObjectStoreTemplates: + catalog: ObjectStoreTemplateCatalog + + def __init__(self, catalog: ObjectStoreTemplateCatalog): + self.catalog = catalog + + @staticmethod + def from_app_config(config: AppConfigProtocol) -> "ConfiguredObjectStoreTemplates": + raw_config = config.object_store_templates + if raw_config is None: + config_file = config.object_store_templates_config_file + if config_file and os.path.exists(config_file): + with open(config_file, "r") as f: + raw_config = safe_load(f) + if raw_config is None: + raw_config = [] + return ConfiguredObjectStoreTemplates(raw_config_to_catalog(raw_config)) + + @property + def summaries(self) -> ObjectStoreTemplateSummaries: + templates = self.catalog.__root__ + summaries = [] + for template in templates: + template_dict = template.dict() + template_dict.pop("configuration") + summaries.append(template_dict) + return ObjectStoreTemplateSummaries.parse_obj(summaries) + + +def raw_config_to_catalog(raw_config: List[RawTemplateConfig]) -> ObjectStoreTemplateCatalog: + effective_root = _apply_syntactic_sugar(raw_config) + return ObjectStoreTemplateCatalog.parse_obj(effective_root) + + +def _apply_syntactic_sugar(raw_templates: List[RawTemplateConfig]) -> List[RawTemplateConfig]: + templates = [] + for template in raw_templates: + _force_key_to_list(template, "variables") + _force_key_to_list(template, "secrets") + templates.append(template) + return templates + + +def _force_key_to_list(template: RawTemplateConfig, key: str) -> None: + value = template.get(key, None) + if isinstance(value, dict): + value_as_list = [] + for key_name, key_value in value.items(): + key_value["name"] = key_name + value_as_list.append(key_value) + template[key] = value_as_list diff --git a/lib/galaxy/objectstore/templates/models.py b/lib/galaxy/objectstore/templates/models.py new file mode 100644 index 000000000000..aea222218e50 --- /dev/null +++ b/lib/galaxy/objectstore/templates/models.py @@ -0,0 +1,200 @@ +from typing import ( + Any, + Dict, + List, + Optional, + Type, + Union, +) + +from boltons.iterutils import remap +from jinja2.nativetypes import NativeEnvironment +from pydantic import ( + BaseModel, + Extra, +) +from typing_extensions import Literal + + +class StrictModel(BaseModel): + class Config: + extra = Extra.forbid + + +ObjectStoreTemplateVariableType = Literal["string", "boolean", "integer"] +TemplateExpansion = str + + +class S3AuthTemplate(StrictModel): + access_key: Union[str, TemplateExpansion] + secret_key: Union[str, TemplateExpansion] + + +class S3Auth(StrictModel): + access_key: str + secret_key: str + + +class S3BucketTemplate(StrictModel): + name: Union[str, TemplateExpansion] + use_reduced_redundancy: Optional[Union[bool, TemplateExpansion]] = None + + +class S3Bucket(StrictModel): + name: str + use_reduced_redundancy: Optional[bool] = None + + +class S3ObjectStoreTemplateConfiguration(StrictModel): + type: Literal["s3"] + auth: S3AuthTemplate + bucket: S3BucketTemplate + + +class S3ObjectStoreConfiguration(StrictModel): + type: Literal["s3"] + auth: S3Auth + bucket: S3Bucket + + +class AzureAuthTemplate(StrictModel): + account_name: Union[str, TemplateExpansion] + account_key: Union[str, TemplateExpansion] + + +class AzureAuth(StrictModel): + account_name: str + account_key: str + + +class AzureContainerTemplate(StrictModel): + name: Union[str, TemplateExpansion] + + +class AzureContainer(StrictModel): + name: str + + +class AzureObjectStoreTemplateConfiguration(StrictModel): + type: Literal["azure"] + auth: AzureAuthTemplate + container: AzureContainerTemplate + + +class AzureObjectStoreConfiguration(StrictModel): + type: Literal["azure"] + auth: AzureAuth + container: AzureContainer + + +class DiskObjectStoreTemplateConfiguration(StrictModel): + type: Literal["disk"] + files_dir: Union[str, TemplateExpansion] + + +class DiskObjectStoreConfiguration(StrictModel): + type: Literal["disk"] + files_dir: str + + +ObjectStoreTemplateConfiguration = Union[ + S3ObjectStoreTemplateConfiguration, + DiskObjectStoreTemplateConfiguration, + AzureObjectStoreTemplateConfiguration, +] +ObjectStoreConfiguration = Union[ + S3ObjectStoreConfiguration, + DiskObjectStoreConfiguration, + AzureObjectStoreConfiguration, +] +MarkdownContent = str + + +class ObjectStoreTemplateVariable(StrictModel): + name: str + help: Optional[MarkdownContent] + type: ObjectStoreTemplateVariableType + + +class ObjectStoreTemplateSecret(StrictModel): + name: str + help: Optional[MarkdownContent] + + +class ObjectStoreTemplateSummary(StrictModel): + """Version of ObjectStoreTemplate we can send to the UI/API. + + The configuration key in the child type may have secretes + and shouldn't be exposed over the API - at least to non-admins. + """ + + id: str + name: Optional[str] + description: Optional[MarkdownContent] + # The UI should just show the most recent version but allow + # admins to define newer versions with new parameterizations + # and keep old versions in template catalog for backward compatibility + # for users with existing stores of that template. + version: int = 0 + # Like with multiple versions, allow admins to deprecate a + # template by hiding but keep it in the catalog for backward + # compatibility for users with existing stores of that template. + hidden: bool = False + variables: Optional[List[ObjectStoreTemplateVariable]] + secrets: Optional[List[ObjectStoreTemplateSecret]] + + +class ObjectStoreTemplate(ObjectStoreTemplateSummary): + configuration: ObjectStoreTemplateConfiguration + + +class ObjectStoreTemplateCatalog(StrictModel): + """Represents a collection of ObjectStoreTemplates.""" + + __root__: List[ObjectStoreTemplate] + + +class ObjectStoreTemplateSummaries(StrictModel): + """Represents a collection of ObjectStoreTemplate summaries.""" + + __root__: List[ObjectStoreTemplateSummary] + + +def template_to_configuration( + template: ObjectStoreTemplate, + variables: Dict[str, Any], + secrets: Dict[str, str], + user_details: Dict[str, Any], +) -> ObjectStoreConfiguration: + configuration_template = template.configuration + template_variables = { + "variables": variables, + "secrets": secrets, + "user": user_details, + } + + def expand_template(_, key, value): + if isinstance(value, str) and "{{" in value and "}}" in value: + # NativeEnvironment preserves Python types + template = NativeEnvironment().from_string(value) + return key, template.render(**template_variables) + return key, value + + raw_config = remap(configuration_template.dict(), visit=expand_template) + return to_configuration_object(raw_config) + + +TypesToConfigurationClasses: Dict[str, Type[ObjectStoreConfiguration]] = { + "s3": S3ObjectStoreConfiguration, + "azure": AzureObjectStoreConfiguration, + "disk": DiskObjectStoreConfiguration, +} + + +def to_configuration_object(configuration_dict: Dict[str, Any]) -> ObjectStoreConfiguration: + if "type" not in configuration_dict: + raise KeyError("Configuration objects require an object store 'type' key, none found.") + object_store_type = configuration_dict["type"] + if object_store_type not in TypesToConfigurationClasses: + raise ValueError(f"Unknown object store type found in raw configuration dictionary ({object_store_type}).") + return TypesToConfigurationClasses[object_store_type](**configuration_dict) diff --git a/lib/galaxy/webapps/galaxy/api/object_store.py b/lib/galaxy/webapps/galaxy/api/object_store.py index f8d7319b885f..31bfed6ba918 100644 --- a/lib/galaxy/webapps/galaxy/api/object_store.py +++ b/lib/galaxy/webapps/galaxy/api/object_store.py @@ -18,6 +18,10 @@ BaseObjectStore, ConcreteObjectStoreModel, ) +from galaxy.objectstore.templates import ( + ConfiguredObjectStoreTemplates, + ObjectStoreTemplateSummaries, +) from . import ( depends, DependsOnTrans, @@ -42,6 +46,7 @@ @router.cbv class FastAPIObjectStore: object_store: BaseObjectStore = depends(BaseObjectStore) + object_store_templates: ConfiguredObjectStoreTemplates = depends(ConfiguredObjectStoreTemplates) @router.get( "/api/object_stores", @@ -76,3 +81,14 @@ def _model_for(self, object_store_id: str) -> ConcreteObjectStoreModel: if concrete_object_store is None: raise ObjectNotFound() return concrete_object_store.to_model(object_store_id) + + @router.get( + "/api/object_store_templates", + summary="Get a list of object store templates available to build user defined object stores from", + response_description="A list of the configured object store templates.", + operation_id="object_stores__templates_index", + ) + def index_templates( + self, + ) -> ObjectStoreTemplateSummaries: + return self.object_store_templates.summaries diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index a9bcaba11314..7ac883b77fc0 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -218,6 +218,7 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs): webapp.add_client_route("/tours/{tour_id}") webapp.add_client_route("/user") webapp.add_client_route("/user/{form_id}") + webapp.add_client_route("/object_store/create") webapp.add_client_route("/welcome/new") webapp.add_client_route("/visualizations") webapp.add_client_route("/visualizations/edit") diff --git a/test/unit/objectstore/test_template_manager.py b/test/unit/objectstore/test_template_manager.py new file mode 100644 index 000000000000..86f245f2ac92 --- /dev/null +++ b/test/unit/objectstore/test_template_manager.py @@ -0,0 +1,18 @@ +from galaxy.objectstore.templates import ConfiguredObjectStoreTemplates +from .test_template_models import LIBRARY_2 + + +class MockConfig: + def __init__(self, config_path): + self.object_store_templates = None + self.object_store_templates_config_file = config_path + + +def test_manager(tmpdir): + config_path = tmpdir / "conf.yml" + config_path.write_text(LIBRARY_2, "utf-8") + config = MockConfig(config_path) + templates = ConfiguredObjectStoreTemplates.from_app_config(config) + summaries = templates.summaries + assert summaries + assert len(summaries.__root__) == 2 diff --git a/test/unit/objectstore/test_template_models.py b/test/unit/objectstore/test_template_models.py new file mode 100644 index 000000000000..1a5a67ee7d25 --- /dev/null +++ b/test/unit/objectstore/test_template_models.py @@ -0,0 +1,132 @@ +from yaml import safe_load + +from galaxy.objectstore.templates.manager import raw_config_to_catalog +from galaxy.objectstore.templates.models import ( + DiskObjectStoreConfiguration, + ObjectStoreTemplateCatalog, + S3ObjectStoreConfiguration, + template_to_configuration, +) + +LIBRARY_1 = """ +- id: amazon_bucket + name: Amazon Bucket + description: An Amazon S3 Bucket + variables: + use_reduced_redundancy: + type: boolean + help: Reduce redundancy and save money. + secrets: + access_key: + help: AWS access key to use when connecting to AWS resources. + secret_key: + help: AWS secret key to use when connecting to AWS resources. + bucket_name: + help: Name of bucket to use when connecting to AWS resources. + configuration: + type: s3 + auth: + access_key: '{{ secrets.access_key}}' + secret_key: '{{ secrets.secret_key}}' + bucket: + name: '{{ secrets.bucket_name}}' + use_reduced_redundancy: '{{ variables.use_reduced_redundancy}}' +""" + + +def test_parsing_simple_s3(): + template_library = _parse_template_library(LIBRARY_1) + assert len(template_library.__root__) == 1 + s3_template = template_library.__root__[0] + assert s3_template.description == "An Amazon S3 Bucket" + configuration_obj = template_to_configuration( + s3_template, + {"use_reduced_redundancy": False}, + {"access_key": "sec1", "secret_key": "sec2", "bucket_name": "sec3"}, + user_details={}, + ) + # expanded configuration should validate with template expansions... + assert isinstance(configuration_obj, S3ObjectStoreConfiguration) + configuration = configuration_obj.dict() + + assert configuration["type"] == "s3" + assert configuration["auth"]["access_key"] == "sec1" + assert configuration["auth"]["secret_key"] == "sec2" + assert configuration["bucket"]["name"] == "sec3" + assert configuration["bucket"]["use_reduced_redundancy"] is False + + +LIBRARY_2 = """ +- id: general_disk + name: General Disk + description: General Disk Bound to You + configuration: + type: disk + files_dir: '/data/general/{{ user.username }}' +- id: secure_disk + name: Secure Disk + description: Secure Disk Bound to You + configuration: + type: disk + files_dir: '/data/secure/{{ user.username }}' +""" + + +def test_parsing_multiple_posix(): + template_library = _parse_template_library(LIBRARY_2) + assert len(template_library.__root__) == 2 + general_template = template_library.__root__[0] + secure_template = template_library.__root__[1] + + assert general_template.version == 0 + assert secure_template.version == 0 + assert secure_template.hidden is False + + general_configuration = template_to_configuration(general_template, {}, {}, user_details={"username": "jane"}) + assert isinstance(general_configuration, DiskObjectStoreConfiguration) + assert general_configuration.files_dir == "/data/general/jane" + + secure_configuration = template_to_configuration(secure_template, {}, {}, user_details={"username": "jane"}) + assert isinstance(secure_configuration, DiskObjectStoreConfiguration) + assert secure_configuration.files_dir == "/data/secure/jane" + + +LIBRARY_AZURE_CONTAINER = """ +- id: amazon_bucket + name: Azure Container + description: An Azure Container + variables: + account_name: + type: string + help: Azure account name to use when connecting to Azure resources. + secrets: + account_key: + help: Azure account key to use when connecting to Azure resources. + container_name: + help: Name of container to use when connecting to Azure cloud resources. + configuration: + type: azure + auth: + account_name: '{{ variables.account_name}}' + account_key: '{{ secrets.account_key}}' + container: + name: '{{ secrets.container_name}}' +""" + + +def test_parsing_azure(): + template_library = _parse_template_library(LIBRARY_AZURE_CONTAINER) + assert len(template_library.__root__) == 1 + azure_template = template_library.__root__[0] + assert azure_template.description == "An Azure Container" + configuration_obj = template_to_configuration( + azure_template, + {"account_name": "galaxyproject"}, + {"account_key": "sec1", "container_name": "sec2"}, + user_details={}, + ) + + +def _parse_template_library(contents: str) -> ObjectStoreTemplateCatalog: + raw_contents = safe_load(contents) + return raw_config_to_catalog(raw_contents)