diff --git a/contxt/cli/clients.py b/contxt/cli/clients.py index 88efbe6f..982cc692 100644 --- a/contxt/cli/clients.py +++ b/contxt/cli/clients.py @@ -22,6 +22,10 @@ class Clients: env: str org_slug: Optional[str] + @cachedproperty + def accessible_orgs(self) -> str: + return self.contxt.get("organizations") + @cachedproperty def org_id(self) -> str: if not self.org_slug: diff --git a/contxt/cli/commands/find.py b/contxt/cli/commands/find.py new file mode 100644 index 00000000..07c76272 --- /dev/null +++ b/contxt/cli/commands/find.py @@ -0,0 +1,62 @@ +import click + +from contxt.cli.clients import Clients +from contxt.cli.utils import print_table +from contxt.utils.serializer import Serializer + + +@click.group() +def find() -> None: + """Search functions""" + + +@find.command("client") +@click.argument("CLIENT_ID") +@click.pass_obj +def client(clients: Clients, client_id: str) -> None: + """Find a service instance by its Client ID""" + print( + f"Searching all accessible orgs ({','.join([x.get('slug') for x in clients.accessible_orgs])})" + ) + for org in clients.accessible_orgs: + try: + instances = clients.contxt_deployments.get_service_instances(org.get("id")) + edges = clients.contxt_deployments.get_edge_nodes(org.get("id")) + instances.extend(edges) + for inst in instances: + client = inst.client_id + if client: + if client.find(client_id) > -1: + print(f'Found match in org {org.get("slug")}') + print(Serializer.to_pretty_cli(inst)) + except Exception: + pass + + +@find.command("by-name") +@click.argument("NAME_MATCH") +@click.pass_obj +def by_name(clients: Clients, name_match: str) -> None: + """Find a service instance by its name""" + print( + f"Searching all accessible orgs ({','.join([x.get('slug') for x in clients.accessible_orgs])})" + ) + hits = [] + for org in clients.accessible_orgs: + try: + instances = clients.contxt_deployments.get_service_instances(org.get("id")) + edges = clients.contxt_deployments.get_edge_nodes(org.get("id")) + instances.extend(edges) + for inst in instances: + name = inst.name + if name: + name = name.lower() + if name.find(name_match.lower()) > -1: + match = inst.__dict__ + match["type"] = str(type(inst)) + match["org"] = org.get("slug") + hits.append(match) + except Exception: + pass + + print_table(hits, keys=["org", "type", "id", "name", "description", "client_id"]) diff --git a/contxt/cli/commands/service_instances.py b/contxt/cli/commands/service_instances.py index e3f112cb..6c66567a 100644 --- a/contxt/cli/commands/service_instances.py +++ b/contxt/cli/commands/service_instances.py @@ -1,10 +1,11 @@ +import sys from typing import List, Optional import click from contxt.cli.clients import Clients from contxt.cli.utils import OPTIONAL_PROMPT_KWARGS, fields_option, print_item, print_table, sort_option -from contxt.models.contxt import ServiceInstance +from contxt.models.contxt import ServiceInstance, ServiceInstanceGrant, ServiceInstanceScope @click.group() @@ -13,17 +14,23 @@ def service_instances() -> None: @service_instances.command() +@click.argument("SERVICE_INSTANCE_ID", type=int, required=False) @click.option("--service-id") @click.pass_obj @fields_option(default="id, slug, service_id, description", obj=ServiceInstance) @sort_option(default="slug") -def get(clients: Clients, service_id: Optional[str], fields: List[str], sort: str) -> None: +def get( + clients: Clients, service_instance_id: int, service_id: Optional[str], fields: List[str], sort: str +) -> None: """Get service instance(s)""" - items = ( - clients.contxt_deployments.get(f"{clients.org_id}/services/{service_id}/service_instances") - if service_id - else clients.contxt_deployments.get(f"{clients.org_id}/service_instances") - ) + if service_instance_id: + items = [clients.contxt_deployments.get_service_instance(clients.org_id, service_instance_id)] + else: + items = ( + clients.contxt_deployments.get(f"{clients.org_id}/services/{service_id}/service_instances") + if service_id + else clients.contxt_deployments.get(f"{clients.org_id}/service_instances") + ) print_table(items, keys=fields, sort_by=sort) @@ -65,3 +72,107 @@ def create( def delete(clients: Clients, id: str) -> None: """Delete service instance""" clients.contxt_deployments.delete(f"{clients.org_id}/service_instances/{id}") + + +""" +Scopes Commands +""" + + +@service_instances.command("scopes") +@click.argument("SERVICE_INSTANCE_ID") +@fields_option(default="label, description", obj=ServiceInstanceScope) +@sort_option(default="label") +@click.pass_obj +def scopes(clients: Clients, service_instance_id: str, fields: List[str], sort: str) -> None: + """Get scopes for a specific service instance""" + scopes = clients.contxt_deployments.get_service_instance_scopes(clients.org_id, service_instance_id) + print_table(items=scopes, keys=fields, sort_by=sort) + + +""" +Dependency Commands +""" + + +@service_instances.command("deps") +@click.argument("SERVICE_INSTANCE_ID") +@click.pass_obj +def get_dependencies(clients: Clients, service_instance_id: str) -> None: + """Get dependencies for a specific service instance""" + dependencies = clients.contxt_deployments.get_service_instance_dependencies( + clients.org_id, service_instance_id + ) + objs = [] + for dep in dependencies: + to_service = clients.contxt_deployments.get_service_instance( + clients.org_id, dep.to_service_instance_id + ) + if len(dep.ServiceInstanceScopes): + for row in dep.ServiceInstanceScopes: + objs.append( + { + "to_service_instance_id": dep.to_service_instance_id, + "to_service_instance_name": to_service.name, + "scope": row.label, + "description": row.description, + } + ) + else: + objs.append( + { + "to_service_instance_id": dep.to_service_instance_id, + "to_service_instance_name": to_service.name, + "scope": "", + "description": "", + } + ) + print_table( + items=objs, + keys=["to_service_instance_id", "to_service_instance_name", "scope", "description"], + sort_by="sort", + ) + + +@service_instances.command("add-dep") +@click.option("--from-id", help="From Service ID", required=True) +@click.option("--to-id", help="To Service ID", required=True) +@click.pass_obj +def add_dep(clients: Clients, from_id: int, to_id: int) -> None: + """Add a new dependency to a service instance""" + from_service = clients.contxt_deployments.get_service_instance(clients.org_id, from_id) + to_service = clients.contxt_deployments.get_service_instance(clients.org_id, to_id) + + print(f"Creating dependency between {from_service.name} -> {to_service.name}") + grant = ServiceInstanceGrant( + from_service_instance_id=from_service.id, to_service_instance_id=to_service.id + ) + dep = clients.contxt_deployments.create_service_dependency(clients.org_id, grant) + print(dep) + + +@service_instances.command("rm-dep") +@click.option("--from-id", help="From Service ID", required=True) +@click.option("--to-id", help="To Service ID", required=True) +@click.pass_obj +def remove_dep(clients: Clients, from_id: int, to_id: int) -> None: + """Remove an existing dependency from a service instance""" + from_service = clients.contxt_deployments.get_service_instance(clients.org_id, from_id) + existing_deps = clients.contxt_deployments.get_service_instance_dependencies(clients.org_id, from_id) + + target_dep = None + for dep in existing_deps: + print(dep) + if dep.to_service_instance_id == to_id: + target_dep = dep + break + + if not target_dep: + print( + f"Service Instance with ID {to_id} is not currently a dependency " + f"of the service with ID {from_id} ({from_service.name})" + ) + sys.exit(0) + + clients.contxt_deployments.remove_service_dependency(clients.org_id, from_id, to_id) + print("Successfully removed dependency") diff --git a/contxt/models/contxt.py b/contxt/models/contxt.py index 95589431..1c5d94c0 100644 --- a/contxt/models/contxt.py +++ b/contxt/models/contxt.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from datetime import datetime from json import loads -from typing import ClassVar, List, Optional +from typing import Any, ClassVar, List, Optional from . import ApiField, ApiObject, Parsers @@ -282,38 +282,40 @@ class Service(ApiObject): @dataclass class ServiceInstance(ApiObject): _api_fields: ClassVar = ( - ApiField("id", data_type=int), + ApiField("id", data_type=int, optional=True), ApiField("name"), - ApiField("slug"), - ApiField("description"), - ApiField("descriptor"), - ApiField("service_id", data_type=int), - ApiField("project_environment_id"), + ApiField("organization_id"), + ApiField("slug", optional=True), + ApiField("description", optional=True), + ApiField("descriptor", optional=True), + ApiField("service_id", data_type=int, optional=True), + ApiField("project_environment_id", optional=True), ApiField("client_id"), - ApiField("command"), - ApiField("arguments"), - ApiField("last_deployed_at"), - ApiField("last_configured_at"), + ApiField("command", optional=True), + ApiField("arguments", optional=True), + ApiField("last_deployed_at", optional=True), + ApiField("last_configured_at", optional=True), ApiField("service_env_variables", data_type=ServiceEnvironmentVariable, optional=True), ApiField("frontend", data_type=Frontend, optional=True), ApiField("image", data_type=Image, optional=True), - ApiField("service_type"), - ApiField("created_at", data_type=Parsers.datetime), + ApiField("service_type", optional=True), + ApiField("created_at", data_type=Parsers.datetime, optional=True), ) - id: int name: str - slug: str - description: str - descriptor: str - service_id: int - project_environment_id: str client_id: str - command: str - arguments: str - last_deployed_at: Optional[datetime] - last_configured_at: Optional[datetime] - service_type: str + organization_id: str + id: Optional[int] = None + slug: Optional[str] = None + description: Optional[str] = None + descriptor: Optional[str] = None + service_id: Optional[int] = None + project_environment_id: Optional[str] = None + command: Optional[str] = None + arguments: Optional[str] = None + last_deployed_at: Optional[datetime] = None + last_configured_at: Optional[datetime] = None + service_type: Optional[str] = None service_env_variables: Optional[List[ServiceEnvironmentVariable]] = None frontend: Optional[Frontend] = None image: Optional[Image] = None @@ -372,18 +374,18 @@ class ProjectEnvironment(ApiObject): @dataclass class EdgeNode(ApiObject): _api_fields: ClassVar = ( - ApiField("id", data_type=int), + ApiField("id", data_type=str), ApiField("name"), - ApiField("stack_id"), + ApiField("project_id"), ApiField("organization_id"), ApiField("description"), ApiField("client_id"), ApiField("created_at", data_type=Parsers.datetime), ) - id: int + id: str name: str - stack_id: int + project_id: int organization_id: str description: str client_id: str @@ -417,3 +419,62 @@ class Cluster(ApiObject): certificate_authority: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None + + +@dataclass +class ServiceInstanceScope(ApiObject): + _api_fields: ClassVar = ( + ApiField("id"), + ApiField("service_instance_id"), + ApiField("label"), + ApiField("service_instance_grant_scopes", optional=True), + ApiField("description"), + ApiField("created_at"), + ApiField("updated_at"), + ) + + id: str + service_instance_id: int + label: str + description: str + service_instance_grant_scopes: Optional[Any] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +@dataclass +class ServiceInstanceGrant(ApiObject): + _api_fields: ClassVar = ( + ApiField("id"), + ApiField("from_service_instance_id", creatable=True), + ApiField("to_service_instance_id", creatable=True), + ApiField("auth0_id"), + ApiField("ServiceInstanceScopes", data_type=ServiceInstanceScope, optional=True), + ApiField("created_at", data_type=Parsers.datetime), + ApiField("updated_at", data_type=Parsers.datetime), + ) + + from_service_instance_id: int + to_service_instance_id: int + auth0_id: Optional[str] = None + id: Optional[str] = None + ServiceInstanceScopes: Optional[List[ServiceInstanceScope]] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +@dataclass +class ServiceInstanceGrantScope(ApiObject): + _api_fields: ClassVar = ( + ApiField("id"), + ApiField("service_instance_grant_id"), + ApiField("service_instance_scope_id"), + ApiField("created_at", data_type=Parsers.datetime), + ApiField("updated_at", data_type=Parsers.datetime), + ) + + id: str + service_instance_grant_id: str + service_instance_scope_id: str + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None diff --git a/contxt/services/contxt_deployments.py b/contxt/services/contxt_deployments.py index 5b528021..aeba920d 100644 --- a/contxt/services/contxt_deployments.py +++ b/contxt/services/contxt_deployments.py @@ -3,7 +3,14 @@ from requests.exceptions import HTTPError from ..auth import Auth -from ..models.contxt import Cluster +from ..models.contxt import ( + Cluster, + EdgeNode, + ServiceInstance, + ServiceInstanceGrant, + ServiceInstanceGrantScope, + ServiceInstanceScope, +) from .api import ApiEnvironment, ConfiguredApi @@ -41,3 +48,73 @@ def register_cluster(self, organization_id: str, cluster: Cluster) -> None: except HTTPError: pass return + + """ + Edge Nodes + """ + + def get_edge_nodes(self, organization_id: str) -> List[EdgeNode]: + resp = self.get(f"{organization_id}/edgenodes") + return [EdgeNode.from_api(row) for row in resp] + + """ + Service Instances + """ + + def get_service_instance(self, organization_id: str, service_instance_id: int) -> ServiceInstance: + resp = self.get(f"{organization_id}/service_instances/{service_instance_id}") + return ServiceInstance.from_api(resp) + + def get_service_instances(self, organization_id: str) -> List[ServiceInstance]: + resp = self.get(f"{organization_id}/service_instances") + return [ServiceInstance.from_api(row) for row in resp] + + """ + Scopes + """ + + def get_service_instance_scopes( + self, organization_id: str, service_instance_id: int + ) -> List[ServiceInstanceScope]: + resp = self.get(f"{organization_id}/service_instances/{service_instance_id}/scopes") + return [ServiceInstanceScope.from_api(rec) for rec in resp] + + def add_service_instance_scope( + self, service_grant: ServiceInstanceGrant, service_scope: ServiceInstanceScope + ) -> ServiceInstanceGrantScope: + resp = self.post(f"grants/{service_grant.id}/scopes/{service_scope.id}") + return ServiceInstanceGrantScope.from_api(resp) + + def remove_service_instance_scope( + self, service_grant: ServiceInstanceGrant, service_scope: ServiceInstanceScope + ): + resp = self.delete(f"grants/{service_grant.id}/scopes/{service_scope.id}") + return resp + + """ + Dependencies + """ + + def get_service_instance_dependencies( + self, organization_id: str, service_instance_id: int + ) -> List[ServiceInstanceGrant]: + resp = self.get(f"{organization_id}/service_instances/{service_instance_id}/grants") + return [ServiceInstanceGrant.from_api(rec) for rec in resp] + + def create_service_dependency( + self, organization_id: str, service_grant: ServiceInstanceGrant + ) -> ServiceInstanceGrant: + resp = self.post( + f"{organization_id}/service_instances/{service_grant.from_service_instance_id}/grants", + json=service_grant.post(), + ) + return ServiceInstanceGrant.from_api(resp) + + def remove_service_dependency( + self, organization_id: str, from_service_instance_id: int, dependent_service_id: int + ) -> bool: + self.delete( + f"{organization_id}/service_instances/{from_service_instance_id}/" + f"dependencies/{dependent_service_id}" + ) + return True