easyrunner-cli 0.0.8.dev126__tar.gz → 0.0.8.dev128__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/PKG-INFO +1 -1
- easyrunner_cli-0.0.8.dev128/easyrunner/source/services/__init__.py +28 -0
- easyrunner_cli-0.0.8.dev128/easyrunner/source/services/app_service.py +168 -0
- easyrunner_cli-0.0.8.dev128/easyrunner/source/services/deployment_service.py +170 -0
- easyrunner_cli-0.0.8.dev128/easyrunner/source/services/license_service.py +76 -0
- easyrunner_cli-0.0.8.dev128/easyrunner/source/services/server_service.py +172 -0
- easyrunner_cli-0.0.8.dev128/easyrunner/source/services/service_result.py +48 -0
- easyrunner_cli-0.0.8.dev128/easyrunner/tests/test_services.py +495 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/pyproject.toml +1 -1
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/README.md +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/authelia/configuration.template.yml +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/authelia/configuration.yml +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/authelia/local-dns-setup.sh +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/authelia/notification.txt +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/authelia/users_database.yaml +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/caddy/Caddyfile +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/caddy/Caddyfile with authcrunch +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/caddy/Caddyfile with ldap +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/docker-compose/.env.dev +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/docker-compose/.env.prod +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.server-config/infra/docker-compose/docker-compose-host.yaml +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.templates/docker-compose/docker-compose-app.yaml +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/.templates/docker-compose/docker-compose-helloworld.yaml +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/README.md +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/easyrunner_build.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/poetry.lock +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/pyproject.toml +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/pytest.ini +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/cloud_providers/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/cloud_providers/cloud_provider_base.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/cloud_providers/cloud_providers.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/cloud_providers/hetzner_provider.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/command_executor.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/command_executor_local.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/archive_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/caddy_api_curl_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/caddy_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/command_base.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/curl_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/dir_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/docker_compose_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/file_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/git_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/ip_tables_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/ip_tables_persistent_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/null_command.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/os_package_manager_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/podman_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/ssh_agent_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/ssh_keygen_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/systemctl_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/user_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/base/utility_commands.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/runnable_command_string.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/archive_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/caddy_api_curl_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/caddy_commands_container_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/curl_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/dir_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/docker_compose_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/file_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/git_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/ip_tables_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/ip_tables_persistent_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/os_package_manager_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/podman_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/ssh_agent_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/ssh_keygen_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/systemctl_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/user_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/commands/ubuntu/utility_commands_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/format_utils.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/http_client.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/cloudflare/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/cloudflare/cloudflare_api_client.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/cloudflare/cloudflare_api_config.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/cloudflare/cloudflare_api_token_manager.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/github/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/github/github_device_flow.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/github/github_oauth_config.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/github/github_token_manager.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/hetzner/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/integrations/hetzner/hetzner_api_key_manager.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/known_host_ssh_keys.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/cloud_firewall_base.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/cloud_resource_api_base.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/cloud_resource_pulumi_base.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/cloud_virtual_machine_base.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/github/github_api_client.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/github/github_api_client_dtos.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/github/github_repo.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_firewall.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_firewall_rule.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_resource_factory.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_stack.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/cloud_resources/hetzner/hetzner_virtual_machine.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/caddy.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/directory.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/docker_compose.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/file.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/git_repo.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/host_server_ubuntu.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/ip_tables.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/os_package_manager.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/os_resource_base.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/podman.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/podman_network.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/ssh_agent.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/systemd_service.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/os_resources/user.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/resource_base.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/resources/web_security_scanner.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/ssh.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/ssh_key.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/data_models/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/data_models/app.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/data_models/database_dto_base.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/data_models/server.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/db_config.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/db_ctx.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/easyrunner_store.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/json_encoder.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/object_id.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/secret_store.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/store/uuid7.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/tool_paths.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/caddy/caddy_config.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/caddy/caddy_site.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/compose_project/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/compose_project/compose_network.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/compose_project/compose_project.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/compose_project/compose_service.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/compose_project/compose_volume.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/cpu_arch_types.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/dir_info.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/dto_base.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/exec_result.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/file_info.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/json.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/jsonobject_to_dataclass.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/os_type.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/podman_network_driver.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/security_scan_result.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/ssh_key_type.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/source/types/vm_config.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/conftest.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/test_compose_to_quadlet.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/test_hello_world.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/test_secret_store.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/easyrunner/tests/test_user_resource.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/app_sub_command.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/infrastructure_deps.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/cloudflare/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/github/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/hetzner/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/secret_store/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/secret_store/keyring_secret_store.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/integrations/secret_store/secret_store.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/license_sub_command.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/licensing/__init__.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/licensing/license_manager.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/link_sub_command.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/main.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/servers_sub_command.py +0 -0
- {easyrunner_cli-0.0.8.dev126 → easyrunner_cli-0.0.8.dev128}/source/ssh_config.py +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Service layer for business logic orchestration.
|
|
2
|
+
|
|
3
|
+
Services are the bridge between interfaces (CLI, Web API, Web UI) and core logic.
|
|
4
|
+
They encapsulate business rules, validation, and orchestration without being
|
|
5
|
+
tied to any specific interface.
|
|
6
|
+
|
|
7
|
+
Key principles:
|
|
8
|
+
- Pure business logic (testable without I/O)
|
|
9
|
+
- Return structured results (not UI formatting)
|
|
10
|
+
- Can be consumed by CLI, Web API, and Web UI equally
|
|
11
|
+
- No presentation logic (no Rich tables, JSON formatting, etc.)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .app_service import AppService
|
|
15
|
+
from .deployment_service import DeploymentService
|
|
16
|
+
from .license_service import LicenseService
|
|
17
|
+
from .server_service import ServerService
|
|
18
|
+
from .service_result import (
|
|
19
|
+
ServiceResult,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ServerService",
|
|
24
|
+
"AppService",
|
|
25
|
+
"DeploymentService",
|
|
26
|
+
"LicenseService",
|
|
27
|
+
"ServiceResult",
|
|
28
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""AppService: Business logic for application management operations.
|
|
2
|
+
|
|
3
|
+
Handles:
|
|
4
|
+
- App validation and persistence
|
|
5
|
+
- App deployment orchestration
|
|
6
|
+
- App status checks
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
from ..store import EasyRunnerStore
|
|
13
|
+
from ..store.data_models.app import App
|
|
14
|
+
from .service_result import ServiceResult
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AppService:
|
|
20
|
+
"""Service for app management business logic."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, store: EasyRunnerStore):
|
|
23
|
+
"""Initialize service with store dependency.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
store: EasyRunnerStore instance for persistence
|
|
27
|
+
"""
|
|
28
|
+
self.store = store
|
|
29
|
+
|
|
30
|
+
def get_app(self, server_name: str, app_name: str) -> ServiceResult[App]:
|
|
31
|
+
"""Get an app from a server.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
server_name: Name of the server
|
|
35
|
+
app_name: Name of the app
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
ServiceResult with App on success, or error if not found
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
# Get server
|
|
42
|
+
server = self.store.get_server_by_name(name=server_name)
|
|
43
|
+
if not server:
|
|
44
|
+
logger.debug(f"Server not found: {server_name}")
|
|
45
|
+
return ServiceResult.error(
|
|
46
|
+
message=f"Server '{server_name}' not found",
|
|
47
|
+
error_code="SERVER_NOT_FOUND",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Get app from server
|
|
51
|
+
app = next((a for a in server.apps if a.name == app_name), None)
|
|
52
|
+
if not app:
|
|
53
|
+
logger.debug(f"App not found: {app_name} on {server_name}")
|
|
54
|
+
return ServiceResult.error(
|
|
55
|
+
message=f"App '{app_name}' not found on server '{server_name}'",
|
|
56
|
+
error_code="APP_NOT_FOUND",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return ServiceResult.ok(
|
|
60
|
+
message=f"App '{app_name}' retrieved",
|
|
61
|
+
data=app,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Error getting app: {str(e)}", exc_info=True)
|
|
66
|
+
return ServiceResult.error(
|
|
67
|
+
message=f"Failed to get app: {str(e)}",
|
|
68
|
+
error_code="INTERNAL_ERROR",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def list_apps(self, server_name: str) -> ServiceResult[List[App]]:
|
|
72
|
+
"""List all apps on a server.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
server_name: Name of the server
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
ServiceResult with list of apps
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
# Get server
|
|
82
|
+
server = self.store.get_server_by_name(name=server_name)
|
|
83
|
+
if not server:
|
|
84
|
+
logger.debug(f"Server not found: {server_name}")
|
|
85
|
+
return ServiceResult.error(
|
|
86
|
+
message=f"Server '{server_name}' not found",
|
|
87
|
+
error_code="SERVER_NOT_FOUND",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
apps = server.apps if server.apps else []
|
|
91
|
+
return ServiceResult.ok(
|
|
92
|
+
message=f"Retrieved {len(apps)} app(s)",
|
|
93
|
+
data=apps,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Error listing apps: {str(e)}", exc_info=True)
|
|
98
|
+
return ServiceResult.error(
|
|
99
|
+
message=f"Failed to list apps: {str(e)}",
|
|
100
|
+
error_code="INTERNAL_ERROR",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def validate_app_for_deployment(
|
|
104
|
+
self, server_name: str, app_name: str
|
|
105
|
+
) -> ServiceResult[App]:
|
|
106
|
+
"""Validate that an app is ready for deployment.
|
|
107
|
+
|
|
108
|
+
Business validation:
|
|
109
|
+
- App exists
|
|
110
|
+
- Server exists
|
|
111
|
+
- App has custom domain set (required for deployment)
|
|
112
|
+
- App has repo URL set
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
server_name: Name of the server
|
|
116
|
+
app_name: Name of the app
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
ServiceResult with App if valid, error otherwise
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
# Get the app (also validates server exists)
|
|
123
|
+
app_result = self.get_app(server_name=server_name, app_name=app_name)
|
|
124
|
+
if not app_result.success:
|
|
125
|
+
return app_result
|
|
126
|
+
|
|
127
|
+
app = app_result.data
|
|
128
|
+
if not app:
|
|
129
|
+
# Should not happen if app_result.success is True, but guard anyway
|
|
130
|
+
return ServiceResult.error(
|
|
131
|
+
message=f"App '{app_name}' data is missing",
|
|
132
|
+
error_code="APP_DATA_MISSING",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Business rule: custom domain required
|
|
136
|
+
if not app.custom_domain:
|
|
137
|
+
logger.warning(
|
|
138
|
+
f"App validation failed - no custom domain: {app_name} on {server_name}"
|
|
139
|
+
)
|
|
140
|
+
return ServiceResult.error(
|
|
141
|
+
message=f"App '{app_name}' must have a custom domain set before deployment",
|
|
142
|
+
error_code="MISSING_CUSTOM_DOMAIN",
|
|
143
|
+
details={"app_name": app_name, "server_name": server_name},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Business rule: repo URL required
|
|
147
|
+
if not app.repo_url or str(app.repo_url).strip() == "":
|
|
148
|
+
logger.warning(
|
|
149
|
+
f"App validation failed - no repo URL: {app_name} on {server_name}"
|
|
150
|
+
)
|
|
151
|
+
return ServiceResult.error(
|
|
152
|
+
message=f"App '{app_name}' must have a repository URL set before deployment",
|
|
153
|
+
error_code="MISSING_REPO_URL",
|
|
154
|
+
details={"app_name": app_name, "server_name": server_name},
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
logger.info(f"App validation passed: {app_name} on {server_name}")
|
|
158
|
+
return ServiceResult.ok(
|
|
159
|
+
message=f"App '{app_name}' is ready for deployment",
|
|
160
|
+
data=app,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"Error validating app: {str(e)}", exc_info=True)
|
|
165
|
+
return ServiceResult.error(
|
|
166
|
+
message=f"Failed to validate app: {str(e)}",
|
|
167
|
+
error_code="INTERNAL_ERROR",
|
|
168
|
+
)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""DeploymentService: Orchestrates multi-step app deployment flows.
|
|
2
|
+
|
|
3
|
+
Handles:
|
|
4
|
+
- Deployment validation and prerequisites
|
|
5
|
+
- SSH connection setup
|
|
6
|
+
- Coordinating resource operations
|
|
7
|
+
- Error handling and rollback
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from ..command_executor import CommandExecutor
|
|
14
|
+
from ..integrations.github import GitHubTokenManager
|
|
15
|
+
from ..resources.os_resources import HostServerUbuntu
|
|
16
|
+
from ..ssh import Ssh
|
|
17
|
+
from ..store import EasyRunnerStore, SecretStore
|
|
18
|
+
from .app_service import AppService
|
|
19
|
+
from .service_result import ServiceResult
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DeploymentService:
|
|
25
|
+
"""Service for orchestrating app deployments."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, store: EasyRunnerStore, secret_store: SecretStore):
|
|
28
|
+
"""Initialize service with store and secret store dependencies.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
store: EasyRunnerStore instance for persistence
|
|
32
|
+
secret_store: SecretStore instance for secure token storage
|
|
33
|
+
"""
|
|
34
|
+
self.store = store
|
|
35
|
+
self.secret_store = secret_store
|
|
36
|
+
self.app_service = AppService(store=store)
|
|
37
|
+
self.token_manager = GitHubTokenManager(secret_store=secret_store)
|
|
38
|
+
|
|
39
|
+
def deploy_app(
|
|
40
|
+
self,
|
|
41
|
+
server_name: str,
|
|
42
|
+
app_name: str,
|
|
43
|
+
ssh_username: str,
|
|
44
|
+
ssh_key_path: str,
|
|
45
|
+
github_token: Optional[str] = None,
|
|
46
|
+
debug: bool = False,
|
|
47
|
+
silent: bool = False,
|
|
48
|
+
) -> ServiceResult[dict]:
|
|
49
|
+
"""Deploy an app to a server.
|
|
50
|
+
|
|
51
|
+
This is the main orchestration method. It:
|
|
52
|
+
1. Validates the app is ready for deployment
|
|
53
|
+
2. Retrieves GitHub token if needed
|
|
54
|
+
3. Establishes SSH connection
|
|
55
|
+
4. Delegates to HostServerUbuntu resource for actual deployment
|
|
56
|
+
5. Handles errors and provides feedback
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
server_name: Name of the server to deploy to
|
|
60
|
+
app_name: Name of the app to deploy
|
|
61
|
+
ssh_username: SSH username for connection
|
|
62
|
+
ssh_key_path: Path to SSH private key
|
|
63
|
+
github_token: Optional GitHub token (retrieved from store if not provided)
|
|
64
|
+
debug: Enable debug logging
|
|
65
|
+
silent: Suppress non-error output
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
ServiceResult with deployment details on success, or error information
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
# Step 1: Validate app is ready for deployment
|
|
72
|
+
validation_result = self.app_service.validate_app_for_deployment(
|
|
73
|
+
server_name=server_name, app_name=app_name
|
|
74
|
+
)
|
|
75
|
+
if not validation_result.success:
|
|
76
|
+
logger.warning(f"App validation failed: {validation_result.message}")
|
|
77
|
+
return ServiceResult.error(
|
|
78
|
+
message=validation_result.message,
|
|
79
|
+
error_code=validation_result.error_code,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
app = validation_result.data
|
|
83
|
+
if not app:
|
|
84
|
+
# Should not happen if validation_result.success is True, but guard anyway
|
|
85
|
+
return ServiceResult.error(
|
|
86
|
+
message=f"App '{app_name}' data is missing",
|
|
87
|
+
error_code="APP_DATA_MISSING",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Step 2: Get GitHub token if not provided
|
|
91
|
+
if github_token is None:
|
|
92
|
+
try:
|
|
93
|
+
github_token = self.token_manager.get_token()
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Failed to retrieve GitHub token: {str(e)}")
|
|
96
|
+
return ServiceResult.error(
|
|
97
|
+
message="GitHub token not found. Authenticate with GitHub first using 'er link github'.",
|
|
98
|
+
error_code="GITHUB_TOKEN_NOT_FOUND",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if not github_token:
|
|
102
|
+
logger.error("No GitHub token available")
|
|
103
|
+
return ServiceResult.error(
|
|
104
|
+
message="No GitHub token configured. Please authenticate with GitHub.",
|
|
105
|
+
error_code="GITHUB_TOKEN_NOT_CONFIGURED",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Step 3: Get server info to get hostname/IP
|
|
109
|
+
server_result = self.app_service.get_app(
|
|
110
|
+
server_name=server_name, app_name=app_name
|
|
111
|
+
)
|
|
112
|
+
if not server_result.success:
|
|
113
|
+
logger.error(f"Server not found: {server_name}")
|
|
114
|
+
return ServiceResult.error(
|
|
115
|
+
message=f"Server '{server_name}' not found",
|
|
116
|
+
error_code="SERVER_NOT_FOUND",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
server = self.store.get_server_by_name(name=server_name)
|
|
120
|
+
if not server:
|
|
121
|
+
logger.error(f"Server '{server_name}' is missing.")
|
|
122
|
+
return ServiceResult.error(
|
|
123
|
+
message=f"Server '{server_name}' is missing.",
|
|
124
|
+
error_code="SERVER_NOT_FOUND",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Step 4: Establish SSH connection and deploy
|
|
128
|
+
logger.info(
|
|
129
|
+
f"Starting deployment: {app_name} to {server_name} via SSH as {ssh_username}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
with Ssh(
|
|
133
|
+
hostname_or_ipv4=server.hostname_or_ip,
|
|
134
|
+
username=ssh_username,
|
|
135
|
+
key_filename=ssh_key_path,
|
|
136
|
+
) as ssh_client:
|
|
137
|
+
executor = CommandExecutor(ssh_client=ssh_client)
|
|
138
|
+
host_server = HostServerUbuntu(
|
|
139
|
+
easyrunner_username=ssh_username,
|
|
140
|
+
executor=executor,
|
|
141
|
+
debug=debug,
|
|
142
|
+
silent=silent,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Delegate to resource for actual deployment
|
|
146
|
+
logger.info(f"Executing deployment flow for {app_name}")
|
|
147
|
+
host_server.deploy_app_flow_a(
|
|
148
|
+
repo_url=app.repo_url, # type: ignore
|
|
149
|
+
custom_app_domain_name=app.custom_domain, # type: ignore
|
|
150
|
+
github_access_token=github_token,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
logger.info(f"Deployment completed: {app_name} on {server_name}")
|
|
154
|
+
return ServiceResult.ok(
|
|
155
|
+
message=f"App '{app_name}' deployed successfully to '{server_name}'",
|
|
156
|
+
data={
|
|
157
|
+
"app_name": app_name,
|
|
158
|
+
"server_name": server_name,
|
|
159
|
+
"custom_domain": app.custom_domain,
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
error_msg = f"Deployment failed: {str(e)}"
|
|
165
|
+
logger.error(error_msg, exc_info=True)
|
|
166
|
+
return ServiceResult.error(
|
|
167
|
+
message=error_msg,
|
|
168
|
+
error_code="DEPLOYMENT_FAILED",
|
|
169
|
+
details={"exception_type": type(e).__name__},
|
|
170
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""LicenseService: Business logic for license checks and quota enforcement.
|
|
2
|
+
|
|
3
|
+
Handles:
|
|
4
|
+
- Server limit enforcement
|
|
5
|
+
- App limit enforcement
|
|
6
|
+
- License expiration checks
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from ..store import EasyRunnerStore
|
|
12
|
+
from .service_result import ServiceResult
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LicenseService:
|
|
18
|
+
"""Service for license-related business logic."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, store: EasyRunnerStore):
|
|
21
|
+
"""Initialize service with store dependency.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
store: EasyRunnerStore instance for persistence
|
|
25
|
+
"""
|
|
26
|
+
self.store = store
|
|
27
|
+
|
|
28
|
+
def can_add_server(self) -> ServiceResult[None]:
|
|
29
|
+
"""Check if a new server can be added based on license limits.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
ServiceResult indicating if server can be added
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
# TODO: Integrate with actual LicenseManager when it's refactored to core
|
|
36
|
+
# For now, just return True (no license checks)
|
|
37
|
+
logger.debug("License check: can_add_server - not yet integrated")
|
|
38
|
+
return ServiceResult.ok(
|
|
39
|
+
message="Server can be added (TODO: integrate with LicenseManager)",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.error(f"Error checking server quota: {str(e)}", exc_info=True)
|
|
44
|
+
# Be permissive on errors (don't block operations)
|
|
45
|
+
return ServiceResult.ok(
|
|
46
|
+
message="Server can be added (error in license check, permissive mode)",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def can_add_app(self, server_name: str) -> ServiceResult[None]:
|
|
50
|
+
"""Check if a new app can be added to a server based on license limits.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
server_name: Name of the server
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
ServiceResult indicating if app can be added
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
server = self.store.get_server_by_name(name=server_name)
|
|
60
|
+
if not server:
|
|
61
|
+
return ServiceResult.error(
|
|
62
|
+
message=f"Server '{server_name}' not found",
|
|
63
|
+
error_code="SERVER_NOT_FOUND",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# TODO: Check license limits for apps per server
|
|
67
|
+
logger.debug(f"License check: can_add_app on {server_name}")
|
|
68
|
+
return ServiceResult.ok(
|
|
69
|
+
message="App can be added (TODO: check license limits)",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Error checking app quota: {str(e)}", exc_info=True)
|
|
74
|
+
return ServiceResult.ok(
|
|
75
|
+
message="App can be added (error in license check, permissive mode)",
|
|
76
|
+
)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""ServerService: Business logic for server management operations.
|
|
2
|
+
|
|
3
|
+
Handles:
|
|
4
|
+
- Server addition with validation (uniqueness, etc.)
|
|
5
|
+
- Server retrieval and listing
|
|
6
|
+
- Server deletion
|
|
7
|
+
- Server setup verification
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
from ..store import EasyRunnerStore
|
|
14
|
+
from ..store.data_models.server import Server
|
|
15
|
+
from .service_result import ServiceResult
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ServerService:
|
|
21
|
+
"""Service for server management business logic."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, store: EasyRunnerStore):
|
|
24
|
+
"""Initialize service with store dependency.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
store: EasyRunnerStore instance for persistence
|
|
28
|
+
"""
|
|
29
|
+
self.store = store
|
|
30
|
+
|
|
31
|
+
def add_server(
|
|
32
|
+
self, name: str, hostname_or_ip: str
|
|
33
|
+
) -> ServiceResult[Server]:
|
|
34
|
+
"""Add a server to the store with validation.
|
|
35
|
+
|
|
36
|
+
Business validation:
|
|
37
|
+
- Server name must be unique
|
|
38
|
+
- Hostname/IP must be unique
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
name: Friendly name for the server
|
|
42
|
+
hostname_or_ip: Hostname or IP address
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
ServiceResult with created Server on success, or error message on failure
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
# Validation: check name uniqueness
|
|
49
|
+
existing_by_name = self.store.get_server_by_name(name=name)
|
|
50
|
+
if existing_by_name:
|
|
51
|
+
logger.warning(f"Attempt to add server with duplicate name: {name}")
|
|
52
|
+
return ServiceResult.error(
|
|
53
|
+
message=f"Server with name '{name}' already exists",
|
|
54
|
+
error_code="DUPLICATE_NAME",
|
|
55
|
+
details={"existing_server": existing_by_name.name},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Validation: check hostname/IP uniqueness
|
|
59
|
+
existing_by_addr = self.store.get_server_by_hostname_or_ip(
|
|
60
|
+
hostname_or_ip=hostname_or_ip
|
|
61
|
+
)
|
|
62
|
+
if existing_by_addr:
|
|
63
|
+
logger.warning(
|
|
64
|
+
f"Attempt to add server with duplicate address: {hostname_or_ip}"
|
|
65
|
+
)
|
|
66
|
+
return ServiceResult.error(
|
|
67
|
+
message=f"Server with address '{hostname_or_ip}' already exists",
|
|
68
|
+
error_code="DUPLICATE_ADDRESS",
|
|
69
|
+
details={"existing_server": existing_by_addr.name},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Create and persist server
|
|
73
|
+
server = Server(name=name, hostname_or_ip=hostname_or_ip)
|
|
74
|
+
self.store.add_server(server=server)
|
|
75
|
+
|
|
76
|
+
logger.info(f"Server added: {name} ({hostname_or_ip})")
|
|
77
|
+
return ServiceResult.ok(
|
|
78
|
+
message=f"Server '{name}' added successfully",
|
|
79
|
+
data=server,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(f"Error adding server: {str(e)}", exc_info=True)
|
|
84
|
+
return ServiceResult.error(
|
|
85
|
+
message=f"Failed to add server: {str(e)}",
|
|
86
|
+
error_code="INTERNAL_ERROR",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def get_server(self, name: str) -> ServiceResult[Server]:
|
|
90
|
+
"""Get a server by name.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
name: Server name
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
ServiceResult with Server on success, or error if not found
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
server = self.store.get_server_by_name(name=name)
|
|
100
|
+
if not server:
|
|
101
|
+
logger.debug(f"Server not found: {name}")
|
|
102
|
+
return ServiceResult.error(
|
|
103
|
+
message=f"Server '{name}' not found",
|
|
104
|
+
error_code="NOT_FOUND",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return ServiceResult.ok(
|
|
108
|
+
message=f"Server '{name}' retrieved",
|
|
109
|
+
data=server,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Error getting server: {str(e)}", exc_info=True)
|
|
114
|
+
return ServiceResult.error(
|
|
115
|
+
message=f"Failed to get server: {str(e)}",
|
|
116
|
+
error_code="INTERNAL_ERROR",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def list_servers(self) -> ServiceResult[List[Server]]:
|
|
120
|
+
"""List all servers.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
ServiceResult with list of servers
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
servers = self.store.list_servers()
|
|
127
|
+
return ServiceResult.ok(
|
|
128
|
+
message=f"Retrieved {len(servers)} server(s)",
|
|
129
|
+
data=servers,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"Error listing servers: {str(e)}", exc_info=True)
|
|
134
|
+
return ServiceResult.error(
|
|
135
|
+
message=f"Failed to list servers: {str(e)}",
|
|
136
|
+
error_code="INTERNAL_ERROR",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def delete_server(self, name: str) -> ServiceResult[None]:
|
|
140
|
+
"""Delete a server by name.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
name: Server name
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
ServiceResult indicating success or failure
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
# Get the server first to validate it exists
|
|
150
|
+
server_result = self.get_server(name)
|
|
151
|
+
if not server_result.success:
|
|
152
|
+
return ServiceResult.error(
|
|
153
|
+
message=server_result.message,
|
|
154
|
+
error_code=server_result.error_code,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Delete from store using server id
|
|
158
|
+
server = server_result.data
|
|
159
|
+
if server:
|
|
160
|
+
self.store.remove_server(server_id=server.id)
|
|
161
|
+
|
|
162
|
+
logger.info(f"Server deleted: {name}")
|
|
163
|
+
return ServiceResult.ok(
|
|
164
|
+
message=f"Server '{name}' deleted successfully",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Error deleting server: {str(e)}", exc_info=True)
|
|
169
|
+
return ServiceResult.error(
|
|
170
|
+
message=f"Failed to delete server: {str(e)}",
|
|
171
|
+
error_code="INTERNAL_ERROR",
|
|
172
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Service result types for structured error/success handling.
|
|
2
|
+
|
|
3
|
+
Services return result objects instead of raising exceptions or returning raw values.
|
|
4
|
+
This enables callers (CLI, Web API, etc.) to handle success and failure uniformly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Generic, Optional, TypeVar
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ServiceResult(Generic[T]):
|
|
15
|
+
"""Base result type for all service operations."""
|
|
16
|
+
|
|
17
|
+
success: bool
|
|
18
|
+
message: str
|
|
19
|
+
data: Optional[T] = None
|
|
20
|
+
error_code: Optional[str] = None
|
|
21
|
+
details: Optional[dict[str, Any]] = None
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def ok(cls, message: str = "Operation successful", data: Optional[T] = None, details: Optional[dict] = None) -> "ServiceResult[T]":
|
|
25
|
+
"""Create a successful result."""
|
|
26
|
+
return cls(
|
|
27
|
+
success=True,
|
|
28
|
+
message=message,
|
|
29
|
+
data=data,
|
|
30
|
+
details=details,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def error(cls, message: str, error_code: Optional[str] = None, details: Optional[dict] = None) -> "ServiceResult[T]":
|
|
35
|
+
"""Create an error result."""
|
|
36
|
+
return cls(
|
|
37
|
+
success=False,
|
|
38
|
+
message=message,
|
|
39
|
+
error_code=error_code,
|
|
40
|
+
details=details,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Use ServiceResult[T] directly for server add, app deploy, and license check operations.
|
|
45
|
+
# Example:
|
|
46
|
+
# result: ServiceResult[Server]
|
|
47
|
+
# result: ServiceResult[AppDeployment]
|
|
48
|
+
# result: ServiceResult[LicenseInfo]
|