diff --git a/docs/guides/all/humanitec-integration.md b/docs/guides/all/humanitec-integration.md index d7bb184dcc..105cb39eb4 100644 --- a/docs/guides/all/humanitec-integration.md +++ b/docs/guides/all/humanitec-integration.md @@ -15,12 +15,21 @@ import HumanitecExporterMainScript from "/docs/guides/templates/humanitec/_human import HumanitecExporterRequirements from "/docs/guides/templates/humanitec/_humanitec_exporter_requirements.mdx"; import HumanitecExporterPortClient from "/docs/guides/templates/humanitec/_humanitec_exporter_port_client.mdx"; import HumanitecExporterHumanitecClient from "/docs/guides/templates/humanitec/_humanitec_exporter_humanitec_client.mdx"; +import HumanitecGroups from "/docs/guides/templates/humanitec/_humanitec_groups.mdx"; +import HumanitecUsers from "/docs/guides/templates/humanitec/_humanitec_users.mdx"; +import HumanitecPipelines from "/docs/guides/templates/humanitec/_humanitec_pipelines.mdx"; +import HumanitecDeploymentDeltas from "/docs/guides/templates/humanitec/_humanitec_deployment_deltas.mdx"; +import HumanitecDeploymentSets from "/docs/guides/templates/humanitec/_humanitec_deployment_sets.mdx"; +import HumanitecSecretStores from "/docs/guides/templates/humanitec/_humanitec_secret_stores.mdx"; +import HumanitecValueSetVersions from "/docs/guides/templates/humanitec/_humanitec_value_set_versions.mdx"; +import HumanitecSharedValues from "/docs/guides/templates/humanitec/_humanitec_shared_values.mdx"; + # Humanitec Integration ## Overview -In this example, you are going to create a github worklow integration to facilitate the ingestion of Humanitec applications, environments, workloads, resources and resource graphs into your port catalog on schedule +In this example, you are going to create a github worklow integration to facilitate the ingestion of Humanitec applications, environments, workloads, resources, resource graphs, pipelines, deployment deltas, deployment sets, secret stores, shared values, value set versions, users, groups into your port catalog on schedule :::info Prerequisites @@ -31,7 +40,7 @@ In this example, you are going to create a github worklow integration to facilit - `PORT_CLIENT_SECRET` - Your port `client secret` [How to get the credentials](https://docs.port.io/build-your-software-catalog/sync-data-to-catalog/api/#find-your-port-credentials). ::: -## Port blueprints +## Set up data model Create the following blueprint definitions in port: @@ -45,6 +54,23 @@ Create the following blueprint definitions in port: + + + + + + + + + + + + + + + + + :::tip Blueprint Properties You may select the blueprints depending on what you want to track in your Humanitec account. ::: @@ -54,7 +80,7 @@ You may select the blueprints depending on what you want to track in your Humani :::tip Fork our [humanitec integration repository](https://github.com/port-labs/humanitec-integration-script.git) to get started. ::: -1. Create the following Python files in a folder name `integration` folder at the root of your GitHub repository: +1. Create the following Python files in a folder named `integration` at the base directory of your GitHub repository: 1. `main.py` - Orchestrates the synchronization of data from Humanitec to Port, ensuring that resource entities are accurately mirrored and updated on your port catalog. 2. `requirements.txt` - This file contains the dependencies or necessary external packages need to run the integration @@ -146,5 +172,5 @@ jobs: -Done! Any change that happens to your application, environment, workloads or resources in Humanitec will be synced to Port on the schedule interval defined in the GitHub workflow. +Done! Any change that happens to your application, environment, workloads, resources, resource graphs, pipelines, deployment deltas, deployment sets, secret stores, shared values, value set versions, users, groups in Humanitec will be synced to Port on the schedule interval defined in the GitHub workflow. diff --git a/docs/guides/templates/humanitec/_humanitec_deployment_deltas.mdx b/docs/guides/templates/humanitec/_humanitec_deployment_deltas.mdx new file mode 100644 index 0000000000..01793efed7 --- /dev/null +++ b/docs/guides/templates/humanitec/_humanitec_deployment_deltas.mdx @@ -0,0 +1,59 @@ +
+Humanitec Deployment Deltas + +```json showLineNumbers +{ + "identifier": "humanitecDeploymentDelta", + "title": "Humanitec Deployment Delta", + "icon": "Deployment", + "schema": { + "properties": { + "status": { + "title": "Status", + "description": "The status of the deployment delta", + "type": "string", + "icon": "DefaultProperty" + }, + "createdAt": { + "title": "Created At", + "description": "The date and time when the deployment delta was created", + "type": "string", + "format": "date-time", + "icon": "DefaultProperty" + }, + "createdBy": { + "title": "Created By", + "description": "The user who created the deployment delta", + "type": "string", + "icon": "DefaultProperty" + }, + "comment": { + "title": "Comment", + "description": "Comment for the deployment delta", + "type": "string", + "icon": "DefaultProperty" + }, + "environment": { + "title": "Environment", + "description": "The environment for the deployment delta", + "type": "string", + "icon": "DefaultProperty" + } + }, + "required": [] + }, + "mirrorProperties": {}, + "calculationProperties": {}, + "aggregationProperties": {}, + "relations": { + "humanitecApplication": { + "title": "Application", + "target": "humanitecApplication", + "required": false, + "many": false + } + } +} +``` + +
\ No newline at end of file diff --git a/docs/guides/templates/humanitec/_humanitec_deployment_sets.mdx b/docs/guides/templates/humanitec/_humanitec_deployment_sets.mdx new file mode 100644 index 0000000000..56cc74f12f --- /dev/null +++ b/docs/guides/templates/humanitec/_humanitec_deployment_sets.mdx @@ -0,0 +1,53 @@ +
+Humanitec Deployment Sets + +```json showLineNumbers +{ + "identifier": "humanitecDeploymentSet", + "title": "Humanitec Deployment Set", + "icon": "Deployment", + "schema": { + "properties": { + "version": { + "title": "Version", + "description": "The version of the deployment set", + "type": "string", + "icon": "DefaultProperty" + }, + "createdAt": { + "title": "Created At", + "description": "The date and time when the deployment set was created", + "type": "string", + "format": "date-time", + "icon": "DefaultProperty" + }, + "createdBy": { + "title": "Created By", + "description": "The user who created the deployment set", + "type": "string", + "icon": "DefaultProperty" + }, + "comment": { + "title": "Comment", + "description": "Comment for the deployment set", + "type": "string", + "icon": "DefaultProperty" + } + }, + "required": [] + }, + "mirrorProperties": {}, + "calculationProperties": {}, + "aggregationProperties": {}, + "relations": { + "humanitecApplication": { + "title": "Application", + "target": "humanitecApplication", + "required": false, + "many": false + } + } +} +``` + +
diff --git a/docs/guides/templates/humanitec/_humanitec_exporter_humanitec_client.mdx b/docs/guides/templates/humanitec/_humanitec_exporter_humanitec_client.mdx index 71c0ea1c24..fe05eff343 100644 --- a/docs/guides/templates/humanitec/_humanitec_exporter_humanitec_client.mdx +++ b/docs/guides/templates/humanitec/_humanitec_exporter_humanitec_client.mdx @@ -12,7 +12,6 @@ from .cache import InMemoryCache class CACHE_KEYS: APPLICATION = "APPLICATION_CACHE_KEY" ENVIRONMENT = "ENVIRONMENT_CACHE_KEY" - WORKLOAD = "WORKLOAD_CACHE_KEY" RESOURCE = "RESOURCE_CACHE_KEY" @@ -38,7 +37,7 @@ class HumanitecClient: method: str, endpoint: str, headers: Dict[str, str] | None = None, - json: Dict[str, Any] | None = None, + json: Dict[str, Any] | List[Dict[str, Any]] | None = None, ) -> Any: url = self.base_url + endpoint try: @@ -74,95 +73,216 @@ class HumanitecClient: return applications async def get_all_environments(self, app) -> List[Dict[str, Any]]: - if cached_environments := await self.cache.get(CACHE_KEYS.ENVIRONMENT): - cached_environments = cached_environments.get(app["id"], {}) - logger.info( - f"Retrieved {len(cached_environments)} environment for {app['id']} from cache" - ) - return list(cached_environments.values()) - endpoint = f"apps/{app['id']}/envs" - humanitec_headers = self.get_humanitec_headers() - environments: List[Dict[str, Any]] = await self.send_api_request( - "GET", endpoint, headers=humanitec_headers - ) - await self.cache.set( - CACHE_KEYS.ENVIRONMENT, - { - app["id"]: { - environment["id"]: environment for environment in environments - } - }, - ) - logger.info(f"Received {len(environments)} environments from Humanitec") - return environments + try: + if cached_environments := await self.cache.get(CACHE_KEYS.ENVIRONMENT): + if app_environments := cached_environments.get(app["id"]): + logger.info( + f"Retrieved {len(app_environments)} environment for {app['id']} from cache" + ) + return list(app_environments.values()) + + logger.info("Fetching environments from Humanitec") + + endpoint = f"apps/{app['id']}/envs" + humanitec_headers = self.get_humanitec_headers() + environments: List[Dict[str, Any]] = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + await self.cache.set( + CACHE_KEYS.ENVIRONMENT, + { + app["id"]: { + environment["id"]: environment for environment in environments + } + }, + ) + logger.info(f"Received {len(environments)} environments from Humanitec") + return environments + except Exception as e: + logger.error(f"Failed to fetch environments from {app['id']}: {str(e)}") + return [] async def get_all_resources(self, app, env) -> List[Dict[str, Any]]: - if cached_resources := await self.cache.get(CACHE_KEYS.RESOURCE): - cached_resources = cached_resources.get(app["id"], {}).get(env["id"], {}) + try: + if cached_resources := await self.cache.get(CACHE_KEYS.RESOURCE): + if env_resources := cached_resources.get(app["id"], {}).get( + env["id"] + ): + logger.info( + f"Retrieved {len(env_resources)} resources from cache for app {app['id']} and env {env['id']}" + ) + return list(env_resources.values()) + + logger.info("Fetching resources from Humanitec") + endpoint = f"apps/{app['id']}/envs/{env['id']}/resources" + humanitec_headers = self.get_humanitec_headers() + resources: List[Dict[str, Any]] = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + await self.cache.set( + CACHE_KEYS.RESOURCE, + { + app["id"]: { + env["id"]: { + resource["gu_res_id"]: resource for resource in resources + } + } + }, + ) logger.info( - f"Retrieved {len(cached_resources)} resources from cache for app {app['id']} and env {env['id']}" + f"Received {len(resources)} resources for {env['id']} environment in {app['id']}" + ) + return resources + except Exception as e: + logger.error( + f"Failed to fetch resources for {env['id']} environment in {app[id]}: {str(e)}" ) - return list(cached_resources.values()) + return [] - endpoint = f"apps/{app['id']}/envs/{env['id']}/resources" - humanitec_headers = self.get_humanitec_headers() - resources: List[Dict[str, Any]] = await self.send_api_request( - "GET", endpoint, headers=humanitec_headers - ) - await self.cache.set( - CACHE_KEYS.RESOURCE, - { - app["id"]: { - env["id"]: { - resource["gu_res_id"]: resource for resource in resources - } - } - }, - ) - logger.info(f"Received {len(resources)} resources from Humanitec") - return resources + async def get_dependency_graph( + self, app: Dict[str, Any], env: Dict[str, Any] + ) -> List[Dict[str, Any]]: + try: + if dependency_graph_id := env.get("last_deploy", {}).get("dependency_graph_id"): + endpoint = f"apps/{app['id']}/envs/{env['id']}/resources/graphs/{dependency_graph_id}" + humanitec_headers = self.get_humanitec_headers() + graph = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + nodes = graph["nodes"] + logger.info( + f"Received {len(nodes)} graph nodes for {env['id']} environment in {app['id']}" + ) + return nodes + + logger.info( + f"No dependency graph found for {env['id']} environment in {app['id']}" + ) + return [] + except Exception as e: + logger.error( + f"Failed to fetch dependency graphs for {env['id']} environment in {app['id']}: {str(e)}" + ) + return [] async def get_resource_graph( - self, app: str, env: str, data: List[Dict[str, Any]] + self, app: Dict[str, Any], env: Dict[str, Any], data: List[Dict[str, Any]] ) -> Any: endpoint = f"apps/{app['id']}/envs/{env['id']}/resources/graph" humanitec_headers = self.get_humanitec_headers() graph = await self.send_api_request( "POST", endpoint, headers=humanitec_headers, json=data ) - return graph - async def get_all_resource_graphs( - self, modules: List[Dict[str, Any]], app: str, env: str - ) -> Any: - - def get_resource_graph_request_body(modules): - return [ - { - "id": module["gu_res_id"], - "type": module["type"], - "resource": module["resource"], - } - for module in modules - ] - data = get_resource_graph_request_body(modules) - - graph_entities = await self.get_resource_graph(app, env, data) - logger.info( - f"Received {len(graph_entities)} resource graph entities from app: {app['id']} and env: {env['id']} using data: {data}" - ) - return graph_entities - def group_resources_by_type( self, data: List[Dict[str, Any]] ) -> Dict[str, List[Dict[str, Any]]]: - grouped_resources = {} + grouped_resources: dict[str, Any] = {} for resource in data: workload_id = resource["res_id"].split(".")[0] if workload_id not in grouped_resources: grouped_resources[workload_id] = [] grouped_resources[workload_id].append(resource) return grouped_resources + + async def get_secret_stores(self) -> List[Dict[str, Any]]: + """Get all secret stores for the organization.""" + endpoint = "secretstores" + humanitec_headers = self.get_humanitec_headers() + secret_stores: List[Dict[str, Any]] = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + logger.info(f"Received {len(secret_stores)} secret stores from Humanitec") + return secret_stores + + async def get_shared_values(self, app: Dict[str, Any], env: Dict[str, Any]) -> List[Dict[str, Any]]: + """Get shared values for a specific environment.""" + endpoint = f"apps/{app['id']}/envs/{env['id']}/values" + humanitec_headers = self.get_humanitec_headers() + shared_values: List[Dict[str, Any]] = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + logger.info(f"Received {len(shared_values)} shared values for {env['id']} environment in {app['id']}") + return shared_values + + async def get_shared_values_app_level(self, app: Dict[str, Any]) -> List[Dict[str, Any]]: + """Get shared values at application level.""" + endpoint = f"apps/{app['id']}/values" + humanitec_headers = self.get_humanitec_headers() + shared_values: List[Dict[str, Any]] = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + logger.info(f"Received {len(shared_values)} app-level shared values for {app['id']}") + return shared_values + + async def get_value_set_versions(self, app: Dict[str, Any]) -> List[Dict[str, Any]]: + """Get value set versions for an application.""" + endpoint = f"apps/{app['id']}/value-set-versions" + humanitec_headers = self.get_humanitec_headers() + value_set_versions: List[Dict[str, Any]] = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + logger.info(f"Received {len(value_set_versions)} value set versions for {app['id']}") + return value_set_versions + + async def get_deployment_sets(self, app: Dict[str, Any]) -> List[Dict[str, Any]]: + """Get deployment sets for an application.""" + endpoint = f"apps/{app['id']}/sets" + humanitec_headers = self.get_humanitec_headers() + deployment_sets: List[Dict[str, Any]] = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + logger.info(f"Received {len(deployment_sets)} deployment sets for {app['id']}") + return deployment_sets + + async def get_pipelines(self) -> List[Dict[str, Any]]: + """Get all pipelines in the organization.""" + endpoint = "pipelines" + humanitec_headers = self.get_humanitec_headers() + pipelines: List[Dict[str, Any]] = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + logger.info(f"Received {len(pipelines)} pipelines from Humanitec") + return pipelines + + async def get_deployment_deltas(self, app: Dict[str, Any]) -> List[Dict[str, Any]]: + """Get deployment deltas for an application.""" + endpoint = f"apps/{app['id']}/deltas" + humanitec_headers = self.get_humanitec_headers() + deployment_deltas: List[Dict[str, Any]] = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + logger.info(f"Received {len(deployment_deltas)} deployment deltas for {app['id']}") + return deployment_deltas + + async def get_users_and_groups(self) -> tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: + """Get all users and groups in the organization from a single API call.""" + endpoint = "users" + humanitec_headers = self.get_humanitec_headers() + all_entities: List[Dict[str, Any]] = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + + users = [] + groups = [] + for entity in all_entities: + if entity.get("type") == "user": + users.append(entity) + elif entity.get("type") == "group": + groups.append(entity) + + logger.info(f"Received {len(users)} users and {len(groups)} groups from Humanitec") + return users, groups + + async def get_users_in_group(self, group_id: str) -> List[Dict[str, Any]]: + """Get all users in a specific group.""" + endpoint = f"groups/{group_id}/users" + humanitec_headers = self.get_humanitec_headers() + users: List[Dict[str, Any]] = await self.send_api_request( + "GET", endpoint, headers=humanitec_headers + ) + logger.info(f"Received {len(users)} users in group {group_id}") + return users ``` \ No newline at end of file diff --git a/docs/guides/templates/humanitec/_humanitec_exporter_main.mdx b/docs/guides/templates/humanitec/_humanitec_exporter_main.mdx index adf47ca3cc..7661279ba8 100644 --- a/docs/guides/templates/humanitec/_humanitec_exporter_main.mdx +++ b/docs/guides/templates/humanitec/_humanitec_exporter_main.mdx @@ -4,7 +4,7 @@ import asyncio import argparse import time import datetime -from decouple import config +from decouple import config # type: ignore import re import asyncio from loguru import logger @@ -19,12 +19,32 @@ class BLUEPRINT: WORKLOAD = "humanitecWorkload" RESOURCE_GRAPH = "humanitecResourceGraph" RESOURCE = "humanitecResource" + SECRET_STORE = "humanitecSecretStore" + SHARED_VALUE = "humanitecSharedValue" + VALUE_SET_VERSION = "humanitecValueSetVersion" + DEPLOYMENT_SET = "humanitecDeploymentSet" + PIPELINE = "humanitecPipeline" + DEPLOYMENT_DELTA = "humanitecDeploymentDelta" + USER = "humanitecUser" + GROUP = "humanitecGroup" class HumanitecExporter: - def __init__(self, port_client, humanitec_client) -> None: - self.port_client = port_client - self.humanitec_client = humanitec_client + def __init__(self, args) -> None: + + timeout = httpx.Timeout(10.0, connect=10.0, read=20.0, write=10.0) + httpx_async_client = httpx.AsyncClient(timeout=timeout) + self.port_client = PortClient( + args.port_client_id, + args.port_client_secret, + httpx_async_client=httpx_async_client, + ) + self.humanitec_client = HumanitecClient( + args.org_id, + args.api_key, + api_url=args.api_url, + httpx_async_client=httpx_async_client, + ) @staticmethod def convert_to_datetime(timestamp: int) -> str: @@ -68,7 +88,7 @@ class HumanitecExporter: def create_entity(application, environment): return { - "identifier": environment["id"], + "identifier": f"{application['id']}/{environment['id']}", "title": environment["name"], "properties": { "type": environment["type"], @@ -93,7 +113,7 @@ class HumanitecExporter: ) for application in applications for environments in [ - await humanitec_client.get_all_environments(application) + await self.humanitec_client.get_all_environments(application) ] for environment in environments ] @@ -102,9 +122,11 @@ class HumanitecExporter: async def sync_workloads(self): logger.info(f"Syncing entities for blueprint {BLUEPRINT.WORKLOAD}") - def create_workload_entity(resource): + + def create_workload_entity(resource, application, environment): + identifier = f"{application['id']}/{environment['id']}/{resource['res_id'].replace('modules.', '')}" return { - "identifier": resource["res_id"].replace("modules.", ""), + "identifier": identifier, "title": self.remove_symbols_and_title_case( resource["res_id"].replace("modules.", "") ), @@ -118,22 +140,24 @@ class HumanitecExporter: "graphResourceID": resource["gu_res_id"], }, "relations": { - BLUEPRINT.ENVIRONMENT: resource["env_id"], + BLUEPRINT.ENVIRONMENT: f"{application['id']}/{environment['id']}", }, } - applications = await humanitec_client.get_all_applications() + applications = await self.humanitec_client.get_all_applications() for application in applications: environments = await self.humanitec_client.get_all_environments(application) for environment in environments: resources = await self.humanitec_client.get_all_resources( application, environment ) - resource_group = humanitec_client.group_resources_by_type(resources) + resource_group = self.humanitec_client.group_resources_by_type( + resources + ) tasks = [ self.port_client.upsert_entity( blueprint_id=BLUEPRINT.WORKLOAD, - entity_object=create_workload_entity(resource), + entity_object=create_workload_entity(resource, application, environment), ) for resource in resource_group.get("modules", []) if resource and resource["type"] == "workload" @@ -144,7 +168,9 @@ class HumanitecExporter: async def sync_resource_graphs(self) -> None: logger.info(f"Syncing entities for blueprint {BLUEPRINT.RESOURCE_GRAPH}") - def create_resource_graph_entity(graph_data, include_relations): + def create_resource_graph_entity( + graph_data, include_relations, application, environment + ): entity = { "identifier": graph_data["guresid"], "title": self.remove_symbols_and_title_case(graph_data["def_id"]), @@ -157,8 +183,10 @@ class HumanitecExporter: "relations": {}, } if include_relations: + entity["relations"] = { - BLUEPRINT.RESOURCE_GRAPH: graph_data["depends_on"] + BLUEPRINT.RESOURCE_GRAPH: graph_data["depends_on"], + BLUEPRINT.ENVIRONMENT: f"{application['id']}/{environment['id']}", } return entity @@ -166,15 +194,7 @@ class HumanitecExporter: for application in applications: environments = await self.humanitec_client.get_all_environments(application) for environment in environments: - resources = await self.humanitec_client.get_all_resources( - application, environment - ) - resources = humanitec_client.group_resources_by_type(resources) - modules = resources.get("modules", []) - if not modules: - continue - - resource_graph = await humanitec_client.get_all_resource_graphs(modules, + graph_nodes = await self.humanitec_client.get_dependency_graph( application, environment ) @@ -183,10 +203,10 @@ class HumanitecExporter: self.port_client.upsert_entity( blueprint_id=BLUEPRINT.RESOURCE_GRAPH, entity_object=create_resource_graph_entity( - graph_data, include_relations=False + node, False, application, environment ), ) - for graph_data in resource_graph + for node in graph_nodes ] await asyncio.gather(*tasks) @@ -195,39 +215,52 @@ class HumanitecExporter: self.port_client.upsert_entity( blueprint_id=BLUEPRINT.RESOURCE_GRAPH, entity_object=create_resource_graph_entity( - graph_data, include_relations=True + node, True, application, environment ), ) - for graph_data in resource_graph + for node in graph_nodes ] await asyncio.gather(*tasks) - logger.info(f"Finished syncing entities for blueprint {BLUEPRINT.RESOURCE_GRAPH}") + logger.info( + f"Finished syncing entities for blueprint {BLUEPRINT.RESOURCE_GRAPH}" + ) async def enrich_resource_with_graph(self, resource, application, environment): - data = { - "id": resource["gu_res_id"], - "type": resource["type"], - "resource": resource["resource"], - } - response = await humanitec_client.get_resource_graph( - application, environment, [data] - ) + try: + logger.info("Enriching resource %s with graph", resource["res_id"]) + data = { + "id": resource["res_id"], + "type": resource["type"], + "resource": resource["resource"], + } + response = await self.humanitec_client.get_resource_graph( + application, environment, [data] + ) - resource.update( - {"__resourceGraph": i for i in response if i["type"] == data["type"]} - ) - return resource + resource.update( + {"__resourceGraph": i for i in response if i["type"] == data["type"]} + ) + return resource + except Exception as e: + logger.error( + f"Failed to enrich resource {resource['res_id']} with graph: %s", str(e) + ) + return resource async def sync_resources(self) -> None: logger.info(f"Syncing entities for blueprint {BLUEPRINT.RESOURCE}") + def create_resource_entity(resource): workload_id = ( resource["res_id"].split(".")[1] if resource["res_id"].split(".")[0].startswith("modules") else "" ) - return { - "identifier": resource["__resourceGraph"]["guresid"], + resource_id = ( + f"{resource['app_id']}/{resource['env_id']}/{resource['res_id']}" + ) + entity = { + "identifier": resource_id, "title": self.remove_symbols_and_title_case(resource["def_id"]), "properties": { "type": resource["type"], @@ -237,50 +270,370 @@ class HumanitecExporter: "updateAt": resource["updated_at"], "driverType": resource["driver_type"], }, - "relations": { - BLUEPRINT.RESOURCE_GRAPH: resource["__resourceGraph"]["depends_on"], - BLUEPRINT.WORKLOAD: workload_id, + "relations": {}, + } + if workload_id: + workload_id = f"{resource['app_id']}/{resource['env_id']}/{workload_id}" + entity["relations"][BLUEPRINT.WORKLOAD] = workload_id + return entity + + applications = await self.humanitec_client.get_all_applications() + for application in applications: + environments = await self.humanitec_client.get_all_environments(application) + for environment in environments: + resources = await self.humanitec_client.get_all_resources( + application, environment + ) + + entity_tasks = [ + self.port_client.upsert_entity( + blueprint_id=BLUEPRINT.RESOURCE, + entity_object=create_resource_entity(resource), + ) + for resource in resources + ] + await asyncio.gather(*entity_tasks) + logger.info( + "Upserted resource entities for %s environment", environment["id"] + ) + + logger.info(f"Finished syncing entities for blueprint {BLUEPRINT.RESOURCE}") + + async def sync_secret_stores(self) -> None: + logger.info(f"Syncing entities for blueprint {BLUEPRINT.SECRET_STORE}") + secret_stores = await self.humanitec_client.get_secret_stores() + + def create_secret_store_entity(secret_store): + # Determine the secret store type based on which configuration is present + secret_store_type = "unknown" + if secret_store.get("awssm") is not None: + secret_store_type = "AWS Secrets Manager" + elif secret_store.get("azurekv") is not None: + secret_store_type = "Azure Key Vault" + elif secret_store.get("gcpsm") is not None: + secret_store_type = "Google Cloud Secret Manager" + elif secret_store.get("humanitec") is not None: + secret_store_type = "Humanitec" + elif secret_store.get("vault") is not None: + secret_store_type = "HashiCorp Vault" + + # Create a title based on the type and ID + title = f"{secret_store_type} - {secret_store['id']}" + if secret_store.get("primary"): + title = f"{title} (Primary)" + + return { + "identifier": secret_store["id"], + "title": title, + "properties": { + "primary": secret_store.get("primary", False), + "createdAt": secret_store.get("created_at"), + "createdBy": secret_store.get("created_by"), + "updatedAt": secret_store.get("updated_at"), + "updatedBy": secret_store.get("updated_by"), + "awssm": secret_store.get("awssm"), + "azurekv": secret_store.get("azurekv"), + "gcpsm": secret_store.get("gcpsm"), + "humanitec": secret_store.get("humanitec"), + "vault": secret_store.get("vault"), }, + "relations": {}, } - async def fetch_resources(application, environment): - resources = await self.humanitec_client.get_all_resources( - application, environment + tasks = [ + self.port_client.upsert_entity( + blueprint_id=BLUEPRINT.SECRET_STORE, + entity_object=create_secret_store_entity(secret_store), ) - resources = humanitec_client.group_resources_by_type(resources) - modules = resources.get("modules", []) - if not modules: - return [] - - tasks = [ - self.enrich_resource_with_graph(resource, application, environment) - for resource in modules - ] - enriched_resources = await asyncio.gather(*tasks) - return enriched_resources + for secret_store in secret_stores + ] + + await asyncio.gather(*tasks) + logger.info(f"Finished syncing entities for blueprint {BLUEPRINT.SECRET_STORE}") + async def sync_shared_values(self) -> None: + logger.info(f"Syncing entities for blueprint {BLUEPRINT.SHARED_VALUE}") applications = await self.humanitec_client.get_all_applications() + + def create_shared_value_entity(shared_value, application, environment=None): + # Create identifier based on source and context + if environment: + identifier = f"{application['id']}/{environment['id']}/{shared_value['key']}" + else: + identifier = f"{application['id']}/{shared_value['key']}" + + + # Build relations + relations = {BLUEPRINT.APPLICATION: application["id"]} + + if environment: + relations[BLUEPRINT.ENVIRONMENT] = f"{application['id']}/{environment['id']}" + + # Add secret store relation if present + if shared_value.get("secret_store_id"): + relations[BLUEPRINT.SECRET_STORE] = shared_value["secret_store_id"] + + return { + "identifier": identifier, + "title": shared_value["key"], + "properties": { + "description": shared_value.get("description"), + "isSecret": shared_value.get("is_secret", False), + "key": shared_value.get("key"), + "secretKey": shared_value.get("secret_key"), + "secretVersion": shared_value.get("secret_version"), + "source": shared_value.get("source"), + "value": shared_value.get("value"), + "createdAt": shared_value.get("created_at"), + "updatedAt": shared_value.get("updated_at"), + }, + "relations": relations, + } + + # Sync app-level shared values + app_level_tasks = [] + for application in applications: + shared_values = await self.humanitec_client.get_shared_values_app_level(application) + app_level_tasks.extend([ + self.port_client.upsert_entity( + blueprint_id=BLUEPRINT.SHARED_VALUE, + entity_object=create_shared_value_entity(shared_value, application), + ) + for shared_value in shared_values + ]) + + # Sync environment-level shared values + env_level_tasks = [] for application in applications: environments = await self.humanitec_client.get_all_environments(application) + for environment in environments: + shared_values = await self.humanitec_client.get_shared_values(application, environment) + env_level_tasks.extend([ + self.port_client.upsert_entity( + blueprint_id=BLUEPRINT.SHARED_VALUE, + entity_object=create_shared_value_entity(shared_value, application, environment), + ) + for shared_value in shared_values + ]) - resource_tasks = [ - fetch_resources(application, environment) - for environment in environments - ] - all_resources = await asyncio.gather(*resource_tasks) - all_resources = [ - resource for sublist in all_resources for resource in sublist - ] # Flatten the list + await asyncio.gather(*(app_level_tasks + env_level_tasks)) + logger.info(f"Finished syncing entities for blueprint {BLUEPRINT.SHARED_VALUE}") + + async def sync_value_set_versions(self) -> None: + logger.info(f"Syncing entities for blueprint {BLUEPRINT.VALUE_SET_VERSION}") + applications = await self.humanitec_client.get_all_applications() + + def create_value_set_version_entity(value_set_version, application): + return { + "identifier": f"{application['id']}/{value_set_version['id']}", + "title": f"Value Set Version {value_set_version['id']}", + "properties": { + "version": value_set_version.get("version"), + "createdAt": value_set_version.get("created_at"), + "createdBy": value_set_version.get("created_by"), + "comment": value_set_version.get("comment"), + }, + "relations": {BLUEPRINT.APPLICATION: application["id"]}, + } - entity_tasks = [ + tasks = [] + for application in applications: + value_set_versions = await self.humanitec_client.get_value_set_versions(application) + tasks.extend([ self.port_client.upsert_entity( - blueprint_id=BLUEPRINT.RESOURCE, - entity_object=create_resource_entity(resource), + blueprint_id=BLUEPRINT.VALUE_SET_VERSION, + entity_object=create_value_set_version_entity(value_set_version, application), ) - for resource in all_resources - ] - await asyncio.gather(*entity_tasks) - logger.info(f"Finished syncing entities for blueprint {BLUEPRINT.RESOURCE}") + for value_set_version in value_set_versions + ]) + + await asyncio.gather(*tasks) + logger.info(f"Finished syncing entities for blueprint {BLUEPRINT.VALUE_SET_VERSION}") + + async def sync_deployment_sets(self) -> None: + logger.info(f"Syncing entities for blueprint {BLUEPRINT.DEPLOYMENT_SET}") + applications = await self.humanitec_client.get_all_applications() + + def create_deployment_set_entity(deployment_set, application): + return { + "identifier": f"{application['id']}/{deployment_set['id']}", + "title": self.remove_symbols_and_title_case(deployment_set.get("name", deployment_set["id"])), + "properties": { + "version": deployment_set.get("version"), + "createdAt": deployment_set.get("created_at"), + "createdBy": deployment_set.get("created_by"), + "comment": deployment_set.get("comment"), + }, + "relations": {BLUEPRINT.APPLICATION: application["id"]}, + } + + tasks = [] + for application in applications: + deployment_sets = await self.humanitec_client.get_deployment_sets(application) + tasks.extend([ + self.port_client.upsert_entity( + blueprint_id=BLUEPRINT.DEPLOYMENT_SET, + entity_object=create_deployment_set_entity(deployment_set, application), + ) + for deployment_set in deployment_sets + ]) + + await asyncio.gather(*tasks) + logger.info(f"Finished syncing entities for blueprint {BLUEPRINT.DEPLOYMENT_SET}") + + async def sync_pipelines(self) -> None: + logger.info(f"Syncing entities for blueprint {BLUEPRINT.PIPELINE}") + pipelines = await self.humanitec_client.get_pipelines() + + # Get cached applications to map pipeline to app names + applications = await self.humanitec_client.get_all_applications() + app_map = {app["id"]: app for app in applications} + + def create_pipeline_entity(pipeline): + app_id = pipeline.get("app_id") + app_name = app_map.get(app_id, {}).get("name", "Unknown App") + + # Create identifier that includes app context + identifier = f"{app_id}/{pipeline['id']}" + + # Create title that includes app name and pipeline name + pipeline_name = pipeline.get("name", pipeline["id"]) + title = f"{app_name} - {pipeline_name}" + + return { + "identifier": identifier, + "title": title, + "properties": { + "etag": pipeline.get("etag"), + "name": pipeline.get("name"), + "status": pipeline.get("status"), + "version": pipeline.get("version"), + "createdAt": pipeline.get("created_at"), + "triggerTypes": pipeline.get("trigger_types", []), + "metadata": pipeline.get("metadata", {}), + }, + "relations": { + BLUEPRINT.APPLICATION: app_id + } if app_id else {}, + } + + tasks = [ + self.port_client.upsert_entity( + blueprint_id=BLUEPRINT.PIPELINE, + entity_object=create_pipeline_entity(pipeline), + ) + for pipeline in pipelines + ] + + await asyncio.gather(*tasks) + logger.info(f"Finished syncing entities for blueprint {BLUEPRINT.PIPELINE}") + + async def sync_deployment_deltas(self) -> None: + logger.info(f"Syncing entities for blueprint {BLUEPRINT.DEPLOYMENT_DELTA}") + applications = await self.humanitec_client.get_all_applications() + + def create_deployment_delta_entity(deployment_delta, application): + return { + "identifier": f"{application['id']}/{deployment_delta['id']}", + "title": self.remove_symbols_and_title_case(deployment_delta.get("name", deployment_delta["id"])), + "properties": { + "status": deployment_delta.get("status"), + "createdAt": deployment_delta.get("created_at"), + "createdBy": deployment_delta.get("created_by"), + "comment": deployment_delta.get("comment"), + "environment": deployment_delta.get("environment"), + }, + "relations": {BLUEPRINT.APPLICATION: application["id"]}, + } + + tasks = [] + for application in applications: + deployment_deltas = await self.humanitec_client.get_deployment_deltas(application) + tasks.extend([ + self.port_client.upsert_entity( + blueprint_id=BLUEPRINT.DEPLOYMENT_DELTA, + entity_object=create_deployment_delta_entity(deployment_delta, application), + ) + for deployment_delta in deployment_deltas + ]) + + await asyncio.gather(*tasks) + logger.info(f"Finished syncing entities for blueprint {BLUEPRINT.DEPLOYMENT_DELTA}") + + async def sync_users_and_groups(self) -> None: + logger.info(f"Syncing entities for blueprints {BLUEPRINT.USER} and {BLUEPRINT.GROUP}") + + all_users, all_groups = await self.humanitec_client.get_users_and_groups() + + user_groups = {} + + for user in all_users: + user_groups[user["id"]] = [] + + group_tasks = [ + self.humanitec_client.get_users_in_group(group["id"]) + for group in all_groups + ] + + group_results = await asyncio.gather(*group_tasks, return_exceptions=True) + + for i, result in enumerate(group_results): + group_id = all_groups[i]["id"] + + if isinstance(result, Exception): + logger.error(f"Failed to get users for group {group_id}: {str(result)}") + continue + + for user in result: + user_id = user["id"] + if user_id in user_groups: + user_groups[user_id].append(group_id) + + def create_group_entity(group): + return { + "identifier": group["id"], + "title": group["name"], + "properties": { + "role": group.get("role"), + "idp_id": group.get("idp_id"), + "createdAt": group.get("created_at"), + }, + "relations": {}, + } + + def create_user_entity(user): + return { + "identifier": user["id"], + "title": user["name"], + "properties": { + "email": user.get("email"), + "role": user.get("role"), + "invite": user.get("invite"), + "createdAt": user.get("created_at"), + }, + "relations": { + BLUEPRINT.GROUP: user_groups.get(user["id"], []) + }, + } + + group_tasks = [ + self.port_client.upsert_entity( + blueprint_id=BLUEPRINT.GROUP, + entity_object=create_group_entity(group), + ) + for group in all_groups + ] + + user_tasks = [ + self.port_client.upsert_entity( + blueprint_id=BLUEPRINT.USER, + entity_object=create_user_entity(user), + ) + for user in all_users + ] + + await asyncio.gather(*(group_tasks + user_tasks)) + logger.info(f"Finished syncing {len(all_groups)} groups and {len(all_users)} users") async def sync_all(self) -> None: await self.sync_applications() @@ -288,6 +641,13 @@ class HumanitecExporter: await self.sync_workloads() await self.sync_resource_graphs() await self.sync_resources() + await self.sync_secret_stores() + await self.sync_shared_values() + await self.sync_value_set_versions() + await self.sync_deployment_sets() + await self.sync_pipelines() + await self.sync_deployment_deltas() + await self.sync_users_and_groups() logger.info("Event Finished") async def __call__(self, args) -> None: @@ -299,7 +659,7 @@ if __name__ == "__main__": def validate_args(args): required_keys = ["org_id", "api_key", "port_client_id", "port_client_secret"] missing_keys = [key for key in required_keys if not getattr(args, key)] - + if missing_keys: logger.error(f"The following keys are required: {', '.join(missing_keys)}") return False @@ -307,39 +667,46 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( - "--org-id", required=False,default=config("ORG_ID",""), type=str, help="Humanitec organization ID" + "--org-id", + required=False, + default=config("ORG_ID", ""), + type=str, + help="Humanitec organization ID", + ) + parser.add_argument( + "--api-key", + required=False, + default=config("API_KEY", ""), + type=str, + help="Humanitec API key", ) - parser.add_argument("--api-key", required=False,default=config("API_KEY",""), type=str, help="Humanitec API key") parser.add_argument( "--api-url", type=str, - default=config("API_URL","https://api.humanitec.com"), + default=config("API_URL", "https://api.humanitec.com"), help="Humanitec API URL", ) parser.add_argument( - "--port-client-id", type=str, required=False,default=config("PORT_CLIENT_ID",""), help="Port client ID" + "--port-client-id", + type=str, + required=False, + default=config("PORT_CLIENT_ID", ""), + help="Port client ID", ) parser.add_argument( - "--port-client-secret", type=str, required=False,default = config("PORT_CLIENT_SECRET",""), help="Port client secret" + "--port-client-secret", + type=str, + required=False, + default=config("PORT_CLIENT_SECRET", ""), + help="Port client secret", ) args = parser.parse_args() - if not(validate_args(args)): + if not (validate_args(args)): import sys + sys.exit() - httpx_async_client = httpx.AsyncClient() - port_client = PortClient( - args.port_client_id, - args.port_client_secret, - httpx_async_client=httpx_async_client, - ) - humanitec_client = HumanitecClient( - args.org_id, - args.api_key, - api_url=args.api_url, - httpx_async_client=httpx_async_client, - ) - exporter = HumanitecExporter(port_client, humanitec_client) + exporter = HumanitecExporter(args) asyncio.run(exporter(args)) ``` \ No newline at end of file diff --git a/docs/guides/templates/humanitec/_humanitec_exporter_port_client.mdx b/docs/guides/templates/humanitec/_humanitec_exporter_port_client.mdx index b69926a739..1bcc05be56 100644 --- a/docs/guides/templates/humanitec/_humanitec_exporter_port_client.mdx +++ b/docs/guides/templates/humanitec/_humanitec_exporter_port_client.mdx @@ -3,15 +3,13 @@ import httpx from typing import Any, Dict from loguru import logger -from typing import List, Dict, Optional, Union -from .cache import InMemoryCache +from typing import Dict class PortClient: def __init__(self, client_id, client_secret, **kwargs) -> None: self.httpx_async_client = kwargs.get("httpx_async_client", httpx.AsyncClient()) self.client_id = client_id - self.cache = InMemoryCache() self.client_secret = client_secret self.base_url = kwargs.get("base_url", "https://api.getport.io/v1") self.port_headers = None @@ -51,7 +49,7 @@ class PortClient: async def upsert_entity( self, blueprint_id: str, entity_object: Dict[str, Any] - ) -> None: + ) -> Dict[str, Any]: endpoint = f"/blueprints/{blueprint_id}/entities?upsert=true&merge=true" port_headers = ( self.port_headers if self.port_headers else await self.get_port_headers() diff --git a/docs/guides/templates/humanitec/_humanitec_groups.mdx b/docs/guides/templates/humanitec/_humanitec_groups.mdx new file mode 100644 index 0000000000..37896e4adc --- /dev/null +++ b/docs/guides/templates/humanitec/_humanitec_groups.mdx @@ -0,0 +1,40 @@ +
+Humanitec Groups + +```json showLineNumbers +{ + "identifier": "humanitecGroup", + "title": "Humanitec Group", + "icon": "TwoUsers", + "schema": { + "properties": { + "role": { + "title": "Role", + "description": "The role of the group in the organization", + "type": "string", + "icon": "Role" + }, + "idp_id": { + "title": "IDP ID", + "description": "The identity provider ID", + "type": "string", + "icon": "DefaultProperty" + }, + "createdAt": { + "title": "Created At", + "description": "The date and time when the group was created", + "type": "string", + "format": "date-time", + "icon": "DefaultProperty" + } + }, + "required": [] + }, + "mirrorProperties": {}, + "calculationProperties": {}, + "aggregationProperties": {}, + "relations": {} +} +``` + +
diff --git a/docs/guides/templates/humanitec/_humanitec_pipelines.mdx b/docs/guides/templates/humanitec/_humanitec_pipelines.mdx new file mode 100644 index 0000000000..070272bbbd --- /dev/null +++ b/docs/guides/templates/humanitec/_humanitec_pipelines.mdx @@ -0,0 +1,71 @@ +
+Humanitec Pipelines + +```json showLineNumbers +{ + "identifier": "humanitecPipeline", + "title": "Humanitec Pipeline", + "icon": "Pipeline", + "schema": { + "properties": { + "etag": { + "title": "ETag", + "description": "Entity tag for the pipeline", + "type": "string", + "icon": "DefaultProperty" + }, + "name": { + "title": "Name", + "description": "The name of the pipeline", + "type": "string", + "icon": "DefaultProperty" + }, + "status": { + "title": "Status", + "description": "The status of the pipeline", + "type": "string", + "icon": "DefaultProperty" + }, + "version": { + "title": "Version", + "description": "The version of the pipeline", + "type": "string", + "icon": "DefaultProperty" + }, + "createdAt": { + "title": "Created At", + "description": "The date and time when the pipeline was created", + "type": "string", + "format": "date-time", + "icon": "DefaultProperty" + }, + "triggerTypes": { + "title": "Trigger Types", + "description": "Types of triggers for the pipeline", + "type": "array", + "icon": "DefaultProperty" + }, + "metadata": { + "title": "Metadata", + "description": "Additional metadata for the pipeline", + "type": "object", + "icon": "DefaultProperty" + } + }, + "required": [] + }, + "mirrorProperties": {}, + "calculationProperties": {}, + "aggregationProperties": {}, + "relations": { + "humanitecApplication": { + "title": "Application", + "target": "humanitecApplication", + "required": false, + "many": false + } + } +} +``` + +
diff --git a/docs/guides/templates/humanitec/_humanitec_secret_stores.mdx b/docs/guides/templates/humanitec/_humanitec_secret_stores.mdx new file mode 100644 index 0000000000..20ee3c9a1d --- /dev/null +++ b/docs/guides/templates/humanitec/_humanitec_secret_stores.mdx @@ -0,0 +1,83 @@ +
+Humanitec Secret Stores + +```json showLineNumbers +{ + "identifier": "humanitecSecretStore", + "title": "Humanitec Secret Store", + "icon": "Lock", + "schema": { + "properties": { + "primary": { + "title": "Primary", + "description": "Whether this is the primary secret store", + "type": "boolean", + "icon": "DefaultProperty" + }, + "createdAt": { + "title": "Created At", + "description": "The date and time when the secret store was created", + "type": "string", + "format": "date-time", + "icon": "DefaultProperty" + }, + "createdBy": { + "title": "Created By", + "description": "The user who created the secret store", + "type": "string", + "icon": "DefaultProperty" + }, + "updatedAt": { + "title": "Updated At", + "description": "The date and time when the secret store was last updated", + "type": "string", + "format": "date-time", + "icon": "DefaultProperty" + }, + "updatedBy": { + "title": "Updated By", + "description": "The user who last updated the secret store", + "type": "string", + "icon": "DefaultProperty" + }, + "awssm": { + "title": "AWS Secrets Manager", + "description": "AWS Secrets Manager configuration", + "type": "object", + "icon": "DefaultProperty" + }, + "azurekv": { + "title": "Azure Key Vault", + "description": "Azure Key Vault configuration", + "type": "object", + "icon": "DefaultProperty" + }, + "gcpsm": { + "title": "Google Cloud Secret Manager", + "description": "Google Cloud Secret Manager configuration", + "type": "object", + "icon": "DefaultProperty" + }, + "humanitec": { + "title": "Humanitec", + "description": "Humanitec secret store configuration", + "type": "object", + "icon": "DefaultProperty" + }, + "vault": { + "title": "Vault", + "description": "HashiCorp Vault configuration", + "type": "object", + "icon": "DefaultProperty" + } + }, + "required": [] + }, + "mirrorProperties": {}, + "calculationProperties": {}, + "aggregationProperties": {}, + "relations": {} +} +``` + +
diff --git a/docs/guides/templates/humanitec/_humanitec_shared_values.mdx b/docs/guides/templates/humanitec/_humanitec_shared_values.mdx new file mode 100644 index 0000000000..7501597479 --- /dev/null +++ b/docs/guides/templates/humanitec/_humanitec_shared_values.mdx @@ -0,0 +1,90 @@ +
+Humanitec Shared Values + +```json showLineNumbers +{ + "identifier": "humanitecSharedValue", + "title": "Humanitec Shared Value", + "icon": "Settings", + "schema": { + "properties": { + "description": { + "title": "Description", + "description": "A human friendly description of what the Shared Value is", + "type": "string", + "icon": "DefaultProperty" + }, + "isSecret": { + "title": "Is Secret", + "description": "Specified that the Shared Value contains a secret", + "type": "boolean", + "icon": "DefaultProperty" + }, + "secretKey": { + "title": "Secret Key", + "description": "Location of the secret value in the secret store", + "type": "string", + "icon": "DefaultProperty" + }, + "secretVersion": { + "title": "Secret Version", + "description": "Version of the current secret value as returned by the secret store", + "type": "string", + "icon": "DefaultProperty" + }, + "source": { + "title": "Source", + "description": "Source of the value, 'app' for app level, 'env' for app env level", + "type": "string", + "icon": "DefaultProperty" + }, + "value": { + "title": "Value", + "description": "The value that will be stored (will be always empty for secrets)", + "type": "string", + "icon": "DefaultProperty" + }, + "createdAt": { + "title": "Created At", + "description": "The date and time when the shared value was created", + "type": "string", + "format": "date-time", + "icon": "DefaultProperty" + }, + "updatedAt": { + "title": "Updated At", + "description": "The date and time when the shared value was last updated", + "type": "string", + "format": "date-time", + "icon": "DefaultProperty" + } + }, + "required": [] + }, + "mirrorProperties": {}, + "calculationProperties": {}, + "aggregationProperties": {}, + "relations": { + "humanitecApplication": { + "title": "Application", + "target": "humanitecApplication", + "required": false, + "many": false + }, + "humanitecEnvironment": { + "title": "Environment", + "target": "humanitecEnvironment", + "required": false, + "many": false + }, + "humanitecSecretStore": { + "title": "Secret Store", + "target": "humanitecSecretStore", + "required": false, + "many": false + } + } +} +``` + +
diff --git a/docs/guides/templates/humanitec/_humanitec_users.mdx b/docs/guides/templates/humanitec/_humanitec_users.mdx new file mode 100644 index 0000000000..9b77485557 --- /dev/null +++ b/docs/guides/templates/humanitec/_humanitec_users.mdx @@ -0,0 +1,54 @@ +
+Humanitec Users + +```json showLineNumbers +{ + "identifier": "humanitecUser", + "title": "Humanitec User", + "icon": "User", + "schema": { + "properties": { + "email": { + "title": "Email", + "description": "The email address of the user", + "type": "string", + "icon": "User", + "format": "user" + }, + "role": { + "title": "Role", + "description": "The role of the user in the organization", + "type": "string", + "icon": "Role" + }, + "invite": { + "title": "Invite Status", + "description": "The status of the user's invitation", + "type": "string", + "icon": "DefaultProperty" + }, + "createdAt": { + "title": "Created At", + "description": "The date and time when the user was created", + "type": "string", + "format": "date-time", + "icon": "DefaultProperty" + } + }, + "required": [] + }, + "mirrorProperties": {}, + "calculationProperties": {}, + "aggregationProperties": {}, + "relations": { + "humanitecGroup": { + "title": "Groups", + "target": "humanitecGroup", + "required": false, + "many": true + } + } +} +``` + +
diff --git a/docs/guides/templates/humanitec/_humanitec_value_set_versions.mdx b/docs/guides/templates/humanitec/_humanitec_value_set_versions.mdx new file mode 100644 index 0000000000..d28b6dd7d4 --- /dev/null +++ b/docs/guides/templates/humanitec/_humanitec_value_set_versions.mdx @@ -0,0 +1,53 @@ +
+Humanitec Value Set Versions + +```json showLineNumbers +{ + "identifier": "humanitecValueSetVersion", + "title": "Humanitec Value Set Version", + "icon": "Version", + "schema": { + "properties": { + "version": { + "title": "Version", + "description": "The version number", + "type": "string", + "icon": "DefaultProperty" + }, + "createdAt": { + "title": "Created At", + "description": "The date and time when the value set version was created", + "type": "string", + "format": "date-time", + "icon": "DefaultProperty" + }, + "createdBy": { + "title": "Created By", + "description": "The user who created the value set version", + "type": "string", + "icon": "DefaultProperty" + }, + "comment": { + "title": "Comment", + "description": "Comment for the value set version", + "type": "string", + "icon": "DefaultProperty" + } + }, + "required": [] + }, + "mirrorProperties": {}, + "calculationProperties": {}, + "aggregationProperties": {}, + "relations": { + "humanitecApplication": { + "title": "Application", + "target": "humanitecApplication", + "required": false, + "many": false + } + } +} +``` + +