diff --git a/README.md b/README.md index 4274e461..95017e8a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,14 @@ Install the SDK via Composer: composer require upsun/upsun-sdk-php ``` +> **Important**: +> This SDK relies on PSR interfaces and requires a **PSR-18 HTTP client** implementation at runtime. +> If your project does not already provide one, install a compatible client, for example: +> +> ```bash +> composer require symfony/http-client +> ``` + Then include Composer's autoloader in your PHP application: ```php @@ -26,9 +34,13 @@ require __DIR__ . '/vendor/autoload.php'; ## Authentication -You will need an [Upsun API token](https://docs.upsun.com/administration/cli/api-tokens.html) to use this SDK. +You will need an [Upsun API token](https://developer.upsun.com/cli/api-tokens) to use this SDK. Store it securely, preferably in an environment variable. +### With an API token (default) + +Use this outside of an Upsun runtime, authenticating with your API token. + ```php use Upsun\UpsunConfig; use Upsun\UpsunClient; @@ -37,6 +49,24 @@ $config = new UpsunConfig(apiToken: getenv('UPSUN_API_TOKEN')); $upsunClient = new UpsunClient($config); ``` +### With the local token service (inside an Upsun container) + +Inside an Upsun runtime container, a local token service is exposed on +`http://localhost:8200`. Point `auth_url` to `UpsunConfig::LOCAL_AUTH` and the SDK +authenticates with the `client_credentials` grant automatically (no API token +required). You may optionally request a token lifetime via `tokenTtl` (60-900 seconds). + +```php +use Upsun\UpsunConfig; +use Upsun\UpsunClient; + +$config = new UpsunConfig( + auth_url: UpsunConfig::LOCAL_AUTH, + tokenTtl: 900, // optional, 60-900 seconds +); +$upsunClient = new UpsunClient($config); +``` + ## Usage ### Example: List organizations @@ -72,7 +102,7 @@ composer install The SDK is built as follows: -* From the [JSON specs of our API](https://docs.upsun.com/api/openapispec-upsun.json) +* From the [JSON specs of our API](https://meta.upsun.com/openapi-spec) * Using [``@openapitools/openapi-generator-cli``](https://www.npmjs.com/package/%40openapitools/openapi-generator-cli) * Which generates: * PHP **Models** (in `src/Model/`) @@ -82,7 +112,7 @@ The SDK is built as follows: ### Regenerating API & Model classes API and Model classes are generated using [openapi-generator-cli](https://openapi-generator.tech) -from the [Upsun OpenAPI spec](https://docs.upsun.com/api/openapispec-upsun.json). +from the [Upsun OpenAPI spec](https://meta.upsun.com/openapi-spec). ```bash composer run spec:install diff --git a/deployments_current.json b/deployments_current.json new file mode 100644 index 00000000..686f6b02 --- /dev/null +++ b/deployments_current.json @@ -0,0 +1,1021 @@ +{ + "id": "current", + "_links": { + "self": { + "href": "https://eu-5.platform.sh/api/projects/iok5pcum4s4au/environments/main/deployments/b876f0f2661da0f2cd880004472920886719dcd0" + }, + "#sboms": { + "href": "/api/projects/iok5pcum4s4au/environments/main/deployments/b876f0f2661da0f2cd880004472920886719dcd0/sboms" + }, + "#topology": { + "href": "/api/projects/iok5pcum4s4au/environments/main/deployments/b876f0f2661da0f2cd880004472920886719dcd0/topology" + }, + "#operations": { + "href": "/api/projects/iok5pcum4s4au/environments/main/deployments/b876f0f2661da0f2cd880004472920886719dcd0/operations" + }, + "#edit": { + "href": "/api/projects/iok5pcum4s4au/environments/main/deployments/next" + } + }, + "created_at": "2026-02-11T08:06:32+00:00", + "updated_at": null, + "fingerprint": "e66736045c1d3a89f24045f4486a7f472db09c51", + "cluster_name": "iok5pcum4s4au-main-bvxea6i", + "project_info": { + "title": "Test upsun-sdk-node (Symfony demo)", + "name": "iok5pcum4s4au", + "entropy": "IX5MWO2TKSGWOLYQYU3PQSHNSQX7GI2YLYRXN7PDS3JVHJZNN7RA====", + "namespace": "upsun", + "organization": "01HEW7TZWTGFR7A4EGAMGAKB55", + "capabilities": { + "autoscaling": { + "enabled": true + }, + "build_resources": { + "enabled": true, + "max_cpu": 4.0, + "max_memory": 10240 + }, + "custom_domains": { + "enabled": true, + "environments_with_domains_limit": 5 + }, + "data_retention": { + "enabled": true + }, + "guaranteed_resources": { + "enabled": false, + "instance_limit": 32 + }, + "images": { + "elasticsearch-enterprise": { + "*": { + "available": false + } + }, + "mongodb-enterprise": { + "*": { + "available": false + } + } + }, + "instance_limit": 8, + "integrations": { + "enabled": true, + "config": { + "newrelic": { + "enabled": true + }, + "sumologic": { + "enabled": true + }, + "splunk": { + "enabled": true + }, + "httplog": { + "enabled": true + }, + "syslog": { + "enabled": true + }, + "webhook": { + "enabled": true + }, + "script": { + "enabled": true + }, + "github": { + "enabled": true + }, + "gitlab": { + "enabled": true + }, + "bitbucket": { + "enabled": true + }, + "bitbucket_server": { + "enabled": true + }, + "health.email": { + "enabled": true + }, + "health.webhook": { + "enabled": true + }, + "health.pagerduty": { + "enabled": true + }, + "health.slack": { + "enabled": true + }, + "cdn.fastly": { + "enabled": true + }, + "blackfire": { + "enabled": true, + "role": "admin" + }, + "otlplog": { + "enabled": false + } + }, + "allowed_integrations": [ + "sumologic", + "newrelic", + "splunk", + "httplog", + "syslog", + "webhook", + "script", + "github", + "gitlab", + "bitbucket", + "bitbucket_server", + "health.email", + "health.webhook", + "health.pagerduty", + "health.slack", + "cdn.fastly", + "blackfire" + ] + }, + "logs_forwarding": { + "max_extra_payload_size": 1048576 + }, + "metrics": { + "max_range": "30d" + }, + "runtime_operations": { + "enabled": true + }, + "source_operations": { + "enabled": true + } + }, + "settings": { + "activity_logs_max_size": 67108864, + "additional_hosts": {}, + "allow_activity_reschedule": false, + "allow_burst": false, + "allow_manual_deployments": true, + "allow_rolling_deployments": true, + "allow_scaling_to_zero": false, + "app_error_page_template": null, + "application_config_file": ".upsun.app.yaml", + "bot_email": "bot@upsun.com", + "build_resources": { + "cpu": 1.0, + "memory": 2048 + }, + "centralized_permissions": true, + "certificate_renewal_activity": true, + "certificate_style": "ecdsa", + "certifier_url": "https://ssh.api.platform.sh", + "concurrency_limits": { + "internal": 1, + "integration": 4, + "backup": 2, + "cron": 5, + "cron:production": 1, + "default": 2, + "cleanup": 3 + }, + "continuous_profiling": { + "supported_runtimes": [ + "ruby", + "python", + "php", + "nodejs", + "rust", + "java", + "golang" + ] + }, + "cron_maximum_jitter": 20, + "cron_minimum_interval": 5, + "cron_non_production_expiry_interval": 30, + "cron_production_expiry_interval": 30, + "crons_in_git": true, + "custom_error_template": null, + "data_retention": { + "production": { + "max_backups": 4, + "default_config": { + "manual_count": 2, + "schedule": [ + { + "interval": "1d", + "count": 2 + } + ] + } + }, + "development": { + "max_backups": 2, + "default_config": { + "manual_count": 2, + "schedule": [] + } + } + }, + "development_application_size": "S", + "development_domain_template": null, + "development_service_size": "S", + "disable_agent_error_reporter": false, + "enable_admin_agent": false, + "enable_cache_grace_period": true, + "enable_certificate_provisioning": true, + "enable_codesource_integration_push": true, + "enable_disk_health_monitoring": true, + "enable_github_app_token_exchange": false, + "enable_guaranteed_resources": false, + "enable_incremental_backups": true, + "enable_paused_environments": true, + "enable_routes_tracing": true, + "enable_state_api_deployments": true, + "enable_unified_configuration": true, + "enable_zero_downtime_deployments": false, + "enforce_mfa": false, + "environment_name_strategy": "name-and-hash", + "flexible_build_cache": false, + "git_server": { + "push_size_hard_limit": 100 + }, + "glue_server_max_request_size": 10, + "has_sleepy_crons": true, + "image_deployment_validation": true, + "initialize": {}, + "local_disk_size": 8192, + "maintenance_window": null, + "max_allowed_redirects_paths": 50000, + "max_allowed_routes": 50000, + "outbound_restrictions_default_policy": "allow", + "persistent_endpoints_ssh": true, + "persistent_endpoints_ssl_certificates": true, + "product_code": "upsun", + "product_name": "Upsun", + "project_config_dir": ".upsun", + "requires_domain_ownership": false, + "router_gen2": false, + "router_resources": { + "baseline_cpu": 0.05, + "baseline_memory": 128, + "max_cpu": 1.0, + "max_memory": 1024 + }, + "save_applications_vendors": false, + "self_upgrade": true, + "self_upgrade_latest_major": false, + "sizing_api_enabled": true, + "strict_configuration": true, + "support_generic_images": true, + "systemd": false, + "temporary_disk_size": 8192, + "ui_uri_template": "https://console.upsun.com/{organization}/{project}", + "use_drupal_defaults": false, + "use_legacy_subdomains": false, + "variables_prefix": "PLATFORM_" + } + }, + "environment_info": { + "name": "main", + "status": "active", + "is_main": true, + "is_production": true, + "constraints": { + "cluster_type": "environment", + "deployment_type": "production" + }, + "reference": "refs/heads/main", + "machine_name": "main-bvxea6i", + "environment_type": "production", + "links": { + "#ui": { + "href": "https://console.upsun.com/01HEW7TZWTGFR7A4EGAMGAKB55/iok5pcum4s4au/main" + } + } + }, + "deployment_target": "local", + "vpn": null, + "http_access": { + "is_enabled": true, + "addresses": [], + "basic_auth": {} + }, + "enable_smtp": true, + "restrict_robots": true, + "variables": [], + "access": [ + { + "entity_id": "e2137712-9320-4c5d-bc8c-36965ddbd638", + "role": "admin" + } + ], + "subscription": { + "license_uri": "https://accounts.platform.sh/api/v1/licenses/3305793", + "storage": 1024, + "included_users": 1, + "subscription_management_uri": "https://console.upsun.com/florent-huck/-/billing/plan/3305793", + "restricted": false, + "suspended": false, + "user_licenses": 1, + "resource_validation_url": "https://projects.api.platform.sh/resources/cloud" + }, + "services": { + "mysql": { + "type": "mariadb:11.8:37", + "size": "AUTO", + "disk": 2048, + "access": {}, + "configuration": { + "schemas": [ + "main" + ], + "endpoints": { + "mysql": { + "default_schema": "main", + "privileges": { + "main": "admin" + } + } + }, + "backwards_compatibility_mode": true + }, + "relationships": {}, + "firewall": null, + "resources": { + "base_memory": null, + "memory_ratio": null, + "profile_size": "1", + "minimum": { + "cpu": 0.1, + "memory": 448, + "cpu_type": "shared", + "disk": 256, + "profile_size": "0.1" + }, + "default": { + "cpu": 0.5, + "memory": 1408, + "cpu_type": "shared", + "disk": 512, + "profile_size": "0.5" + }, + "disk": { + "temporary": 8192, + "instance": 8192, + "storage": 2048 + } + }, + "container_profile": "HIGH_MEMORY", + "endpoints": { + "mysql": { + "scheme": "mysql", + "port": 3306, + "default": true, + "path": "main", + "username": "", + "password": "", + "query": { + "is_master": true + } + }, + "mysql-all": { + "scheme": "mysql", + "port": 3306, + "default": false, + "path": "main", + "username": "", + "password": "", + "query": { + "is_master": true + } + }, + "mysql-replica": { + "scheme": "mysql", + "port": 3306, + "default": false, + "path": "main", + "username": "", + "password": "", + "query": { + "is_master": false + } + } + }, + "instance_count": 1, + "supports_horizontal_scaling": false + }, + "redis": { + "type": "redis:6.2:401", + "size": "AUTO", + "disk": null, + "access": {}, + "configuration": {}, + "relationships": {}, + "firewall": null, + "resources": { + "base_memory": null, + "memory_ratio": null, + "profile_size": "0.5", + "minimum": { + "cpu": 0.1, + "memory": 64, + "cpu_type": "shared", + "disk": null, + "profile_size": "0.1" + }, + "default": { + "cpu": 0.5, + "memory": 1088, + "cpu_type": "shared", + "disk": null, + "profile_size": "0.5" + }, + "disk": { + "temporary": 8192, + "instance": 8192, + "storage": null + } + }, + "container_profile": "BALANCED", + "endpoints": { + "redis": { + "scheme": "redis", + "port": 6379, + "default": true + }, + "redis-replica": { + "scheme": "redis", + "port": 6379 + } + }, + "instance_count": 1, + "supports_horizontal_scaling": false + } + }, + "routes": { + "https://main-bvxea6i-iok5pcum4s4au.eu-5.platformsh.site/": { + "primary": true, + "id": null, + "production_url": "https://main-bvxea6i-iok5pcum4s4au.eu-5.platformsh.site/", + "attributes": {}, + "type": "upstream", + "tls": { + "strict_transport_security": { + "enabled": null, + "include_subdomains": null, + "preload": null + }, + "min_version": null, + "client_authentication": null, + "client_certificate_authorities": [] + }, + "original_url": "https://{all}/", + "http_access": { + "is_enabled": true, + "addresses": [], + "basic_auth": {} + }, + "restrict_robots": true, + "cache": { + "enabled": true, + "default_ttl": 0, + "cookies": [ + "*" + ], + "headers": [ + "Accept", + "Accept-Language" + ] + }, + "ssi": { + "enabled": false + }, + "upstream": "app:http", + "redirects": { + "expires": "-1s", + "paths": {} + }, + "sticky": { + "enabled": false + } + }, + "http://main-bvxea6i-iok5pcum4s4au.eu-5.platformsh.site/": { + "primary": false, + "id": null, + "production_url": "http://main-bvxea6i-iok5pcum4s4au.eu-5.platformsh.site/", + "attributes": {}, + "type": "redirect", + "tls": { + "strict_transport_security": { + "enabled": null, + "include_subdomains": null, + "preload": null + }, + "min_version": null, + "client_authentication": null, + "client_certificate_authorities": [] + }, + "original_url": "http://{all}/", + "http_access": { + "is_enabled": true, + "addresses": [], + "basic_auth": {} + }, + "restrict_robots": true, + "to": "https://main-bvxea6i-iok5pcum4s4au.eu-5.platformsh.site/", + "redirects": { + "expires": "-1s", + "paths": {} + } + } + }, + "webapps": { + "app": { + "resources": { + "base_memory": null, + "memory_ratio": null, + "profile_size": "1", + "minimum": { + "cpu": 0.1, + "memory": 64, + "cpu_type": "shared", + "disk": 128, + "profile_size": "0.1" + }, + "default": { + "cpu": 0.5, + "memory": 224, + "cpu_type": "shared", + "disk": 512, + "profile_size": "0.5" + }, + "disk": { + "temporary": 8192, + "instance": 8192, + "storage": 1024 + } + }, + "size": "AUTO", + "disk": 1024, + "access": { + "ssh": "contributor" + }, + "relationships": { + "database": { + "service": "mysql", + "endpoint": "mysql" + }, + "cache": { + "service": "redis", + "endpoint": "redis" + } + }, + "additional_hosts": {}, + "mounts": { + "/var/cache": { + "source": "local", + "source_path": "var/cache" + }, + "/var/share": { + "source": "storage", + "source_path": "var/share" + }, + "/data": { + "source": "storage", + "source_path": "data" + } + }, + "timezone": null, + "variables": { + "php": { + "opcache.preload": "config/preload.php" + } + }, + "firewall": null, + "container_profile": "HIGH_CPU", + "operations": {}, + "name": "app", + "type": "php:8.5:97", + "preflight": { + "enabled": true, + "ignored_rules": [] + }, + "tree_id": "bbd8e8c96196275d1a7e774b98db768b22b3d260", + "app_dir": "/app", + "endpoints": { + "http": { + "scheme": "http", + "port": 80 + }, + "php": { + "scheme": "http", + "port": 80 + } + }, + "runtime": { + "extensions": [ + "apcu", + "blackfire", + "ctype", + "iconv", + "mbstring", + "pdo_sqlite", + "sodium", + "xsl", + "datadog-profiling" + ] + }, + "web": { + "locations": { + "/": { + "root": "public", + "expires": "1h", + "passthru": "/index.php", + "scripts": true, + "allow": true, + "headers": {}, + "rules": {} + } + }, + "move_to_root": false + }, + "hooks": { + "build": "set -x -e\n\ncurl -fs https://get.symfony.com/cloud/configurator | bash\n\nNODE_VERSION=22 symfony-build\n", + "deploy": "set -x -e\n\nsymfony-deploy\n", + "post_deploy": null + }, + "crons": { + "security-check": { + "spec": "50 23 * * *", + "commands": { + "start": "if [ \"$PLATFORM_ENVIRONMENT_TYPE\" = \"production\" ]; then croncape COMPOSER_ROOT_VERSION=1.0.0 COMPOSER_AUDIT_ABANDONED=ignore composer audit --no-cache; fi", + "stop": null + }, + "shutdown_timeout": null, + "timeout": 86400 + }, + "clean-expired-sessions": { + "spec": "17,47 * * * *", + "commands": { + "start": "croncape php-session-clean", + "stop": null + }, + "shutdown_timeout": null, + "timeout": 86400 + } + }, + "source": { + "root": "/", + "operations": {} + }, + "build": { + "flavor": "none", + "caches": {} + }, + "dependencies": {}, + "stack": { + "runtimes": [], + "packages": [] + }, + "is_across_submodule": false, + "instance_count": 2, + "config_id": "14a2606511f6420a3c52b9285c5a5e18a1944fa9", + "slug_id": "iok5pcum4s4au-app-bbd8e8c96196275d1a7e774b98db768b22b3d260-14a2606511f6420a3c52b9285c5a5e18a1944fa9", + "supports_horizontal_scaling": true + } + }, + "workers": { + "app--app-worker": { + "resources": { + "base_memory": null, + "memory_ratio": null, + "profile_size": "1", + "minimum": { + "cpu": 0.1, + "memory": 64, + "cpu_type": "shared", + "disk": null, + "profile_size": "0.1" + }, + "default": { + "cpu": 0.5, + "memory": 224, + "cpu_type": "shared", + "disk": null, + "profile_size": "0.5" + }, + "disk": { + "temporary": 8192, + "instance": 8192, + "storage": null + } + }, + "size": "AUTO", + "disk": null, + "access": { + "ssh": "contributor" + }, + "relationships": { + "database": { + "service": "mysql", + "endpoint": "mysql" + }, + "cache": { + "service": "redis", + "endpoint": "redis" + } + }, + "additional_hosts": {}, + "mounts": { + "/var/cache": { + "source": "local", + "source_path": "var/cache" + }, + "/var/share": { + "source": "storage", + "source_path": "var/share" + }, + "/data": { + "source": "storage", + "source_path": "data" + } + }, + "timezone": null, + "variables": { + "php": { + "opcache.preload": "config/preload.php" + } + }, + "firewall": null, + "container_profile": "HIGH_CPU", + "operations": {}, + "name": "app--app-worker", + "type": "php:8.5:97", + "preflight": { + "enabled": true, + "ignored_rules": [] + }, + "tree_id": "bbd8e8c96196275d1a7e774b98db768b22b3d260", + "app_dir": "/app", + "endpoints": {}, + "runtime": { + "extensions": [ + "apcu", + "blackfire", + "ctype", + "iconv", + "mbstring", + "pdo_sqlite", + "sodium", + "xsl", + "datadog-profiling" + ] + }, + "worker": { + "commands": { + "start": "php -v" + } + }, + "app": "app", + "stack": { + "runtimes": [], + "packages": [] + }, + "instance_count": 2, + "slug_id": "iok5pcum4s4au-app-bbd8e8c96196275d1a7e774b98db768b22b3d260-14a2606511f6420a3c52b9285c5a5e18a1944fa9", + "supports_horizontal_scaling": true + } + }, + "container_profiles": { + "BALANCED": { + "0.1": { + "cpu": 0.1, + "memory": 352, + "cpu_type": "shared" + }, + "0.25": { + "cpu": 0.25, + "memory": 640, + "cpu_type": "shared" + }, + "0.5": { + "cpu": 0.5, + "memory": 1088, + "cpu_type": "shared" + }, + "1": { + "cpu": 1.0, + "memory": 1920, + "cpu_type": "shared" + }, + "2": { + "cpu": 2.0, + "memory": 2800, + "cpu_type": "shared" + }, + "4": { + "cpu": 4.0, + "memory": 4800, + "cpu_type": "shared" + }, + "16.gc": { + "cpu": 16.0, + "memory": 65536, + "cpu_type": "guaranteed" + }, + "4.gc": { + "cpu": 4.0, + "memory": 16384, + "cpu_type": "guaranteed" + }, + "48.gc": { + "cpu": 48.0, + "memory": 196608, + "cpu_type": "guaranteed" + }, + "6": { + "cpu": 6.0, + "memory": 6080, + "cpu_type": "shared" + }, + "8": { + "cpu": 8.0, + "memory": 7296, + "cpu_type": "shared" + }, + "8.gc": { + "cpu": 8.0, + "memory": 32768, + "cpu_type": "guaranteed" + } + }, + "HIGHER_MEMORY": { + "0.1": { + "cpu": 0.1, + "memory": 864, + "cpu_type": "shared" + }, + "0.25": { + "cpu": 0.25, + "memory": 1472, + "cpu_type": "shared" + }, + "0.5": { + "cpu": 0.5, + "memory": 2368, + "cpu_type": "shared" + }, + "1": { + "cpu": 1.0, + "memory": 3840, + "cpu_type": "shared" + }, + "2": { + "cpu": 2.0, + "memory": 6336, + "cpu_type": "shared" + }, + "4": { + "cpu": 4.0, + "memory": 10496, + "cpu_type": "shared" + }, + "6": { + "cpu": 6.0, + "memory": 14080, + "cpu_type": "shared" + }, + "8": { + "cpu": 8.0, + "memory": 17408, + "cpu_type": "shared" + } + }, + "HIGH_CPU": { + "0.1": { + "cpu": 0.1, + "memory": 64, + "cpu_type": "shared" + }, + "0.25": { + "cpu": 0.25, + "memory": 128, + "cpu_type": "shared" + }, + "0.5": { + "cpu": 0.5, + "memory": 224, + "cpu_type": "shared" + }, + "1": { + "cpu": 1.0, + "memory": 384, + "cpu_type": "shared" + }, + "2": { + "cpu": 2.0, + "memory": 704, + "cpu_type": "shared" + }, + "4": { + "cpu": 4.0, + "memory": 1216, + "cpu_type": "shared" + }, + "16.gc": { + "cpu": 16.0, + "memory": 32768, + "cpu_type": "guaranteed" + }, + "4.gc": { + "cpu": 4.0, + "memory": 8192, + "cpu_type": "guaranteed" + }, + "48.gc": { + "cpu": 48.0, + "memory": 98304, + "cpu_type": "guaranteed" + }, + "6": { + "cpu": 6.0, + "memory": 1728, + "cpu_type": "shared" + }, + "8": { + "cpu": 8.0, + "memory": 2240, + "cpu_type": "shared" + }, + "8.gc": { + "cpu": 8.0, + "memory": 16384, + "cpu_type": "guaranteed" + } + }, + "HIGH_MEMORY": { + "0.1": { + "cpu": 0.1, + "memory": 448, + "cpu_type": "shared" + }, + "0.25": { + "cpu": 0.25, + "memory": 832, + "cpu_type": "shared" + }, + "0.5": { + "cpu": 0.5, + "memory": 1408, + "cpu_type": "shared" + }, + "1": { + "cpu": 1.0, + "memory": 2432, + "cpu_type": "shared" + }, + "2": { + "cpu": 2.0, + "memory": 4032, + "cpu_type": "shared" + }, + "4": { + "cpu": 4.0, + "memory": 6720, + "cpu_type": "shared" + }, + "16.gc": { + "cpu": 16.0, + "memory": 131072, + "cpu_type": "guaranteed" + }, + "4.gc": { + "cpu": 4.0, + "memory": 32768, + "cpu_type": "guaranteed" + }, + "48.gc": { + "cpu": 48.0, + "memory": 393216, + "cpu_type": "guaranteed" + }, + "6": { + "cpu": 6.0, + "memory": 9024, + "cpu_type": "shared" + }, + "8": { + "cpu": 8.0, + "memory": 11200, + "cpu_type": "shared" + }, + "8.gc": { + "cpu": 8.0, + "memory": 65536, + "cpu_type": "guaranteed" + } + } + } +} \ No newline at end of file diff --git a/schema/openapispec-upsun.json b/schema/openapispec-upsun.json index 3e055ec8..086bda1d 100644 --- a/schema/openapispec-upsun.json +++ b/schema/openapispec-upsun.json @@ -245,6 +245,7 @@ } }, "x-vendor": "upsun", + "x-deprecated": true, "x-property-id-kebab": "get-org-addons", "x-tag-id-kebab": "Add-ons", "x-return-types-displayReturn": false, @@ -9082,6 +9083,7 @@ } }, "x-vendor": "upsun", + "x-deprecated": true, "x-property-id-kebab": "list-org-discounts", "x-tag-id-kebab": "Discounts", "x-return-types-displayReturn": false, @@ -15176,6 +15178,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "list-org-invoices", "x-tag-id-kebab": "Invoices", "x-return-types-displayReturn": false, @@ -15241,6 +15244,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "get-org-invoice", "x-tag-id-kebab": "Invoices", "x-return-types-displayReturn": false, @@ -15882,6 +15886,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "list-org-orders", "x-tag-id-kebab": "Orders", "x-return-types-displayReturn": false, @@ -15950,6 +15955,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "get-org-order", "x-tag-id-kebab": "Orders", "x-return-types-displayReturn": false, @@ -16046,6 +16052,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "create-authorization-credentials", "x-tag-id-kebab": "Orders", "x-return-types-displayReturn": false, @@ -16359,6 +16366,7 @@ } }, "x-vendor": "upsun", + "x-deprecated": true, "x-property-id-kebab": "estimate-org", "x-tag-id-kebab": "Organization-Management", "x-return-types-displayReturn": false, @@ -16423,6 +16431,7 @@ } }, "x-vendor": "upsun", + "x-deprecated": true, "x-property-id-kebab": "get-org-billing-alert-config", "x-tag-id-kebab": "Organization-Management", "x-return-types-displayReturn": false, @@ -16514,6 +16523,7 @@ } }, "x-vendor": "upsun", + "x-deprecated": true, "x-property-id-kebab": "update-org-billing-alert-config", "x-tag-id-kebab": "Organization-Management", "x-return-types-displayReturn": false, @@ -16608,6 +16618,7 @@ } }, "x-vendor": "upsun", + "x-deprecated": true, "x-property-id-kebab": "get-org-prepayment-info", "x-tag-id-kebab": "Organization-Management", "x-return-types-displayReturn": false, @@ -16728,6 +16739,7 @@ } }, "x-vendor": "upsun", + "x-deprecated": true, "x-property-id-kebab": "list-org-prepayment-transactions", "x-tag-id-kebab": "Organization-Management", "x-return-types-displayReturn": false, @@ -17537,6 +17549,12 @@ "404": { "$ref": "#/components/responses/NotFound" }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "422": { + "$ref": "#/components/responses/UnprocessableEntity" + }, "503": { "$ref": "#/components/responses/ServiceUnavailable" } @@ -18622,6 +18640,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "get-org-address", "x-tag-id-kebab": "Profiles", "x-return-types-displayReturn": false, @@ -18702,6 +18721,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "update-org-address", "x-tag-id-kebab": "Profiles", "x-return-types-displayReturn": false, @@ -18765,6 +18785,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "get-org-profile", "x-tag-id-kebab": "Profiles", "x-return-types-displayReturn": false, @@ -18882,6 +18903,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "update-org-profile", "x-tag-id-kebab": "Profiles", "x-return-types-displayReturn": false, @@ -20141,6 +20163,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "list-org-plan-records", "x-tag-id-kebab": "Records", "x-return-types-displayReturn": false, @@ -20229,6 +20252,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "list-org-usage-records", "x-tag-id-kebab": "Records", "x-return-types-displayReturn": false, @@ -20810,261 +20834,6 @@ ] } }, - "/projects/{projectId}/oci-registries": { - "get": { - "parameters": [ - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "projectId" - } - ], - "operationId": "list-projects-oci-registries", - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegistryCredentialCollection" - } - } - } - } - }, - "tags": [ - "RegistryCredential" - ], - "x-property-id-kebab": "list-projects-oci-registries", - "x-tag-id-kebab": "RegistryCredential", - "x-return-types-displayReturn": true, - "x-phpdoc": { - "return": "\\Upsun\\Model\\RegistryCredential[]" - }, - "x-ndjson-stream": false, - "x-stream-item-type": null, - "x-returnable": true, - "x-return-types": [ - "\\Upsun\\Model\\RegistryCredential[]" - ], - "x-return-types-union": "array" - }, - "post": { - "parameters": [ - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "projectId" - } - ], - "operationId": "create-projects-oci-registries", - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AcceptedResponse" - } - } - } - } - }, - "requestBody": { - "description": "", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegistryCredentialCreateInput" - } - } - } - }, - "tags": [ - "RegistryCredential" - ], - "x-property-id-kebab": "create-projects-oci-registries", - "x-tag-id-kebab": "RegistryCredential", - "x-return-types-displayReturn": false, - "x-phpdoc": { - "return": "\\Upsun\\Model\\AcceptedResponse" - }, - "x-ndjson-stream": false, - "x-stream-item-type": null, - "x-returnable": true, - "x-return-types": [ - "\\Upsun\\Model\\AcceptedResponse" - ], - "x-return-types-union": "\\Upsun\\Model\\AcceptedResponse" - } - }, - "/projects/{projectId}/oci-registries/{registryCredentialId}": { - "get": { - "parameters": [ - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "projectId" - }, - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "registryCredentialId" - } - ], - "operationId": "get-projects-oci-registries", - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegistryCredential" - } - } - } - } - }, - "tags": [ - "RegistryCredential" - ], - "x-property-id-kebab": "get-projects-oci-registries", - "x-tag-id-kebab": "RegistryCredential", - "x-return-types-displayReturn": false, - "x-phpdoc": { - "return": "\\Upsun\\Model\\RegistryCredential" - }, - "x-ndjson-stream": false, - "x-stream-item-type": null, - "x-returnable": true, - "x-return-types": [ - "\\Upsun\\Model\\RegistryCredential" - ], - "x-return-types-union": "\\Upsun\\Model\\RegistryCredential" - }, - "patch": { - "requestBody": { - "description": "", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegistryCredentialPatch" - } - } - } - }, - "parameters": [ - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "projectId" - }, - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "registryCredentialId" - } - ], - "operationId": "update-projects-oci-registries", - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AcceptedResponse" - } - } - } - } - }, - "tags": [ - "RegistryCredential" - ], - "x-property-id-kebab": "update-projects-oci-registries", - "x-tag-id-kebab": "RegistryCredential", - "x-return-types-displayReturn": false, - "x-phpdoc": { - "return": "\\Upsun\\Model\\AcceptedResponse" - }, - "x-ndjson-stream": false, - "x-stream-item-type": null, - "x-returnable": true, - "x-return-types": [ - "\\Upsun\\Model\\AcceptedResponse" - ], - "x-return-types-union": "\\Upsun\\Model\\AcceptedResponse" - }, - "delete": { - "parameters": [ - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "projectId" - }, - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "registryCredentialId" - } - ], - "operationId": "delete-projects-oci-registries", - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AcceptedResponse" - } - } - } - } - }, - "tags": [ - "RegistryCredential" - ], - "x-property-id-kebab": "delete-projects-oci-registries", - "x-tag-id-kebab": "RegistryCredential", - "x-return-types-displayReturn": false, - "x-phpdoc": { - "return": "\\Upsun\\Model\\AcceptedResponse" - }, - "x-ndjson-stream": false, - "x-stream-item-type": null, - "x-returnable": true, - "x-return-types": [ - "\\Upsun\\Model\\AcceptedResponse" - ], - "x-return-types-union": "\\Upsun\\Model\\AcceptedResponse" - } - }, "/projects/{projectId}/git/blobs/{repositoryBlobId}": { "get": { "parameters": [ @@ -25294,180 +25063,54 @@ "name": "deploymentId" } ], - "operationId": "run-operation", - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AcceptedResponse" - } - } - } - } - }, - "requestBody": { - "description": "", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/EnvironmentOperationInput" - } - } - } - }, - "tags": [ - "Runtime Operations" - ], - "summary": "Execute a runtime operation", - "description": "Execute a runtime operation on a currently deployed environment. This allows you to run one-off commands, such as rebuilding static assets on demand, by defining an `operations` key in a project's `.upsun/config.yaml` configuration. More information on runtime operations is [available in our user documentation](https://docs.upsun.com/anchors/app/runtime-operations/).", - "x-property-id-kebab": "run-operation", - "x-tag-id-kebab": "Runtime-Operations", - "x-return-types-displayReturn": false, - "x-phpdoc": { - "return": "\\Upsun\\Model\\AcceptedResponse" - }, - "x-ndjson-stream": false, - "x-stream-item-type": null, - "x-returnable": true, - "x-return-types": [ - "\\Upsun\\Model\\AcceptedResponse" - ], - "x-return-types-union": "\\Upsun\\Model\\AcceptedResponse", - "x-description": [ - "Execute a runtime operation on a currently deployed environment. This allows you to run one-off commands, such as", - "rebuilding static assets on demand, by defining an `operations` key in a project's `.upsun/config.yaml`", - "configuration. More information on runtime operations is [available in our user", - "documentation](https://docs.upsun.com/anchors/app/runtime-operations/)." - ] - } - }, - "/projects/{projectId}/environments/{environmentId}/deployments/{deploymentId}/sboms": { - "get": { - "parameters": [ - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "projectId" - }, - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "environmentId" - }, - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "deploymentId" - } - ], - "operationId": "list-projects-environments-deployments-sboms", + "operationId": "run-operation", "responses": { "default": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SbomCollection" + "$ref": "#/components/schemas/AcceptedResponse" } } } } }, - "tags": [ - "Sbom" - ], - "x-property-id-kebab": "list-projects-environments-deployments-sboms", - "x-tag-id-kebab": "Sbom", - "x-return-types-displayReturn": true, - "x-phpdoc": { - "return": "\\Upsun\\Model\\Sbom[]" - }, - "x-ndjson-stream": false, - "x-stream-item-type": null, - "x-returnable": true, - "x-return-types": [ - "\\Upsun\\Model\\Sbom[]" - ], - "x-return-types-union": "array" - } - }, - "/projects/{projectId}/environments/{environmentId}/deployments/{deploymentId}/sboms/{sbomServiceId}": { - "get": { - "parameters": [ - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "projectId" - }, - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "environmentId" - }, - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "deploymentId" - }, - { - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "name": "sbomServiceId" - } - ], - "operationId": "get-projects-environments-deployments-sboms", - "responses": { - "default": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Sbom" - } + "requestBody": { + "description": "", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvironmentOperationInput" } } } }, "tags": [ - "Sbom" + "Runtime Operations" ], - "x-property-id-kebab": "get-projects-environments-deployments-sboms", - "x-tag-id-kebab": "Sbom", + "summary": "Execute a runtime operation", + "description": "Execute a runtime operation on a currently deployed environment. This allows you to run one-off commands, such as rebuilding static assets on demand, by defining an `operations` key in a project's `.upsun/config.yaml` configuration. More information on runtime operations is [available in our user documentation](https://docs.upsun.com/anchors/app/runtime-operations/).", + "x-property-id-kebab": "run-operation", + "x-tag-id-kebab": "Runtime-Operations", "x-return-types-displayReturn": false, "x-phpdoc": { - "return": "\\Upsun\\Model\\Sbom" + "return": "\\Upsun\\Model\\AcceptedResponse" }, "x-ndjson-stream": false, "x-stream-item-type": null, "x-returnable": true, "x-return-types": [ - "\\Upsun\\Model\\Sbom" + "\\Upsun\\Model\\AcceptedResponse" ], - "x-return-types-union": "\\Upsun\\Model\\Sbom" + "x-return-types-union": "\\Upsun\\Model\\AcceptedResponse", + "x-description": [ + "Execute a runtime operation on a currently deployed environment. This allows you to run one-off commands, such as", + "rebuilding static assets on demand, by defining an `operations` key in a project's `.upsun/config.yaml`", + "configuration. More information on runtime operations is [available in our user", + "documentation](https://docs.upsun.com/anchors/app/runtime-operations/)." + ] } }, "/projects/{projectId}/environments/{environmentId}/source-operation": { @@ -25942,6 +25585,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "estimate-new-org-subscription", "x-tag-id-kebab": "Subscriptions", "x-return-types-displayReturn": false, @@ -26036,6 +25680,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "can-create-new-org-subscription", "x-tag-id-kebab": "Subscriptions", "x-return-types-displayReturn": false, @@ -26132,6 +25777,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "estimate-org-subscription", "x-tag-id-kebab": "Subscriptions", "x-return-types-displayReturn": false, @@ -26200,6 +25846,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "get-org-subscription-current-usage", "x-tag-id-kebab": "Subscriptions", "x-return-types-displayReturn": false, @@ -26253,6 +25900,7 @@ } }, "x-vendor": "upsun", + "x-deprecated": true, "x-property-id-kebab": "list-subscription-addons", "x-tag-id-kebab": "Subscriptions", "x-return-types-displayReturn": false, @@ -26332,6 +25980,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "get-subscription-usage-alerts", "x-tag-id-kebab": "Subscriptions", "x-return-types-displayReturn": false, @@ -26450,6 +26099,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "update-subscription-usage-alerts", "x-tag-id-kebab": "Subscriptions", "x-return-types-displayReturn": false, @@ -26828,6 +26478,9 @@ }, "404": { "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } }, "x-property-id-kebab": "update-org-subscription", @@ -30809,6 +30462,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "list-org-vouchers", "x-tag-id-kebab": "Vouchers", "x-return-types-displayReturn": false, @@ -30884,6 +30538,7 @@ } } }, + "x-deprecated": true, "x-property-id-kebab": "apply-org-voucher", "x-tag-id-kebab": "Vouchers", "x-return-types-displayReturn": false, @@ -34829,6 +34484,16 @@ "Total size of space used on volumes backed up" ] }, + "size_object_storage": { + "type": "integer", + "nullable": true, + "title": "Size of object storage", + "description": "Total object-storage quota configured on webapps at backup time", + "x-isDateTime": false, + "x-description": [ + "Total object-storage quota configured on webapps at backup time" + ] + }, "deployment": { "type": "string", "nullable": true, @@ -34871,6 +34536,7 @@ "safe", "size_of_volumes", "size_used", + "size_object_storage", "deployment", "restorable", "automated" @@ -38174,40 +37840,6 @@ "title": "Resources", "description": "" }, - "size": { - "type": "string", - "enum": [ - "2XL", - "4XL", - "AUTO", - "L", - "M", - "S", - "XL", - "XS" - ], - "title": "Container Size", - "description": "The container size for this application in production. Leave blank to allow it to be set dynamically." - }, - "disk": { - "type": "integer", - "nullable": true, - "title": "Disk size", - "description": "The size of the disk." - }, - "access": { - "type": "object", - "additionalProperties": { - "type": "string", - "enum": [ - "admin", - "contributor", - "viewer" - ] - }, - "title": "Access Information", - "description": "Access information, a mapping between access type and roles." - }, "authorizations": { "type": "array", "items": { @@ -38242,7 +37874,7 @@ "additionalProperties": false }, "title": "Authorizations", - "description": "Authorizations available to this container." + "description": "Authorizations available to this container" }, "relationships": { "type": "object", @@ -38270,7 +37902,7 @@ "nullable": true }, "title": "Service Relationships", - "description": "The relationships of the application to defined services." + "description": "The relationships of the container to defined services" }, "additional_hosts": { "type": "object", @@ -38317,7 +37949,7 @@ "additionalProperties": false }, "title": "Mounts", - "description": "Filesystem mounts of this application. If not specified the application will have no writeable disk space." + "description": "Filesystem mounts of this container. If not specified the container will have no writeable disk space" }, "timezone": { "type": "string", @@ -38332,7 +37964,47 @@ "additionalProperties": {} }, "title": "Environment Variables", - "description": "Variables provide environment-sensitive information to control how your application behaves. To set a Unix environment variable, specify a key of `env:`, and then each sub-item of that is a key/value pair that will be injected into the environment." + "description": "Variables provide environment-sensitive information to control how this container behaves. To set a Unix environment variable, specify a key of `env:`, and then each sub-item of that is a key/value pair that will be injected into the environment" + }, + "container_profile": { + "type": "string", + "nullable": true, + "title": "Container Profile", + "description": "Selected container profile for this container" + }, + "size": { + "type": "string", + "enum": [ + "2XL", + "4XL", + "AUTO", + "L", + "M", + "S", + "XL", + "XS" + ], + "title": "Container Size", + "description": "The container size for this application in production. Leave blank to allow it to be set dynamically." + }, + "disk": { + "type": "integer", + "nullable": true, + "title": "Disk size", + "description": "The size of the disk." + }, + "access": { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "admin", + "contributor", + "viewer" + ] + }, + "title": "Access Information", + "description": "Access information, a mapping between access type and roles." }, "firewall": { "type": "object", @@ -38396,11 +38068,94 @@ "title": "Firewall", "description": "" }, - "container_profile": { - "type": "string", + "egress": { + "type": "object", + "properties": { + "build": { + "type": "object", + "properties": { + "allowed_domains": { + "type": "array", + "items": { + "type": "object", + "properties": { + "host": { + "type": "string", + "title": "Hostname", + "description": "The destination hostname." + }, + "port": { + "type": "integer", + "nullable": true, + "title": "Port", + "description": "Destination port. Omit for the default web ports (80/443), or specify explicitly for non-HTTP destinations (e.g. databases) reached over a CONNECT tunnel." + } + }, + "required": [ + "host", + "port" + ], + "additionalProperties": false + }, + "title": "Allowed Domains", + "description": "Destinations the container is permitted to reach." + } + }, + "required": [ + "allowed_domains" + ], + "additionalProperties": false, + "nullable": true, + "title": "Build-phase egress", + "description": "Egress allowlist applied to build containers." + }, + "runtime": { + "type": "object", + "properties": { + "allowed_domains": { + "type": "array", + "items": { + "type": "object", + "properties": { + "host": { + "type": "string", + "title": "Hostname", + "description": "The destination hostname." + }, + "port": { + "type": "integer", + "nullable": true, + "title": "Port", + "description": "Destination port. Omit for the default web ports (80/443), or specify explicitly for non-HTTP destinations (e.g. databases) reached over a CONNECT tunnel." + } + }, + "required": [ + "host", + "port" + ], + "additionalProperties": false + }, + "title": "Allowed Domains", + "description": "Destinations the container is permitted to reach." + } + }, + "required": [ + "allowed_domains" + ], + "additionalProperties": false, + "nullable": true, + "title": "Runtime-phase egress", + "description": "Egress allowlist applied to runtime containers." + } + }, + "required": [ + "build", + "runtime" + ], + "additionalProperties": false, "nullable": true, - "title": "Container Profile", - "description": "Selected container profile for the application" + "title": "Egress", + "description": "" }, "operations": { "type": "object", @@ -39037,17 +38792,18 @@ }, "required": [ "resources", - "size", - "disk", - "access", "authorizations", "relationships", "additional_hosts", "mounts", "timezone", "variables", - "firewall", "container_profile", + "size", + "disk", + "access", + "firewall", + "egress", "operations", "name", "type", @@ -39247,40 +39003,6 @@ "title": "Resources", "description": "" }, - "size": { - "type": "string", - "enum": [ - "2XL", - "4XL", - "AUTO", - "L", - "M", - "S", - "XL", - "XS" - ], - "title": "Container Size", - "description": "The container size for this application in production. Leave blank to allow it to be set dynamically." - }, - "disk": { - "type": "integer", - "nullable": true, - "title": "Disk size", - "description": "The writeable disk size to reserve on this application container." - }, - "access": { - "type": "object", - "additionalProperties": { - "type": "string", - "enum": [ - "admin", - "contributor", - "viewer" - ] - }, - "title": "Access Information", - "description": "Access information, a mapping between access type and roles." - }, "authorizations": { "type": "array", "items": { @@ -39315,7 +39037,7 @@ "additionalProperties": false }, "title": "Authorizations", - "description": "Authorizations available to this container." + "description": "Authorizations available to this container" }, "relationships": { "type": "object", @@ -39343,7 +39065,7 @@ "nullable": true }, "title": "Service Relationships", - "description": "The relationships of the application to defined services." + "description": "The relationships of the container to defined services" }, "additional_hosts": { "type": "object", @@ -39390,7 +39112,7 @@ "additionalProperties": false }, "title": "Mounts", - "description": "Filesystem mounts of this application. If not specified the application will have no writeable disk space." + "description": "Filesystem mounts of this container. If not specified the container will have no writeable disk space" }, "timezone": { "type": "string", @@ -39405,7 +39127,47 @@ "additionalProperties": {} }, "title": "Environment Variables", - "description": "Variables provide environment-sensitive information to control how your application behaves. To set a Unix environment variable, specify a key of `env:`, and then each sub-item of that is a key/value pair that will be injected into the environment." + "description": "Variables provide environment-sensitive information to control how this container behaves. To set a Unix environment variable, specify a key of `env:`, and then each sub-item of that is a key/value pair that will be injected into the environment" + }, + "container_profile": { + "type": "string", + "nullable": true, + "title": "Container Profile", + "description": "Selected container profile for this container" + }, + "size": { + "type": "string", + "enum": [ + "2XL", + "4XL", + "AUTO", + "L", + "M", + "S", + "XL", + "XS" + ], + "title": "Container Size", + "description": "The container size for this application in production. Leave blank to allow it to be set dynamically." + }, + "disk": { + "type": "integer", + "nullable": true, + "title": "Disk size", + "description": "The writeable disk size to reserve on this application container." + }, + "access": { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "admin", + "contributor", + "viewer" + ] + }, + "title": "Access Information", + "description": "Access information, a mapping between access type and roles." }, "firewall": { "type": "object", @@ -39469,11 +39231,94 @@ "title": "Firewall", "description": "" }, - "container_profile": { - "type": "string", + "egress": { + "type": "object", + "properties": { + "build": { + "type": "object", + "properties": { + "allowed_domains": { + "type": "array", + "items": { + "type": "object", + "properties": { + "host": { + "type": "string", + "title": "Hostname", + "description": "The destination hostname." + }, + "port": { + "type": "integer", + "nullable": true, + "title": "Port", + "description": "Destination port. Omit for the default web ports (80/443), or specify explicitly for non-HTTP destinations (e.g. databases) reached over a CONNECT tunnel." + } + }, + "required": [ + "host", + "port" + ], + "additionalProperties": false + }, + "title": "Allowed Domains", + "description": "Destinations the container is permitted to reach." + } + }, + "required": [ + "allowed_domains" + ], + "additionalProperties": false, + "nullable": true, + "title": "Build-phase egress", + "description": "Egress allowlist applied to build containers." + }, + "runtime": { + "type": "object", + "properties": { + "allowed_domains": { + "type": "array", + "items": { + "type": "object", + "properties": { + "host": { + "type": "string", + "title": "Hostname", + "description": "The destination hostname." + }, + "port": { + "type": "integer", + "nullable": true, + "title": "Port", + "description": "Destination port. Omit for the default web ports (80/443), or specify explicitly for non-HTTP destinations (e.g. databases) reached over a CONNECT tunnel." + } + }, + "required": [ + "host", + "port" + ], + "additionalProperties": false + }, + "title": "Allowed Domains", + "description": "Destinations the container is permitted to reach." + } + }, + "required": [ + "allowed_domains" + ], + "additionalProperties": false, + "nullable": true, + "title": "Runtime-phase egress", + "description": "Egress allowlist applied to runtime containers." + } + }, + "required": [ + "build", + "runtime" + ], + "additionalProperties": false, "nullable": true, - "title": "Container Profile", - "description": "Selected container profile for the application" + "title": "Egress", + "description": "" }, "operations": { "type": "object", @@ -39710,17 +39555,18 @@ }, "required": [ "resources", - "size", - "disk", - "access", "authorizations", "relationships", "additional_hosts", "mounts", "timezone", "variables", - "firewall", "container_profile", + "size", + "disk", + "access", + "firewall", + "egress", "operations", "name", "type", @@ -46377,6 +46223,27 @@ "description": "", "x-isDateTime": false }, + "egress_proxy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "If true, the egress proxy is on for every container in the project: outbound traffic is denied by default and must traverse the proxy. Applies project-wide regardless of whether an application declares an egress allowlist.", + "x-description": [ + "If true, the egress proxy is on for every container in the project: outbound traffic is denied by default and", + "must traverse the proxy. Applies project-wide regardless of whether an application declares an egress allowlist." + ] + } + }, + "required": [ + "enabled" + ], + "additionalProperties": false, + "title": "Egress Proxy", + "description": "", + "x-isDateTime": false + }, "metrics": { "type": "object", "properties": { @@ -46628,10 +46495,19 @@ "x-description": [ "If true, autoscaling can be configured." ] + }, + "supports_horizontal_scaling_services": { + "type": "boolean", + "title": "Enable autoscaling for services", + "description": "Allow autoscaling to be configured for services (not just apps and workers)", + "x-description": [ + "Allow autoscaling to be configured for services (not just apps and workers)" + ] } }, "required": [ - "enabled" + "enabled", + "supports_horizontal_scaling_services" ], "additionalProperties": false, "title": "Autoscaling", @@ -49432,262 +49308,6 @@ "$ref": "#/components/schemas/Ref" } }, - "RegistryCredential": { - "type": "object", - "properties": { - "id": { - "type": "string", - "title": "RegistryCredential Identifier", - "description": "The identifier of RegistryCredential", - "x-isDateTime": false, - "x-description": [ - "The identifier of RegistryCredential" - ] - }, - "created_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "title": "Creation date", - "description": "The creation date", - "x-isDateTime": true, - "x-description": [ - "The creation date" - ] - }, - "updated_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "title": "Update date", - "description": "The update date", - "x-isDateTime": true, - "x-description": [ - "The update date" - ] - }, - "registry": { - "type": "string", - "title": "Registry URL", - "description": "Registry hostname with optional port and optional path namespace, e.g. 'ghcr.io', 'registry.example.com:5000', 'quay.io/myorg' (no scheme, no trailing slash, lowercase only)", - "x-isDateTime": false, - "x-description": [ - "Registry hostname with optional port and optional path namespace, e.g. 'ghcr.io', 'registry.example.com:5000',", - "'quay.io/myorg' (no scheme, no trailing slash, lowercase only)" - ] - }, - "auth": { - "type": "object", - "properties": { - "username": { - "type": "string", - "title": "Username", - "description": "Username used to basic auth to container registry", - "x-description": [ - "Username used to basic auth to container registry" - ] - }, - "password": { - "type": "string", - "title": "Password", - "description": "Password used to basic auth to container registry", - "x-description": [ - "Password used to basic auth to container registry" - ] - } - }, - "required": [ - "username", - "password" - ], - "additionalProperties": false, - "nullable": true, - "title": "Basic Auth", - "description": "Basic Auth for the container registry", - "x-isDateTime": false, - "x-description": [ - "Basic Auth for the container registry" - ] - }, - "identity_token": { - "type": "string", - "nullable": true, - "title": "Identity Token", - "description": "Refresh token to exchange for bearer token with container registry auth", - "x-isDateTime": false, - "x-description": [ - "Refresh token to exchange for bearer token with container registry auth" - ] - }, - "registry_token": { - "type": "string", - "nullable": true, - "title": "Registry token", - "description": "Bearer token used to auth with container registry auth", - "x-isDateTime": false, - "x-description": [ - "Bearer token used to auth with container registry auth" - ] - } - }, - "required": [ - "id", - "created_at", - "updated_at", - "registry", - "auth", - "identity_token", - "registry_token" - ], - "additionalProperties": false - }, - "RegistryCredentialCollection": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RegistryCredential" - } - }, - "RegistryCredentialCreateInput": { - "type": "object", - "properties": { - "registry": { - "type": "string", - "title": "Registry URL", - "description": "Registry hostname with optional port and optional path namespace, e.g. 'ghcr.io', 'registry.example.com:5000', 'quay.io/myorg' (no scheme, no trailing slash, lowercase only)", - "x-isDateTime": false, - "x-description": [ - "Registry hostname with optional port and optional path namespace, e.g. 'ghcr.io', 'registry.example.com:5000',", - "'quay.io/myorg' (no scheme, no trailing slash, lowercase only)" - ] - }, - "auth": { - "type": "object", - "properties": { - "username": { - "type": "string", - "title": "Username", - "description": "Username used to basic auth to container registry", - "x-description": [ - "Username used to basic auth to container registry" - ] - }, - "password": { - "type": "string", - "title": "Password", - "description": "Password used to basic auth to container registry", - "x-description": [ - "Password used to basic auth to container registry" - ] - } - }, - "required": [ - "username", - "password" - ], - "additionalProperties": false, - "nullable": true, - "title": "Basic Auth", - "description": "Basic Auth for the container registry", - "x-isDateTime": false, - "x-description": [ - "Basic Auth for the container registry" - ] - }, - "identity_token": { - "type": "string", - "nullable": true, - "title": "Identity Token", - "description": "Refresh token to exchange for bearer token with container registry auth", - "x-isDateTime": false, - "x-description": [ - "Refresh token to exchange for bearer token with container registry auth" - ] - }, - "registry_token": { - "type": "string", - "nullable": true, - "title": "Registry token", - "description": "Bearer token used to auth with container registry auth", - "x-isDateTime": false, - "x-description": [ - "Bearer token used to auth with container registry auth" - ] - } - }, - "required": [ - "registry" - ], - "additionalProperties": false - }, - "RegistryCredentialPatch": { - "type": "object", - "properties": { - "registry": { - "type": "string", - "title": "Registry URL", - "description": "Registry hostname with optional port and optional path namespace, e.g. 'ghcr.io', 'registry.example.com:5000', 'quay.io/myorg' (no scheme, no trailing slash, lowercase only)", - "x-isDateTime": false, - "x-description": [ - "Registry hostname with optional port and optional path namespace, e.g. 'ghcr.io', 'registry.example.com:5000',", - "'quay.io/myorg' (no scheme, no trailing slash, lowercase only)" - ] - }, - "auth": { - "type": "object", - "properties": { - "username": { - "type": "string", - "title": "Username", - "description": "Username used to basic auth to container registry", - "x-description": [ - "Username used to basic auth to container registry" - ] - }, - "password": { - "type": "string", - "title": "Password", - "description": "Password used to basic auth to container registry", - "x-description": [ - "Password used to basic auth to container registry" - ] - } - }, - "required": [ - "username", - "password" - ], - "additionalProperties": false, - "nullable": true, - "title": "Basic Auth", - "description": "Basic Auth for the container registry", - "x-isDateTime": false, - "x-description": [ - "Basic Auth for the container registry" - ] - }, - "identity_token": { - "type": "string", - "nullable": true, - "title": "Identity Token", - "description": "Refresh token to exchange for bearer token with container registry auth", - "x-isDateTime": false, - "x-description": [ - "Refresh token to exchange for bearer token with container registry auth" - ] - }, - "registry_token": { - "type": "string", - "nullable": true, - "title": "Registry token", - "description": "Bearer token used to auth with container registry auth", - "x-isDateTime": false, - "x-description": [ - "Bearer token used to auth with container registry auth" - ] - } - }, - "additionalProperties": false - }, "ReplacementDomainStorage": { "type": "object", "properties": { @@ -49981,15 +49601,6 @@ "$ref": "#/components/schemas/Route" } }, - "Sbom": { - "type": "object" - }, - "SbomCollection": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Sbom" - } - }, "ScriptIntegration": { "type": "object", "properties": { @@ -51510,70 +51121,77 @@ "The identifier of Task" ] }, - "type": { - "type": "string", - "title": "Image type", - "description": "The runtime type and version for the task (e.g., python:3.8)", - "x-isDateTime": false, - "x-description": [ - "The runtime type and version for the task (e.g., python:3.8)" - ] - }, - "source": { + "resources": { "type": "object", "properties": { - "root": { - "type": "string", - "title": "Task Source Root", - "description": "The root of the task relative to the repository root", + "base_memory": { + "type": "integer", + "title": "Base memory", + "description": "The base memory for the container", "x-description": [ - "The root of the task relative to the repository root" + "The base memory for the container" + ] + }, + "memory_ratio": { + "type": "integer", + "title": "Memory ratio", + "description": "The amount of memory to allocate per units of CPU", + "x-description": [ + "The amount of memory to allocate per units of CPU" ] } }, "required": [ - "root" + "base_memory", + "memory_ratio" ], "additionalProperties": false, - "title": "Source Code Configuration", - "description": "Configuration related to the source code of the task", + "nullable": true, + "title": "Resources", + "description": "Resources configuration (base memory and memory ratio)", "x-isDateTime": false, "x-description": [ - "Configuration related to the source code of the task" + "Resources configuration (base memory and memory ratio)" ] }, - "hooks": { - "type": "object", - "properties": { - "build": { - "type": "string", - "nullable": true, - "title": "Build Hook", - "description": "Hook executed after the build process", - "x-description": [ - "Hook executed after the build process" - ] + "authorizations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "env", + "task" + ], + "title": "Authorization Type", + "description": "The resource type for this authorization." + }, + "action": { + "type": "string", + "title": "Authorization Action", + "description": "The action to authorize on the target resource." + }, + "resource": { + "type": "string", + "nullable": true, + "title": "Authorization Resource", + "description": "The target resource for this authorization." + } }, - "deploy": { - "type": "string", - "nullable": true, - "title": "Deploy Hook", - "description": "Hook executed before the task's run command", - "x-description": [ - "Hook executed before the task's run command" - ] - } + "required": [ + "type", + "action", + "resource" + ], + "additionalProperties": false }, - "required": [ - "build", - "deploy" - ], - "additionalProperties": false, - "title": "Hooks", - "description": "Scripts executed at various points in the lifecycle of the task", + "title": "Authorizations", + "description": "Authorizations available to this container", "x-isDateTime": false, "x-description": [ - "Scripts executed at various points in the lifecycle of the task" + "Authorizations available to this container" ] }, "relationships": { @@ -51601,11 +51219,11 @@ "additionalProperties": false, "nullable": true }, - "title": "Relationships", - "description": "The relationships of the task to defined services and applications", + "title": "Service Relationships", + "description": "The relationships of the container to defined services", "x-isDateTime": false, "x-description": [ - "The relationships of the task to defined services and applications" + "The relationships of the container to defined services" ] }, "additional_hosts": { @@ -51614,10 +51232,10 @@ "type": "string" }, "title": "Additional hosts", - "description": "A mapping of hostname to ip address to be added to the task container's hosts file", + "description": "A mapping of hostname to ip address to be added to the container's hosts file", "x-isDateTime": false, "x-description": [ - "A mapping of hostname to ip address to be added to the task container's hosts file" + "A mapping of hostname to ip address to be added to the container's hosts file" ] }, "mounts": { @@ -51657,10 +51275,10 @@ "additionalProperties": false }, "title": "Mounts", - "description": "Filesystem mounts of this task", + "description": "Filesystem mounts of this container. If not specified the container will have no writeable disk space", "x-isDateTime": false, "x-description": [ - "Filesystem mounts of this task" + "Filesystem mounts of this container. If not specified the container will have no writeable disk space" ] }, "timezone": { @@ -51680,10 +51298,88 @@ "additionalProperties": {} }, "title": "Environment Variables", - "description": "Variables provide environment-sensitive information to control how your task behaves", + "description": "Variables provide environment-sensitive information to control how this container behaves. To set a Unix environment variable, specify a key of `env:`, and then each sub-item of that is a key/value pair that will be injected into the environment", + "x-isDateTime": false, + "x-description": [ + "Variables provide environment-sensitive information to control how this container behaves. To set a Unix", + "environment variable, specify a key of `env:`, and then each sub-item of that is a key/value pair that will be", + "injected into the environment" + ] + }, + "container_profile": { + "type": "string", + "nullable": true, + "title": "Container Profile", + "description": "Selected container profile for this container", + "x-isDateTime": false, + "x-description": [ + "Selected container profile for this container" + ] + }, + "type": { + "type": "string", + "title": "Image type", + "description": "The runtime type and version for the task (e.g., python:3.8)", + "x-isDateTime": false, + "x-description": [ + "The runtime type and version for the task (e.g., python:3.8)" + ] + }, + "source": { + "type": "object", + "properties": { + "root": { + "type": "string", + "title": "Task Source Root", + "description": "The root of the task relative to the repository root", + "x-description": [ + "The root of the task relative to the repository root" + ] + } + }, + "required": [ + "root" + ], + "additionalProperties": false, + "title": "Source Code Configuration", + "description": "Configuration related to the source code of the task", "x-isDateTime": false, "x-description": [ - "Variables provide environment-sensitive information to control how your task behaves" + "Configuration related to the source code of the task" + ] + }, + "hooks": { + "type": "object", + "properties": { + "build": { + "type": "string", + "nullable": true, + "title": "Build Hook", + "description": "Hook executed after the build process", + "x-description": [ + "Hook executed after the build process" + ] + }, + "deploy": { + "type": "string", + "nullable": true, + "title": "Deploy Hook", + "description": "Hook executed before the task's run command", + "x-description": [ + "Hook executed before the task's run command" + ] + } + }, + "required": [ + "build", + "deploy" + ], + "additionalProperties": false, + "title": "Hooks", + "description": "Scripts executed at various points in the lifecycle of the task", + "x-isDateTime": false, + "x-description": [ + "Scripts executed at various points in the lifecycle of the task" ] }, "dependencies": { @@ -51707,46 +51403,6 @@ "Runtime-specific configuration" ] }, - "authorizations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "env", - "task" - ], - "title": "Authorization Type", - "description": "The resource type for this authorization." - }, - "action": { - "type": "string", - "title": "Authorization Action", - "description": "The action to authorize on the target resource." - }, - "resource": { - "type": "string", - "nullable": true, - "title": "Authorization Resource", - "description": "The target resource for this authorization." - } - }, - "required": [ - "type", - "action", - "resource" - ], - "additionalProperties": false - }, - "title": "Authorizations", - "description": "Authorizations available to this task", - "x-isDateTime": false, - "x-description": [ - "Authorizations available to this task" - ] - }, "run": { "type": "object", "properties": { @@ -51779,49 +51435,6 @@ "Configuration for task execution" ] }, - "resources": { - "type": "object", - "properties": { - "base_memory": { - "type": "integer", - "title": "Base memory", - "description": "The base memory for the container", - "x-description": [ - "The base memory for the container" - ] - }, - "memory_ratio": { - "type": "integer", - "title": "Memory ratio", - "description": "The amount of memory to allocate per units of CPU", - "x-description": [ - "The amount of memory to allocate per units of CPU" - ] - } - }, - "required": [ - "base_memory", - "memory_ratio" - ], - "additionalProperties": false, - "nullable": true, - "title": "Resources", - "description": "Resources configuration (base memory and memory ratio)", - "x-isDateTime": false, - "x-description": [ - "Resources configuration (base memory and memory ratio)" - ] - }, - "container_profile": { - "type": "string", - "nullable": true, - "title": "Container Profile", - "description": "Selected container profile for the task", - "x-isDateTime": false, - "x-description": [ - "Selected container profile for the task" - ] - }, "name": { "type": "string", "title": "Task name", @@ -51834,20 +51447,20 @@ }, "required": [ "id", - "type", - "source", - "hooks", + "resources", + "authorizations", "relationships", "additional_hosts", "mounts", "timezone", "variables", + "container_profile", + "type", + "source", + "hooks", "dependencies", "runtime", - "authorizations", "run", - "resources", - "container_profile", "name" ], "additionalProperties": false @@ -58139,6 +57752,16 @@ } } }, + "UnprocessableEntity": { + "description": "Unprocessable Entity", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "ServiceUnavailable": { "description": "Service Unavailable", "content": { diff --git a/src/Api/AbstractApi.php b/src/Api/AbstractApi.php index 14c60c1d..da045241 100644 --- a/src/Api/AbstractApi.php +++ b/src/Api/AbstractApi.php @@ -3,6 +3,7 @@ namespace Upsun\Api; use Exception; +use Generator; use Http\Client\Common\Plugin\RedirectPlugin; use Http\Client\Common\PluginClientFactory; use Http\Discovery\Psr17FactoryDiscovery; @@ -268,6 +269,63 @@ protected function handleResponseWithDataType( return ObjectSerializer::deserialize($content, $dataType, []); } + /** + * Stream newline-delimited JSON responses as model instances. + * + * @throws ApiException + * @return Generator + */ + protected function handleNdjsonStreamWithDataType( + string $itemType, + RequestInterface $request, + ResponseInterface $response + ): Generator { + $stream = $response->getBody(); + $buffer = ''; + + while (!$stream->eof()) { + $buffer .= $stream->read(8192); + + while (($lineBreakPos = strpos($buffer, "\n")) !== false) { + $line = trim(substr($buffer, 0, $lineBreakPos)); + $buffer = substr($buffer, $lineBreakPos + 1); + + if ($line === '') { + continue; + } + + try { + $decoded = json_decode($line, false, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new ApiException( + sprintf('Error JSON decoding NDJSON line (%s)', $request->getUri()), + $request, + $response, + $exception + ); + } + + yield ObjectSerializer::deserialize($decoded, $itemType, []); + } + } + + $tail = trim($buffer); + if ($tail !== '') { + try { + $decoded = json_decode($tail, false, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new ApiException( + sprintf('Error JSON decoding NDJSON tail (%s)', $request->getUri()), + $request, + $response, + $exception + ); + } + + yield ObjectSerializer::deserialize($decoded, $itemType, []); + } + } + /** * Deserialize generic types array * @throws Exception diff --git a/src/Api/AddOnsApi.php b/src/Api/AddOnsApi.php index 75edd96f..8e364b5c 100644 --- a/src/Api/AddOnsApi.php +++ b/src/Api/AddOnsApi.php @@ -61,6 +61,8 @@ public function __construct( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Add-ons/operation/get-org-addons + * + * @deprecated */ public function getOrgAddons( string $organizationId @@ -73,6 +75,8 @@ public function getOrgAddons( /** * Get add-ons with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -122,6 +126,8 @@ private function getOrgAddonsWithHttpInfo( * name instead. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function getOrgAddonsRequest( string $organizationId diff --git a/src/Api/ApiException.php b/src/Api/ApiException.php index ee41ce65..82e722ce 100644 --- a/src/Api/ApiException.php +++ b/src/Api/ApiException.php @@ -30,7 +30,7 @@ class ApiException extends RequestException /** * The deserialized response object */ - private mixed $responseObject; + private mixed $responseObject = null; public function __construct( $message, @@ -43,6 +43,17 @@ public function __construct( $this->responseHeaders = $response->getHeaders(); $this->responseBody = (string) $response->getBody(); $this->code = $response->getStatusCode(); + } elseif ($previous instanceof self) { + // No response in this scope (the operation-level catch in the + // generated *Api.php re-wraps the original ApiException with a + // null response). Inherit the wrapped exception's context so the + // HTTP status, body, headers and deserialized error are preserved. + $this->responseHeaders = $previous->getResponseHeaders(); + $this->responseBody = $previous->getResponseBody(); + $this->responseObject = $previous->getResponseObject(); + if ($previous->getCode() !== 0) { + $this->code = $previous->getCode(); + } } } diff --git a/src/Api/DiscountsApi.php b/src/Api/DiscountsApi.php index 4eefdae1..73d03204 100644 --- a/src/Api/DiscountsApi.php +++ b/src/Api/DiscountsApi.php @@ -338,6 +338,8 @@ private function getTypeAllowanceRequest(): RequestInterface * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Discounts/operation/list-org-discounts + * + * @deprecated */ public function listOrgDiscounts( string $organizationId @@ -350,6 +352,8 @@ public function listOrgDiscounts( /** * List organization discounts with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -399,6 +403,8 @@ private function listOrgDiscountsWithHttpInfo( * name instead. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function listOrgDiscountsRequest( string $organizationId diff --git a/src/Api/InvoicesApi.php b/src/Api/InvoicesApi.php index 2c74fce1..10575485 100644 --- a/src/Api/InvoicesApi.php +++ b/src/Api/InvoicesApi.php @@ -64,6 +64,8 @@ public function __construct( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Invoices/operation/get-org-invoice + * + * @deprecated */ public function getOrgInvoice( string $invoiceId, @@ -78,6 +80,8 @@ public function getOrgInvoice( /** * Get invoice with HTTP Info * + * @deprecated + * * @param string $invoiceId * The ID of the invoice. (required) * @param string $organizationId @@ -133,6 +137,8 @@ private function getOrgInvoiceWithHttpInfo( * name instead. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function getOrgInvoiceRequest( string $invoiceId, @@ -256,6 +262,8 @@ private function getOrgInvoiceRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Invoices/operation/list-org-invoices + * + * @deprecated */ public function listOrgInvoices( string $organizationId, @@ -276,6 +284,8 @@ public function listOrgInvoices( /** * List invoices with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -351,6 +361,8 @@ private function listOrgInvoicesWithHttpInfo( * Page to be displayed. Defaults to 1. (optional) * * @throws InvalidArgumentException + * + * @deprecated */ private function listOrgInvoicesRequest( string $organizationId, diff --git a/src/Api/OrdersApi.php b/src/Api/OrdersApi.php index 7d083f5c..6d115fa7 100644 --- a/src/Api/OrdersApi.php +++ b/src/Api/OrdersApi.php @@ -65,6 +65,8 @@ public function __construct( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Orders/operation/create-authorization-credentials + * + * @deprecated */ public function createAuthorizationCredentials( string $organizationId, @@ -79,6 +81,8 @@ public function createAuthorizationCredentials( /** * Create confirmation credentials for for 3D-Secure with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -134,6 +138,8 @@ private function createAuthorizationCredentialsWithHttpInfo( * The ID of the order. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function createAuthorizationCredentialsRequest( string $organizationId, @@ -406,6 +412,8 @@ private function downloadInvoiceRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Orders/operation/get-org-order + * + * @deprecated */ public function getOrgOrder( string $organizationId, @@ -422,6 +430,8 @@ public function getOrgOrder( /** * Get order with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -483,6 +493,8 @@ private function getOrgOrderWithHttpInfo( * The output mode. (optional) * * @throws InvalidArgumentException + * + * @deprecated */ private function getOrgOrderRequest( string $organizationId, @@ -619,6 +631,8 @@ private function getOrgOrderRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Orders/operation/list-org-orders + * + * @deprecated */ public function listOrgOrders( string $organizationId, @@ -639,6 +653,8 @@ public function listOrgOrders( /** * List orders with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -712,6 +728,8 @@ private function listOrgOrdersWithHttpInfo( * The output mode. (optional) * * @throws InvalidArgumentException + * + * @deprecated */ private function listOrgOrdersRequest( string $organizationId, diff --git a/src/Api/OrganizationManagementApi.php b/src/Api/OrganizationManagementApi.php index 21c67cbb..1f0067b4 100644 --- a/src/Api/OrganizationManagementApi.php +++ b/src/Api/OrganizationManagementApi.php @@ -64,6 +64,8 @@ public function __construct( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Organization-Management/operation/estimate-org + * + * @deprecated */ public function estimateOrg( string $organizationId @@ -76,6 +78,8 @@ public function estimateOrg( /** * Estimate total spend with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -125,6 +129,8 @@ private function estimateOrgWithHttpInfo( * name instead. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function estimateOrgRequest( string $organizationId @@ -222,6 +228,8 @@ private function estimateOrgRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Organization-Management/operation/get-org-billing-alert-config + * + * @deprecated */ public function getOrgBillingAlertConfig( string $organizationId @@ -234,6 +242,8 @@ public function getOrgBillingAlertConfig( /** * Get billing alert configuration with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -283,6 +293,8 @@ private function getOrgBillingAlertConfigWithHttpInfo( * name instead. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function getOrgBillingAlertConfigRequest( string $organizationId @@ -379,6 +391,8 @@ private function getOrgBillingAlertConfigRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Organization-Management/operation/get-org-prepayment-info + * + * @deprecated */ public function getOrgPrepaymentInfo( string $organizationId @@ -391,6 +405,8 @@ public function getOrgPrepaymentInfo( /** * Get organization prepayment information with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. (required) * @@ -438,6 +454,8 @@ private function getOrgPrepaymentInfoWithHttpInfo( * The ID of the organization. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function getOrgPrepaymentInfoRequest( string $organizationId @@ -534,6 +552,8 @@ private function getOrgPrepaymentInfoRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Organization-Management/operation/list-org-prepayment-transactions + * + * @deprecated */ public function listOrgPrepaymentTransactions( string $organizationId @@ -546,6 +566,8 @@ public function listOrgPrepaymentTransactions( /** * List organization prepayment transactions with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. (required) * @@ -593,6 +615,8 @@ private function listOrgPrepaymentTransactionsWithHttpInfo( * The ID of the organization. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function listOrgPrepaymentTransactionsRequest( string $organizationId @@ -690,6 +714,8 @@ private function listOrgPrepaymentTransactionsRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Organization-Management/operation/update-org-billing-alert-config + * + * @deprecated */ public function updateOrgBillingAlertConfig( string $organizationId, @@ -704,6 +730,8 @@ public function updateOrgBillingAlertConfig( /** * Update billing alert configuration with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -755,6 +783,8 @@ private function updateOrgBillingAlertConfigWithHttpInfo( * name instead. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function updateOrgBillingAlertConfigRequest( string $organizationId, diff --git a/src/Api/OrganizationProjectsApi.php b/src/Api/OrganizationProjectsApi.php index a9c6095e..d13fc137 100644 --- a/src/Api/OrganizationProjectsApi.php +++ b/src/Api/OrganizationProjectsApi.php @@ -1247,7 +1247,7 @@ private function queryProjectCarbonRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * - * @return \Upsun\Model\ProvisionEvent + * @return Generator * @see https://docs.upsun.com/api/#tag/Organization-Projects/operation/stream-org-project-provisioning */ public function streamOrgProjectProvisioning( @@ -1267,7 +1267,7 @@ public function streamOrgProjectProvisioning( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * - * @return \Upsun\Model\ProvisionEvent + * @return Generator */ private function streamOrgProjectProvisioningWithHttpInfo( string $organizationId @@ -1284,7 +1284,7 @@ private function streamOrgProjectProvisioningWithHttpInfo( $request->getBody() ); - return $this->handleResponseWithDataType( + return $this->handleNdjsonStreamWithDataType( '\Upsun\Model\ProvisionEvent', $request, $response diff --git a/src/Api/ProfilesApi.php b/src/Api/ProfilesApi.php index 91a71b3b..ec5ae610 100644 --- a/src/Api/ProfilesApi.php +++ b/src/Api/ProfilesApi.php @@ -62,6 +62,8 @@ public function __construct( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Profiles/operation/get-org-address + * + * @deprecated */ public function getOrgAddress( string $organizationId @@ -74,6 +76,8 @@ public function getOrgAddress( /** * Get address with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -123,6 +127,8 @@ private function getOrgAddressWithHttpInfo( * name instead. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function getOrgAddressRequest( string $organizationId @@ -220,6 +226,8 @@ private function getOrgAddressRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Profiles/operation/get-org-profile + * + * @deprecated */ public function getOrgProfile( string $organizationId @@ -232,6 +240,8 @@ public function getOrgProfile( /** * Get profile with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -281,6 +291,8 @@ private function getOrgProfileWithHttpInfo( * name instead. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function getOrgProfileRequest( string $organizationId @@ -377,6 +389,8 @@ private function getOrgProfileRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Profiles/operation/update-org-address + * + * @deprecated */ public function updateOrgAddress( string $organizationId, @@ -391,6 +405,8 @@ public function updateOrgAddress( /** * Update address with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. (required) * @@ -440,6 +456,8 @@ private function updateOrgAddressWithHttpInfo( * The ID of the organization. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function updateOrgAddressRequest( string $organizationId, @@ -545,6 +563,8 @@ private function updateOrgAddressRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Profiles/operation/update-org-profile + * + * @deprecated */ public function updateOrgProfile( string $organizationId, @@ -559,6 +579,8 @@ public function updateOrgProfile( /** * Update profile with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. (required) * @@ -608,6 +630,8 @@ private function updateOrgProfileWithHttpInfo( * The ID of the organization. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function updateOrgProfileRequest( string $organizationId, diff --git a/src/Api/RecordsApi.php b/src/Api/RecordsApi.php index d28dded6..99c508b8 100644 --- a/src/Api/RecordsApi.php +++ b/src/Api/RecordsApi.php @@ -86,6 +86,8 @@ public function __construct( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Records/operation/list-org-plan-records + * + * @deprecated */ public function listOrgPlanRecords( string $organizationId, @@ -114,6 +116,8 @@ public function listOrgPlanRecords( /** * List plan records with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -227,6 +231,8 @@ private function listOrgPlanRecordsWithHttpInfo( * Page to be displayed. Defaults to 1. (optional) * * @throws InvalidArgumentException + * + * @deprecated */ private function listOrgPlanRecordsRequest( string $organizationId, @@ -450,6 +456,8 @@ private function listOrgPlanRecordsRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Records/operation/list-org-usage-records + * + * @deprecated */ public function listOrgUsageRecords( string $organizationId, @@ -472,6 +480,8 @@ public function listOrgUsageRecords( /** * List usage records with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -559,6 +569,8 @@ private function listOrgUsageRecordsWithHttpInfo( * Page to be displayed. Defaults to 1. (optional) * * @throws InvalidArgumentException + * + * @deprecated */ private function listOrgUsageRecordsRequest( string $organizationId, diff --git a/src/Api/RegistryCredentialApi.php b/src/Api/RegistryCredentialApi.php index 5b023dd3..abd45ebd 100644 --- a/src/Api/RegistryCredentialApi.php +++ b/src/Api/RegistryCredentialApi.php @@ -21,7 +21,7 @@ * Low level RegistryCredentialApi (auto-generated) * * @license Apache-2.0 - * @see https://docs.upsun.com + * @see https://developer.upsun.com * @generated This file was generated by OpenAPI Generator. Do not edit manually. */ final class RegistryCredentialApi extends AbstractApi @@ -57,7 +57,7 @@ public function __construct( * * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface - * @see https://docs.upsun.com/api/#tag/RegistryCredential/operation/create-projects-oci-registries + * @see https://developer.upsun.com/api/#tag/RegistryCredential/operation/create-projects-oci-registries */ public function createProjectsOciRegistries( string $projectId, @@ -224,7 +224,7 @@ private function createProjectsOciRegistriesRequest( * * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface - * @see https://docs.upsun.com/api/#tag/RegistryCredential/operation/delete-projects-oci-registries + * @see https://developer.upsun.com/api/#tag/RegistryCredential/operation/delete-projects-oci-registries */ public function deleteProjectsOciRegistries( string $projectId, @@ -390,7 +390,7 @@ private function deleteProjectsOciRegistriesRequest( * * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface - * @see https://docs.upsun.com/api/#tag/RegistryCredential/operation/get-projects-oci-registries + * @see https://developer.upsun.com/api/#tag/RegistryCredential/operation/get-projects-oci-registries */ public function getProjectsOciRegistries( string $projectId, @@ -558,7 +558,7 @@ private function getProjectsOciRegistriesRequest( * @throws ClientExceptionInterface * * @return \Upsun\Model\RegistryCredential[] - * @see https://docs.upsun.com/api/#tag/RegistryCredential/operation/list-projects-oci-registries + * @see https://developer.upsun.com/api/#tag/RegistryCredential/operation/list-projects-oci-registries */ public function listProjectsOciRegistries( string $projectId @@ -706,7 +706,7 @@ private function listProjectsOciRegistriesRequest( * * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface - * @see https://docs.upsun.com/api/#tag/RegistryCredential/operation/update-projects-oci-registries + * @see https://developer.upsun.com/api/#tag/RegistryCredential/operation/update-projects-oci-registries */ public function updateProjectsOciRegistries( string $projectId, diff --git a/src/Api/SbomApi.php b/src/Api/SbomApi.php index fbb0dfe2..01b9ae70 100644 --- a/src/Api/SbomApi.php +++ b/src/Api/SbomApi.php @@ -18,7 +18,7 @@ * Low level SbomApi (auto-generated) * * @license Apache-2.0 - * @see https://docs.upsun.com + * @see https://developer.upsun.com * @generated This file was generated by OpenAPI Generator. Do not edit manually. */ final class SbomApi extends AbstractApi @@ -53,7 +53,7 @@ public function __construct( * * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface - * @see https://docs.upsun.com/api/#tag/Sbom/operation/get-projects-environments-deployments-sboms + * @see https://developer.upsun.com/api/#tag/Sbom/operation/get-projects-environments-deployments-sboms */ public function getProjectsEnvironmentsDeploymentsSboms( string $projectId, @@ -263,7 +263,7 @@ private function getProjectsEnvironmentsDeploymentsSbomsRequest( * @throws ClientExceptionInterface * * @return object[] - * @see https://docs.upsun.com/api/#tag/Sbom/operation/list-projects-environments-deployments-sboms + * @see https://developer.upsun.com/api/#tag/Sbom/operation/list-projects-environments-deployments-sboms */ public function listProjectsEnvironmentsDeploymentsSboms( string $projectId, diff --git a/src/Api/Serializer/ApiObjectAttributesMapper.php b/src/Api/Serializer/ApiObjectAttributesMapper.php index b6871f47..171dada3 100644 --- a/src/Api/Serializer/ApiObjectAttributesMapper.php +++ b/src/Api/Serializer/ApiObjectAttributesMapper.php @@ -100,6 +100,10 @@ public static function attributeMap(string $classname): array 'updatedAt' => 'updated_at', 'config' => 'config' ], + 'Upsun\Model\AllowedDomainsInner' => [ + 'host' => 'host', + 'port' => 'port' + ], 'Upsun\Model\ApiToken' => [ 'id' => 'id', 'name' => 'name', @@ -196,7 +200,8 @@ public static function attributeMap(string $classname): array 'memoryPressure' => 'memory_pressure' ], 'Upsun\Model\Autoscaling' => [ - 'enabled' => 'enabled' + 'enabled' => 'enabled', + 'supportsHorizontalScalingServices' => 'supports_horizontal_scaling_services' ], 'Upsun\Model\Backup' => [ 'id' => 'id', @@ -211,14 +216,11 @@ public static function attributeMap(string $classname): array 'safe' => 'safe', 'sizeOfVolumes' => 'size_of_volumes', 'sizeUsed' => 'size_used', + 'sizeObjectStorage' => 'size_object_storage', 'deployment' => 'deployment', 'restorable' => 'restorable', 'automated' => 'automated' ], - 'Upsun\Model\BasicAuth' => [ - 'username' => 'username', - 'password' => 'password' - ], 'Upsun\Model\Bitbucket' => [ 'enabled' => 'enabled', 'role' => 'role' @@ -766,6 +768,9 @@ public static function attributeMap(string $classname): array 'flavor' => 'flavor', 'caches' => 'caches' ], + 'Upsun\Model\BuildPhaseEgress' => [ + 'allowedDomains' => 'allowed_domains' + ], 'Upsun\Model\BuildResources' => [ 'enabled' => 'enabled', 'maxCpu' => 'max_cpu', @@ -1326,6 +1331,13 @@ public static function attributeMap(string $classname): array 'attributes' => 'attributes', 'isDefault' => 'is_default' ], + 'Upsun\Model\Egress' => [ + 'build' => 'build', + 'runtime' => 'runtime' + ], + 'Upsun\Model\EgressProxy' => [ + 'enabled' => 'enabled' + ], 'Upsun\Model\EmailIntegration' => [ 'createdAt' => 'created_at', 'updatedAt' => 'updated_at', @@ -3429,6 +3441,7 @@ public static function attributeMap(string $classname): array 'sourceOperations' => 'source_operations', 'runtimeOperations' => 'runtime_operations', 'outboundFirewall' => 'outbound_firewall', + 'egressProxy' => 'egress_proxy', 'integrations' => 'integrations' ], 'Upsun\Model\ProjectCarbon' => [ @@ -3743,27 +3756,6 @@ public static function attributeMap(string $classname): array 'envimpact' => 'envimpact', 'environmentalImpact' => 'environmental_impact' ], - 'Upsun\Model\RegistryCredential' => [ - 'id' => 'id', - 'createdAt' => 'created_at', - 'updatedAt' => 'updated_at', - 'registry' => 'registry', - 'auth' => 'auth', - 'identityToken' => 'identity_token', - 'registryToken' => 'registry_token' - ], - 'Upsun\Model\RegistryCredentialCreateInput' => [ - 'registry' => 'registry', - 'auth' => 'auth', - 'identityToken' => 'identity_token', - 'registryToken' => 'registry_token' - ], - 'Upsun\Model\RegistryCredentialPatch' => [ - 'registry' => 'registry', - 'auth' => 'auth', - 'identityToken' => 'identity_token', - 'registryToken' => 'registry_token' - ], 'Upsun\Model\ReplacementDomainStorage' => [ 'createdAt' => 'created_at', 'updatedAt' => 'updated_at', @@ -4345,6 +4337,9 @@ public static function attributeMap(string $classname): array 'Upsun\Model\RuntimeOperations' => [ 'enabled' => 'enabled' ], + 'Upsun\Model\RuntimePhaseEgress' => [ + 'allowedDomains' => 'allowed_domains' + ], 'Upsun\Model\SSIConfiguration' => [ 'enabled' => 'enabled' ], @@ -4704,20 +4699,20 @@ public static function attributeMap(string $classname): array ], 'Upsun\Model\Task' => [ 'id' => 'id', - 'type' => 'type', - 'source' => 'source', - 'hooks' => 'hooks', + 'resources' => 'resources', + 'authorizations' => 'authorizations', 'relationships' => 'relationships', 'additionalHosts' => 'additional_hosts', 'mounts' => 'mounts', 'timezone' => 'timezone', 'variables' => 'variables', + 'containerProfile' => 'container_profile', + 'type' => 'type', + 'source' => 'source', + 'hooks' => 'hooks', 'dependencies' => 'dependencies', 'runtime' => 'runtime', - 'authorizations' => 'authorizations', 'run' => 'run', - 'resources' => 'resources', - 'containerProfile' => 'container_profile', 'name' => 'name' ], 'Upsun\Model\TaskTriggerInput' => [ @@ -5103,17 +5098,18 @@ public static function attributeMap(string $classname): array ], 'Upsun\Model\WebApplicationsValue' => [ 'resources' => 'resources', - 'size' => 'size', - 'disk' => 'disk', - 'access' => 'access', 'authorizations' => 'authorizations', 'relationships' => 'relationships', 'additionalHosts' => 'additional_hosts', 'mounts' => 'mounts', 'timezone' => 'timezone', 'variables' => 'variables', - 'firewall' => 'firewall', 'containerProfile' => 'container_profile', + 'size' => 'size', + 'disk' => 'disk', + 'access' => 'access', + 'firewall' => 'firewall', + 'egress' => 'egress', 'operations' => 'operations', 'name' => 'name', 'type' => 'type', @@ -5208,17 +5204,18 @@ public static function attributeMap(string $classname): array ], 'Upsun\Model\WorkersValue' => [ 'resources' => 'resources', - 'size' => 'size', - 'disk' => 'disk', - 'access' => 'access', 'authorizations' => 'authorizations', 'relationships' => 'relationships', 'additionalHosts' => 'additional_hosts', 'mounts' => 'mounts', 'timezone' => 'timezone', 'variables' => 'variables', - 'firewall' => 'firewall', 'containerProfile' => 'container_profile', + 'size' => 'size', + 'disk' => 'disk', + 'access' => 'access', + 'firewall' => 'firewall', + 'egress' => 'egress', 'operations' => 'operations', 'name' => 'name', 'type' => 'type', diff --git a/src/Api/Serializer/ApiObjectFormatsMapper.php b/src/Api/Serializer/ApiObjectFormatsMapper.php index 8bf102b1..743f22ce 100644 --- a/src/Api/Serializer/ApiObjectFormatsMapper.php +++ b/src/Api/Serializer/ApiObjectFormatsMapper.php @@ -113,6 +113,11 @@ public static function openApiFormats(string $classname) 'config' => null ], + 'Upsun\Model\AllowedDomainsInner' => [ + 'host' => null, + 'port' => null + ], + 'Upsun\Model\ApiToken' => [ 'id' => 'uuid', 'name' => null, @@ -229,7 +234,8 @@ public static function openApiFormats(string $classname) ], 'Upsun\Model\Autoscaling' => [ - 'enabled' => null + 'enabled' => null, + 'supportsHorizontalScalingServices' => null ], 'Upsun\Model\Backup' => [ @@ -245,16 +251,12 @@ public static function openApiFormats(string $classname) 'safe' => null, 'sizeOfVolumes' => null, 'sizeUsed' => null, + 'sizeObjectStorage' => null, 'deployment' => null, 'restorable' => null, 'automated' => null ], - 'Upsun\Model\BasicAuth' => [ - 'username' => null, - 'password' => null - ], - 'Upsun\Model\Bitbucket' => [ 'enabled' => null, 'role' => null @@ -866,6 +868,10 @@ public static function openApiFormats(string $classname) 'caches' => null ], + 'Upsun\Model\BuildPhaseEgress' => [ + 'allowedDomains' => null + ], + 'Upsun\Model\BuildResources' => [ 'enabled' => null, 'maxCpu' => 'float', @@ -1510,6 +1516,15 @@ public static function openApiFormats(string $classname) 'isDefault' => null ], + 'Upsun\Model\Egress' => [ + 'build' => null, + 'runtime' => null + ], + + 'Upsun\Model\EgressProxy' => [ + 'enabled' => null + ], + 'Upsun\Model\EmailIntegration' => [ 'createdAt' => 'date-time', 'updatedAt' => 'date-time', @@ -3931,6 +3946,7 @@ public static function openApiFormats(string $classname) 'sourceOperations' => null, 'runtimeOperations' => null, 'outboundFirewall' => null, + 'egressProxy' => null, 'integrations' => null ], @@ -4277,30 +4293,6 @@ public static function openApiFormats(string $classname) 'environmentalImpact' => null ], - 'Upsun\Model\RegistryCredential' => [ - 'id' => null, - 'createdAt' => 'date-time', - 'updatedAt' => 'date-time', - 'registry' => null, - 'auth' => null, - 'identityToken' => null, - 'registryToken' => null - ], - - 'Upsun\Model\RegistryCredentialCreateInput' => [ - 'registry' => null, - 'auth' => null, - 'identityToken' => null, - 'registryToken' => null - ], - - 'Upsun\Model\RegistryCredentialPatch' => [ - 'registry' => null, - 'auth' => null, - 'identityToken' => null, - 'registryToken' => null - ], - 'Upsun\Model\ReplacementDomainStorage' => [ 'createdAt' => 'date-time', 'updatedAt' => 'date-time', @@ -4955,6 +4947,10 @@ public static function openApiFormats(string $classname) 'enabled' => null ], + 'Upsun\Model\RuntimePhaseEgress' => [ + 'allowedDomains' => null + ], + 'Upsun\Model\SSIConfiguration' => [ 'enabled' => null ], @@ -5361,20 +5357,20 @@ public static function openApiFormats(string $classname) 'Upsun\Model\Task' => [ 'id' => null, - 'type' => null, - 'source' => null, - 'hooks' => null, + 'resources' => null, + 'authorizations' => null, 'relationships' => null, 'additionalHosts' => null, 'mounts' => null, 'timezone' => null, 'variables' => null, + 'containerProfile' => null, + 'type' => null, + 'source' => null, + 'hooks' => null, 'dependencies' => null, 'runtime' => null, - 'authorizations' => null, 'run' => null, - 'resources' => null, - 'containerProfile' => null, 'name' => null ], @@ -5816,17 +5812,18 @@ public static function openApiFormats(string $classname) 'Upsun\Model\WebApplicationsValue' => [ 'resources' => null, - 'size' => null, - 'disk' => null, - 'access' => null, 'authorizations' => null, 'relationships' => null, 'additionalHosts' => null, 'mounts' => null, 'timezone' => null, 'variables' => null, - 'firewall' => null, 'containerProfile' => null, + 'size' => null, + 'disk' => null, + 'access' => null, + 'firewall' => null, + 'egress' => null, 'operations' => null, 'name' => null, 'type' => null, @@ -5930,17 +5927,18 @@ public static function openApiFormats(string $classname) 'Upsun\Model\WorkersValue' => [ 'resources' => null, - 'size' => null, - 'disk' => null, - 'access' => null, 'authorizations' => null, 'relationships' => null, 'additionalHosts' => null, 'mounts' => null, 'timezone' => null, 'variables' => null, - 'firewall' => null, 'containerProfile' => null, + 'size' => null, + 'disk' => null, + 'access' => null, + 'firewall' => null, + 'egress' => null, 'operations' => null, 'name' => null, 'type' => null, diff --git a/src/Api/Serializer/ApiObjectTypesMapper.php b/src/Api/Serializer/ApiObjectTypesMapper.php index 465d7605..6d3ef20c 100644 --- a/src/Api/Serializer/ApiObjectTypesMapper.php +++ b/src/Api/Serializer/ApiObjectTypesMapper.php @@ -111,6 +111,11 @@ public static function openApiTypes(string $classname): array 'config' => '?object', ], + 'Upsun\Model\AllowedDomainsInner' => [ + 'host' => 'string', + 'port' => '?int', + ], + 'Upsun\Model\ApiToken' => [ 'id' => '?string', 'name' => '?string', @@ -228,6 +233,7 @@ public static function openApiTypes(string $classname): array 'Upsun\Model\Autoscaling' => [ 'enabled' => 'bool', + 'supports_horizontal_scaling_services' => 'bool', ], 'Upsun\Model\Backup' => [ @@ -243,16 +249,12 @@ public static function openApiTypes(string $classname): array 'safe' => 'bool', 'size_of_volumes' => '?int', 'size_used' => '?int', + 'size_object_storage' => '?int', 'deployment' => '?string', 'restorable' => 'bool', 'automated' => 'bool', ], - 'Upsun\Model\BasicAuth' => [ - 'username' => 'string', - 'password' => 'string', - ], - 'Upsun\Model\Bitbucket' => [ 'enabled' => '?bool', 'role' => '?string', @@ -864,6 +866,10 @@ public static function openApiTypes(string $classname): array 'caches' => '\Upsun\Model\BuildCachesValue[]', ], + 'Upsun\Model\BuildPhaseEgress' => [ + 'allowed_domains' => '\Upsun\Model\AllowedDomainsInner[]', + ], + 'Upsun\Model\BuildResources' => [ 'enabled' => 'bool', 'max_cpu' => 'float', @@ -1508,6 +1514,15 @@ public static function openApiTypes(string $classname): array 'is_default' => '?bool', ], + 'Upsun\Model\Egress' => [ + 'build' => '?\Upsun\Model\BuildPhaseEgress', + 'runtime' => '?\Upsun\Model\RuntimePhaseEgress', + ], + + 'Upsun\Model\EgressProxy' => [ + 'enabled' => 'bool', + ], + 'Upsun\Model\EmailIntegration' => [ 'created_at' => '?\DateTime', 'updated_at' => '?\DateTime', @@ -3929,6 +3944,7 @@ public static function openApiTypes(string $classname): array 'source_operations' => '?\Upsun\Model\SourceOperations', 'runtime_operations' => '?\Upsun\Model\RuntimeOperations', 'outbound_firewall' => '?\Upsun\Model\OutboundFirewall', + 'egress_proxy' => '?\Upsun\Model\EgressProxy', 'integrations' => '?\Upsun\Model\Integrations', ], @@ -4275,30 +4291,6 @@ public static function openApiTypes(string $classname): array 'environmental_impact' => '?object', ], - 'Upsun\Model\RegistryCredential' => [ - 'id' => 'string', - 'created_at' => '?\DateTime', - 'updated_at' => '?\DateTime', - 'registry' => 'string', - 'auth' => '?\Upsun\Model\BasicAuth', - 'identity_token' => '?string', - 'registry_token' => '?string', - ], - - 'Upsun\Model\RegistryCredentialCreateInput' => [ - 'registry' => 'string', - 'auth' => '?\Upsun\Model\BasicAuth', - 'identity_token' => '?string', - 'registry_token' => '?string', - ], - - 'Upsun\Model\RegistryCredentialPatch' => [ - 'registry' => '?string', - 'auth' => '?\Upsun\Model\BasicAuth', - 'identity_token' => '?string', - 'registry_token' => '?string', - ], - 'Upsun\Model\ReplacementDomainStorage' => [ 'created_at' => '?\DateTime', 'updated_at' => '?\DateTime', @@ -4953,6 +4945,10 @@ public static function openApiTypes(string $classname): array 'enabled' => 'bool', ], + 'Upsun\Model\RuntimePhaseEgress' => [ + 'allowed_domains' => '\Upsun\Model\AllowedDomainsInner[]', + ], + 'Upsun\Model\SSIConfiguration' => [ 'enabled' => 'bool', ], @@ -5359,20 +5355,20 @@ public static function openApiTypes(string $classname): array 'Upsun\Model\Task' => [ 'id' => 'string', - 'type' => 'string', - 'source' => '\Upsun\Model\SourceCodeConfiguration1', - 'hooks' => '\Upsun\Model\Hooks1', + 'resources' => '?\Upsun\Model\Resources9', + 'authorizations' => '\Upsun\Model\AuthorizationsInner[]', 'relationships' => '\Upsun\Model\ServiceRelationshipsValue[]', 'additional_hosts' => 'string[]', 'mounts' => '\Upsun\Model\MountsValue[]', 'timezone' => '?string', 'variables' => 'array<string,mixed>[]', + 'container_profile' => '?string', + 'type' => 'string', + 'source' => '\Upsun\Model\SourceCodeConfiguration1', + 'hooks' => '\Upsun\Model\Hooks1', 'dependencies' => 'object[]', 'runtime' => 'object', - 'authorizations' => '\Upsun\Model\AuthorizationsInner[]', 'run' => '\Upsun\Model\RunConfiguration', - 'resources' => '?\Upsun\Model\Resources9', - 'container_profile' => '?string', 'name' => 'string', ], @@ -5814,17 +5810,18 @@ public static function openApiTypes(string $classname): array 'Upsun\Model\WebApplicationsValue' => [ 'resources' => '?\Upsun\Model\Resources1', - 'size' => 'string', - 'disk' => '?int', - 'access' => 'string[]', 'authorizations' => '\Upsun\Model\AuthorizationsInner[]', 'relationships' => '\Upsun\Model\ServiceRelationshipsValue[]', 'additional_hosts' => 'string[]', 'mounts' => '\Upsun\Model\MountsValue[]', 'timezone' => '?string', 'variables' => 'array<string,mixed>[]', - 'firewall' => '?\Upsun\Model\Firewall', 'container_profile' => '?string', + 'size' => 'string', + 'disk' => '?int', + 'access' => 'string[]', + 'firewall' => '?\Upsun\Model\Firewall', + 'egress' => '?\Upsun\Model\Egress', 'operations' => '\Upsun\Model\OperationsValue[]', 'name' => 'string', 'type' => 'string', @@ -5928,17 +5925,18 @@ public static function openApiTypes(string $classname): array 'Upsun\Model\WorkersValue' => [ 'resources' => '?\Upsun\Model\Resources', - 'size' => 'string', - 'disk' => '?int', - 'access' => 'string[]', 'authorizations' => '\Upsun\Model\AuthorizationsInner[]', 'relationships' => '\Upsun\Model\ServiceRelationshipsValue[]', 'additional_hosts' => 'string[]', 'mounts' => '\Upsun\Model\MountsValue[]', 'timezone' => '?string', 'variables' => 'array<string,mixed>[]', - 'firewall' => '?\Upsun\Model\Firewall', 'container_profile' => '?string', + 'size' => 'string', + 'disk' => '?int', + 'access' => 'string[]', + 'firewall' => '?\Upsun\Model\Firewall', + 'egress' => '?\Upsun\Model\Egress', 'operations' => '\Upsun\Model\OperationsValue[]', 'name' => 'string', 'type' => 'string', diff --git a/src/Api/Serializer/ObjectSerializer.php b/src/Api/Serializer/ObjectSerializer.php index 6c0e5fbe..781d17c3 100644 --- a/src/Api/Serializer/ObjectSerializer.php +++ b/src/Api/Serializer/ObjectSerializer.php @@ -361,6 +361,8 @@ public static function deserialize($data, string $class, $httpHeaders = null) return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: ''; case 'mixed': return $data; + case 'object': + return (object)$data; case 'array': error_log("Warning: Deserializing generic 'array' type - should use specific model class"); return (array)$data; diff --git a/src/Api/SubscriptionsApi.php b/src/Api/SubscriptionsApi.php index c1daba06..550917fc 100644 --- a/src/Api/SubscriptionsApi.php +++ b/src/Api/SubscriptionsApi.php @@ -232,6 +232,8 @@ private function canAffordSubscriptionRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Subscriptions/operation/can-create-new-org-subscription + * + * @deprecated */ public function canCreateNewOrgSubscription( string $organizationId @@ -244,6 +246,8 @@ public function canCreateNewOrgSubscription( /** * Checks if the user is able to create a new project. with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. (required) * @@ -291,6 +295,8 @@ private function canCreateNewOrgSubscriptionWithHttpInfo( * The ID of the organization. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function canCreateNewOrgSubscriptionRequest( string $organizationId @@ -995,6 +1001,8 @@ private function deleteOrgSubscriptionRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Subscriptions/operation/estimate-new-org-subscription + * + * @deprecated */ public function estimateNewOrgSubscription( string $organizationId, @@ -1017,6 +1025,8 @@ public function estimateNewOrgSubscription( /** * Estimate the price of a new subscription with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. (required) * @param string $plan (required) @@ -1084,6 +1094,8 @@ private function estimateNewOrgSubscriptionWithHttpInfo( * @param string|null $format (optional) * * @throws InvalidArgumentException + * + * @deprecated */ private function estimateNewOrgSubscriptionRequest( string $organizationId, @@ -1284,6 +1296,8 @@ private function estimateNewOrgSubscriptionRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Subscriptions/operation/estimate-org-subscription + * + * @deprecated */ public function estimateOrgSubscription( string $organizationId, @@ -1308,6 +1322,8 @@ public function estimateOrgSubscription( /** * Estimate the price of a subscription with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. (required) * @param string $subscriptionId @@ -1381,6 +1397,8 @@ private function estimateOrgSubscriptionWithHttpInfo( * @param string|null $format (optional) * * @throws InvalidArgumentException + * + * @deprecated */ private function estimateOrgSubscriptionRequest( string $organizationId, @@ -1756,6 +1774,8 @@ private function getOrgSubscriptionRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Subscriptions/operation/get-org-subscription-current-usage + * + * @deprecated */ public function getOrgSubscriptionCurrentUsage( string $organizationId, @@ -1774,6 +1794,8 @@ public function getOrgSubscriptionCurrentUsage( /** * Get current usage for a subscription with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. (required) * @param string $subscriptionId @@ -1835,6 +1857,8 @@ private function getOrgSubscriptionCurrentUsageWithHttpInfo( * @param bool|null $includeNotCharged (optional) * * @throws InvalidArgumentException + * + * @deprecated */ private function getOrgSubscriptionCurrentUsageRequest( string $organizationId, @@ -1979,6 +2003,8 @@ private function getOrgSubscriptionCurrentUsageRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Subscriptions/operation/get-subscription-usage-alerts + * + * @deprecated */ public function getSubscriptionUsageAlerts( string $organizationId, @@ -1993,6 +2019,8 @@ public function getSubscriptionUsageAlerts( /** * Get usage alerts with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -2048,6 +2076,8 @@ private function getSubscriptionUsageAlertsWithHttpInfo( * The ID of the subscription. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function getSubscriptionUsageAlertsRequest( string $organizationId, @@ -2565,6 +2595,8 @@ private function listOrgSubscriptionsRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Subscriptions/operation/list-subscription-addons + * + * @deprecated */ public function listSubscriptionAddons( string $organizationId, @@ -2579,6 +2611,8 @@ public function listSubscriptionAddons( /** * List addons for a subscription with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. (required) * @param string $subscriptionId @@ -2632,6 +2666,8 @@ private function listSubscriptionAddonsWithHttpInfo( * The ID of the subscription. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function listSubscriptionAddonsRequest( string $organizationId, @@ -2943,6 +2979,8 @@ private function updateOrgSubscriptionRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Subscriptions/operation/update-subscription-usage-alerts + * + * @deprecated */ public function updateSubscriptionUsageAlerts( string $organizationId, @@ -2959,6 +2997,8 @@ public function updateSubscriptionUsageAlerts( /** * Update usage alerts. with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -3016,6 +3056,8 @@ private function updateSubscriptionUsageAlertsWithHttpInfo( * The ID of the subscription. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function updateSubscriptionUsageAlertsRequest( string $organizationId, diff --git a/src/Api/UserProfilesApi.php b/src/Api/UserProfilesApi.php index 7f86526d..41cb624b 100644 --- a/src/Api/UserProfilesApi.php +++ b/src/Api/UserProfilesApi.php @@ -13,6 +13,7 @@ use SplFileObject; use Upsun\Api\Serializer\ObjectSerializer; use Upsun\Core\TokenProvider; +use Upsun\FormDataProcessor; use Upsun\Model\Address; use Upsun\Model\CreateProfilePicture200Response; use Upsun\Model\ListProfiles200Response; diff --git a/src/Api/VouchersApi.php b/src/Api/VouchersApi.php index 93bcca44..911e5e14 100644 --- a/src/Api/VouchersApi.php +++ b/src/Api/VouchersApi.php @@ -60,6 +60,8 @@ public function __construct( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Vouchers/operation/apply-org-voucher + * + * @deprecated */ public function applyOrgVoucher( string $organizationId, @@ -74,6 +76,8 @@ public function applyOrgVoucher( /** * Apply voucher with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. (required) * @@ -117,6 +121,8 @@ private function applyOrgVoucherWithHttpInfo( * The ID of the organization. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function applyOrgVoucherRequest( string $organizationId, @@ -230,6 +236,8 @@ private function applyOrgVoucherRequest( * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @see https://docs.upsun.com/api/#tag/Vouchers/operation/list-org-vouchers + * + * @deprecated */ public function listOrgVouchers( string $organizationId @@ -242,6 +250,8 @@ public function listOrgVouchers( /** * List vouchers with HTTP Info * + * @deprecated + * * @param string $organizationId * The ID of the organization. Prefix with name= to retrieve the organization by * name instead. (required) @@ -291,6 +301,8 @@ private function listOrgVouchersWithHttpInfo( * name instead. (required) * * @throws InvalidArgumentException + * + * @deprecated */ private function listOrgVouchersRequest( string $organizationId diff --git a/src/Core/OAuthProvider.php b/src/Core/OAuthProvider.php index a523e97d..e01b0282 100644 --- a/src/Core/OAuthProvider.php +++ b/src/Core/OAuthProvider.php @@ -24,6 +24,18 @@ */ class OAuthProvider implements TokenProvider { + /** Custom grant exchanging a long-lived API token for an access token. */ + public const GRANT_API_TOKEN = 'api_token'; + + /** Grant used by the local token service inside an Upsun runtime container. */ + public const GRANT_CLIENT_CREDENTIALS = 'client_credentials'; + + /** Minimum access-token lifetime (seconds) accepted by the local token service. */ + public const TOKEN_TTL_MIN = 60; + + /** Maximum access-token lifetime (seconds) accepted by the local token service. */ + public const TOKEN_TTL_MAX = 900; + private ?string $accessToken = null; private ?string $refreshToken = null; private ?string $typeToken = null; @@ -48,29 +60,50 @@ public function __construct( private readonly string $clientId, private readonly string $clientSecret, ?string $refreshEndpoint = null, + private readonly string $grantType = self::GRANT_API_TOKEN, + private readonly ?int $tokenTtl = null, ) { $this->effectiveRefreshEndpoint = $refreshEndpoint ?? $this->tokenEndpoint; } /** - * Exchanges the API token for an access token using the custom api_token grant. - * Uses tokenEndpoint (not refreshEndpoint). + * Exchanges credentials for an access token. Uses tokenEndpoint (not refreshEndpoint). + * + * - api_token grant (default): exchanges the configured API token, authenticating + * the confidential client with HTTP Basic. + * - client_credentials grant: used by the local Upsun container token service, + * which requires neither an API token nor Basic auth. * * @throws Exception */ public function exchangeCodeForToken(): bool { try { - $body = http_build_query([ - 'grant_type' => 'api_token', - 'api_token' => $this->clientSecret, - 'client_id' => $this->clientId, - ]); - - $request = $this->requestFactory->createRequest('POST', $this->tokenEndpoint) - ->withHeader('Authorization', 'Basic ' . base64_encode('platform-api-user:')) - ->withHeader('Content-Type', 'application/x-www-form-urlencoded') - ->withBody(Stream::create($body)); + if ($this->grantType === self::GRANT_CLIENT_CREDENTIALS) { + $body = http_build_query([ + 'grant_type' => 'client_credentials', + ]); + + $request = $this->requestFactory->createRequest('POST', $this->tokenEndpoint) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody(Stream::create($body)); + + if ($this->tokenTtl !== null) { + $ttl = max(self::TOKEN_TTL_MIN, min(self::TOKEN_TTL_MAX, $this->tokenTtl)); + $request = $request->withHeader('x-token-ttl', (string)$ttl); + } + } else { + $body = http_build_query([ + 'grant_type' => 'api_token', + 'api_token' => $this->clientSecret, + 'client_id' => $this->clientId, + ]); + + $request = $this->requestFactory->createRequest('POST', $this->tokenEndpoint) + ->withHeader('Authorization', 'Basic ' . base64_encode('platform-api-user:')) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody(Stream::create($body)); + } $response = $this->httpClient->sendRequest($request); diff --git a/src/Core/Tasks/ActivitiesTask.php b/src/Core/Tasks/ActivitiesTask.php index 475e371d..9d435d68 100644 --- a/src/Core/Tasks/ActivitiesTask.php +++ b/src/Core/Tasks/ActivitiesTask.php @@ -18,7 +18,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class ActivitiesTask extends TaskBase { diff --git a/src/Core/Tasks/ApplicationsTask.php b/src/Core/Tasks/ApplicationsTask.php index 682dd92d..a6a42775 100644 --- a/src/Core/Tasks/ApplicationsTask.php +++ b/src/Core/Tasks/ApplicationsTask.php @@ -12,7 +12,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class ApplicationsTask extends TaskBase { diff --git a/src/Core/Tasks/BackupsTask.php b/src/Core/Tasks/BackupsTask.php index dccea677..5d5bad86 100644 --- a/src/Core/Tasks/BackupsTask.php +++ b/src/Core/Tasks/BackupsTask.php @@ -17,7 +17,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class BackupsTask extends TaskBase { diff --git a/src/Core/Tasks/CertificatesTask.php b/src/Core/Tasks/CertificatesTask.php index 93386792..c3bbd8eb 100644 --- a/src/Core/Tasks/CertificatesTask.php +++ b/src/Core/Tasks/CertificatesTask.php @@ -10,6 +10,8 @@ use Upsun\Model\Certificate; use Upsun\Model\CertificateCreateInput; use Upsun\Model\CertificatePatch; +use Upsun\Model\CertificateProvisioner; +use Upsun\Model\CertificateProvisionerPatch; use Upsun\UpsunClient; /** @@ -17,7 +19,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class CertificatesTask extends TaskBase { @@ -132,4 +134,71 @@ public function update( ) ); } + + // Certificate Provisioner Methods + + /** + * Get a certificate provisioner configuration + * This method retrieves the configuration of a certificate provisioner, which is used to automatically manage + * and renew SSL/TLS certificates for your project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + */ + public function getProvisioner( + string $projectId, + string $certificateProvisionerDocumentId + ): CertificateProvisioner { + $this->checkProjectId($projectId); + if (empty($certificateProvisionerDocumentId)) { + throw new InvalidArgumentException('Certificate provisioner document ID cannot be empty'); + } + + return $this->api->getProjectsProvisioners( + projectId: $projectId, + certificateProvisionerDocumentId: $certificateProvisionerDocumentId + ); + } + + /** + * List all certificate provisioners for a project + * This method retrieves a list of all certificate provisioners configured for the project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return CertificateProvisioner[] + */ + public function listProvisioners(string $projectId): array + { + $this->checkProjectId($projectId); + + return $this->api->listProjectsProvisioners(projectId: $projectId); + } + + /** + * Update a certificate provisioner configuration + * This method allows you to modify the configuration of a certificate provisioner. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + */ + public function updateProvisioner( + string $projectId, + string $certificateProvisionerDocumentId, + ?CertificateProvisionerPatch $certificateProvisionerPatch = null + ): AcceptedResponse { + $this->checkProjectId($projectId); + if (empty($certificateProvisionerDocumentId)) { + throw new InvalidArgumentException('Certificate provisioner document ID cannot be empty'); + } + + return $this->api->updateProjectsProvisioners( + projectId: $projectId, + certificateProvisionerDocumentId: $certificateProvisionerDocumentId, + certificateProvisionerPatch: $certificateProvisionerPatch + ); + } } diff --git a/src/Core/Tasks/DomainsTask.php b/src/Core/Tasks/DomainsTask.php index 01e94f41..d0324084 100644 --- a/src/Core/Tasks/DomainsTask.php +++ b/src/Core/Tasks/DomainsTask.php @@ -17,7 +17,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class DomainsTask extends TaskBase { diff --git a/src/Core/Tasks/EnvironmentsTask.php b/src/Core/Tasks/EnvironmentsTask.php index c5d9a17e..663b9cbf 100644 --- a/src/Core/Tasks/EnvironmentsTask.php +++ b/src/Core/Tasks/EnvironmentsTask.php @@ -19,6 +19,7 @@ use Upsun\Model\Environment; use Upsun\Model\EnvironmentActivateInput; use Upsun\Model\EnvironmentBranchInput; +use Upsun\Model\EnvironmentDeployInput; use Upsun\Model\EnvironmentInitializeInput; use Upsun\Model\EnvironmentMergeInput; use Upsun\Model\EnvironmentPatch; @@ -42,7 +43,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class EnvironmentsTask extends TaskBase { @@ -997,4 +998,49 @@ public function runSourceOperation( variables: $variables ); } + + /** + * Deploy an environment + * This method triggers a new deployment of the specified environment, deploying the latest code and configuration. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function deploy( + string $projectId, + string $environmentId, + ?EnvironmentDeployInput $environmentDeployInput = null + ): AcceptedResponse { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->envApi->deployEnvironment( + projectId: $projectId, + environmentId: $environmentId, + environmentDeployInput: $environmentDeployInput ?? new EnvironmentDeployInput() + ); + } + + /** + * Trigger a maintenance redeploy of an environment + * This method performs a maintenance redeploy of the specified environment. This is useful for applying + * system-level updates or maintenance operations without changing the code or user configuration. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function maintenanceRedeploy( + string $projectId, + string $environmentId + ): AcceptedResponse { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->envApi->maintenanceRedeployEnvironment( + projectId: $projectId, + environmentId: $environmentId + ); + } } diff --git a/src/Core/Tasks/IntegrationsTask.php b/src/Core/Tasks/IntegrationsTask.php index 10261a45..56a7b644 100644 --- a/src/Core/Tasks/IntegrationsTask.php +++ b/src/Core/Tasks/IntegrationsTask.php @@ -17,7 +17,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class IntegrationsTask extends TaskBase { diff --git a/src/Core/Tasks/MetricsTask.php b/src/Core/Tasks/MetricsTask.php index d1d0b14d..d9b5f7b2 100644 --- a/src/Core/Tasks/MetricsTask.php +++ b/src/Core/Tasks/MetricsTask.php @@ -2,6 +2,16 @@ namespace Upsun\Core\Tasks; +use DateTime; +use InvalidArgumentException; +use Psr\Http\Client\ClientExceptionInterface; +use Upsun\Api\ApiException; +use Upsun\Api\BlackfireMonitoringApi; +use Upsun\Api\BlackfireProfilingApi; +use Upsun\Api\ContinuousProfilingApi; +use Upsun\Api\EntrypointApi; +use Upsun\Api\HttpTrafficApi; +use Upsun\Model\FilterSelect; use Upsun\UpsunClient; /** @@ -9,13 +19,821 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class MetricsTask extends TaskBase { public function __construct( UpsunClient $client, + private readonly HttpTrafficApi $httpTrafficApi, + private readonly BlackfireMonitoringApi $blackfireMonitoringApi, + private readonly ContinuousProfilingApi $continuousProfilingApi, + private readonly BlackfireProfilingApi $blackfireProfilingApi, + private readonly EntrypointApi $entrypointApi, ) { parent::__construct($client); } + + // HTTP Traffic Metrics Methods + + /** + * Get HTTP metrics timeline grouped by IP addresses + * This method retrieves HTTP traffic metrics over a timeline, broken down by IP addresses accessing the application. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function httpMetricsTimelineIps( + string $projectId, + string $environmentId, + int $from, + int $to, + ?int $limit = null, + ?int $topHitsCount = null, + ?array $applications = null, + ?string $applicationsMode = null, + ?array $methods = null, + ?string $methodsMode = null, + ?array $domains = null, + ?string $domainsMode = null, + ?array $codeSlots = null, + ?string $codeSlotsMode = null, + ?array $codes = null, + ?string $codesMode = null, + ?array $requestDurationSlots = null, + ?string $requestDurationSlotsMode = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->httpTrafficApi->httpMetricsTimelineIps( + projectId: $projectId, + environmentId: $environmentId, + from: $from, + to: $to, + limit: $limit, + topHitsCount: $topHitsCount, + applications: $applications, + applicationsMode: $applicationsMode, + methods: $methods, + methodsMode: $methodsMode, + domains: $domains, + domainsMode: $domainsMode, + codeSlots: $codeSlots, + codeSlotsMode: $codeSlotsMode, + codes: $codes, + codesMode: $codesMode, + requestDurationSlots: $requestDurationSlots, + requestDurationSlotsMode: $requestDurationSlotsMode + ); + } + + /** + * Get HTTP metrics timeline grouped by URLs + * This method retrieves HTTP traffic metrics over a timeline, broken down by the URLs being accessed. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function httpMetricsTimelineUrls( + string $projectId, + string $environmentId, + int $from, + int $to, + ?int $limit = null, + ?int $topHitsCount = null, + ?array $applications = null, + ?string $applicationsMode = null, + ?array $methods = null, + ?string $methodsMode = null, + ?array $domains = null, + ?string $domainsMode = null, + ?array $codeSlots = null, + ?string $codeSlotsMode = null, + ?array $codes = null, + ?string $codesMode = null, + ?array $requestDurationSlots = null, + ?string $requestDurationSlotsMode = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->httpTrafficApi->httpMetricsTimelineUrls( + projectId: $projectId, + environmentId: $environmentId, + from: $from, + to: $to, + limit: $limit, + topHitsCount: $topHitsCount, + applications: $applications, + applicationsMode: $applicationsMode, + methods: $methods, + methodsMode: $methodsMode, + domains: $domains, + domainsMode: $domainsMode, + codeSlots: $codeSlots, + codeSlotsMode: $codeSlotsMode, + codes: $codes, + codesMode: $codesMode, + requestDurationSlots: $requestDurationSlots, + requestDurationSlotsMode: $requestDurationSlotsMode + ); + } + + /** + * Get HTTP metrics timeline grouped by User Agents + * This method retrieves HTTP traffic metrics over a timeline, broken down by the user agents making the requests. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function httpMetricsTimelineUserAgents( + string $projectId, + string $environmentId, + int $from, + int $to, + ?int $limit = null, + ?int $topHitsCount = null, + ?array $applications = null, + ?string $applicationsMode = null, + ?array $methods = null, + ?string $methodsMode = null, + ?array $domains = null, + ?string $domainsMode = null, + ?array $codeSlots = null, + ?string $codeSlotsMode = null, + ?array $codes = null, + ?string $codesMode = null, + ?array $requestDurationSlots = null, + ?string $requestDurationSlotsMode = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->httpTrafficApi->httpMetricsTimelineUserAgents( + projectId: $projectId, + environmentId: $environmentId, + from: $from, + to: $to, + limit: $limit, + topHitsCount: $topHitsCount, + applications: $applications, + applicationsMode: $applicationsMode, + methods: $methods, + methodsMode: $methodsMode, + domains: $domains, + domainsMode: $domainsMode, + codeSlots: $codeSlots, + codeSlotsMode: $codeSlotsMode, + codes: $codes, + codesMode: $codesMode, + requestDurationSlots: $requestDurationSlots, + requestDurationSlotsMode: $requestDurationSlotsMode + ); + } + + // Blackfire Monitoring Methods + + /** + * Get Blackfire PHP server cache metrics + * This method retrieves PHP cache metrics from the Blackfire monitoring service for the specified time range. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function blackfirePhpServerCaches( + string $projectId, + string $environmentId, + int $from, + int $to, + ?int $grain = null, + ?array $contexts = null, + ?string $contextsMode = null, + ?array $applications = null, + ?string $applicationsMode = null, + ?array $instances = null, + ?string $instancesMode = null, + ?string $distributionCost = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->blackfireMonitoringApi->blackfirePhpServerCaches( + projectId: $projectId, + environmentId: $environmentId, + from: $from, + to: $to, + grain: $grain, + contexts: $contexts, + contextsMode: $contextsMode, + applications: $applications, + applicationsMode: $applicationsMode, + instances: $instances, + instancesMode: $instancesMode, + distributionCost: $distributionCost + ); + } + + /** + * Get global Blackfire server metrics + * This method retrieves global metrics from the Blackfire server for the specified keys and time range. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId, environmentId, or keys array is invalid or empty + */ + public function blackfireServerGlobal( + string $projectId, + string $environmentId, + int $from, + int $to, + array $keys, + ?int $grain = null, + ?array $contexts = null, + ?string $contextsMode = null, + ?array $applications = null, + ?string $applicationsMode = null, + ?array $instances = null, + ?string $instancesMode = null, + ?string $distributionCost = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + if (empty($keys)) { + throw new InvalidArgumentException('Keys array cannot be empty'); + } + + return $this->blackfireMonitoringApi->blackfireServerGlobal( + projectId: $projectId, + environmentId: $environmentId, + from: $from, + to: $to, + keys: $keys, + grain: $grain, + contexts: $contexts, + contextsMode: $contextsMode, + applications: $applications, + applicationsMode: $applicationsMode, + instances: $instances, + instancesMode: $instancesMode, + distributionCost: $distributionCost + ); + } + + /** + * Get top spans from Blackfire server + * This method retrieves the top time-consuming spans from the Blackfire server for performance analysis. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function blackfireServerTopSpans( + string $projectId, + string $environmentId, + int $from, + int $to, + ?int $grain = null, + ?string $sort = null, + ?array $contexts = null, + ?string $contextsMode = null, + ?array $applications = null, + ?string $applicationsMode = null, + ?array $instances = null, + ?string $instancesMode = null, + ?array $transactions = null, + ?string $transactionsMode = null, + ?array $wtSlots = null, + ?string $wtSlotsMode = null, + ?array $pmuSlots = null, + ?string $pmuSlotsMode = null, + ?array $httpStatusCodes = null, + ?string $httpStatusCodesMode = null, + ?array $httpHosts = null, + ?string $httpHostsMode = null, + ?array $hosts = null, + ?string $hostsMode = null, + ?array $frameworks = null, + ?string $frameworksMode = null, + ?array $languages = null, + ?string $languagesMode = null, + ?array $methods = null, + ?string $methodsMode = null, + ?array $runtimes = null, + ?string $runtimesMode = null, + ?array $oss = null, + ?string $ossMode = null, + ?string $distributionCost = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->blackfireMonitoringApi->blackfireServerTopSpans( + projectId: $projectId, + environmentId: $environmentId, + from: $from, + to: $to, + grain: $grain, + sort: $sort, + contexts: $contexts, + contextsMode: $contextsMode, + applications: $applications, + applicationsMode: $applicationsMode, + instances: $instances, + instancesMode: $instancesMode, + transactions: $transactions, + transactionsMode: $transactionsMode, + wtSlots: $wtSlots, + wtSlotsMode: $wtSlotsMode, + pmuSlots: $pmuSlots, + pmuSlotsMode: $pmuSlotsMode, + httpStatusCodes: $httpStatusCodes, + httpStatusCodesMode: $httpStatusCodesMode, + httpHosts: $httpHosts, + httpHostsMode: $httpHostsMode, + hosts: $hosts, + hostsMode: $hostsMode, + frameworks: $frameworks, + frameworksMode: $frameworksMode, + languages: $languages, + languagesMode: $languagesMode, + methods: $methods, + methodsMode: $methodsMode, + runtimes: $runtimes, + runtimesMode: $runtimesMode, + oss: $oss, + ossMode: $ossMode, + distributionCost: $distributionCost + ); + } + + /** + * Get transaction breakdown from Blackfire server + * This method retrieves a breakdown of transactions from the Blackfire server, grouped by the specified dimension. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function blackfireServerTransactionsBreakdown( + string $projectId, + string $environmentId, + int $from, + int $to, + ?int $grain = null, + ?string $breakdownDimension = null, + ?string $sort = null, + ?int $breakdownLimit = null, + ?array $contexts = null, + ?string $contextsMode = null, + ?array $applications = null, + ?string $applicationsMode = null, + ?array $instances = null, + ?string $instancesMode = null, + ?array $transactions = null, + ?string $transactionsMode = null, + ?array $wtSlots = null, + ?string $wtSlotsMode = null, + ?array $pmuSlots = null, + ?string $pmuSlotsMode = null, + ?array $httpStatusCodes = null, + ?string $httpStatusCodesMode = null, + ?array $httpHosts = null, + ?string $httpHostsMode = null, + ?array $hosts = null, + ?string $hostsMode = null, + ?array $frameworks = null, + ?string $frameworksMode = null, + ?array $languages = null, + ?string $languagesMode = null, + ?array $methods = null, + ?string $methodsMode = null, + ?array $runtimes = null, + ?string $runtimesMode = null, + ?array $oss = null, + ?string $ossMode = null, + ?string $distributionCost = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->blackfireMonitoringApi->blackfireServerTransactionsBreakdown( + projectId: $projectId, + environmentId: $environmentId, + from: $from, + to: $to, + grain: $grain, + breakdownDimension: $breakdownDimension, + sort: $sort, + breakdownLimit: $breakdownLimit, + contexts: $contexts, + contextsMode: $contextsMode, + applications: $applications, + applicationsMode: $applicationsMode, + instances: $instances, + instancesMode: $instancesMode, + transactions: $transactions, + transactionsMode: $transactionsMode, + wtSlots: $wtSlots, + wtSlotsMode: $wtSlotsMode, + pmuSlots: $pmuSlots, + pmuSlotsMode: $pmuSlotsMode, + httpStatusCodes: $httpStatusCodes, + httpStatusCodesMode: $httpStatusCodesMode, + httpHosts: $httpHosts, + httpHostsMode: $httpHostsMode, + hosts: $hosts, + hostsMode: $hostsMode, + frameworks: $frameworks, + frameworksMode: $frameworksMode, + languages: $languages, + languagesMode: $languagesMode, + methods: $methods, + methodsMode: $methodsMode, + runtimes: $runtimes, + runtimesMode: $runtimesMode, + oss: $oss, + ossMode: $ossMode, + distributionCost: $distributionCost + ); + } + + // Continuous Profiling Methods + + /** + * List continuous profiling applications + * This method retrieves a list of applications available for continuous profiling in the environment. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function listContinuousProfilingApplications( + string $projectId, + string $environmentId, + ?int $from = null, + ?int $to = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->continuousProfilingApi->listApplications( + projectId: $projectId, + envId: $environmentId, + from: $from, + to: $to + ); + } + + /** + * Get continuous profiling application filter data + * This method retrieves filter information for profiling data of a specific application. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId, environmentId, or application name is invalid + */ + public function getContinuousProfilingApplicationFilter( + string $projectId, + string $environmentId, + string $applicationName, + ?int $from = null, + ?int $to = null, + ?string $profileType = null, + ?int $runtimeMode = null, + ?array $runtime = null, + ?int $runtimeVersionMode = null, + ?array $runtimeVersion = null, + ?int $runtimeArchMode = null, + ?array $runtimeArch = null, + ?int $runtimeOsMode = null, + ?array $runtimeOs = null, + ?int $probeVersionMode = null, + ?array $probeVersion = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + if (empty($applicationName)) { + throw new InvalidArgumentException('Application name cannot be empty'); + } + + return $this->continuousProfilingApi->getApplicationFilter( + projectId: $projectId, + envId: $environmentId, + app: $applicationName, + from: $from, + to: $to, + profileType: $profileType, + runtimeMode: $runtimeMode, + runtime: $runtime, + runtimeVersionMode: $runtimeVersionMode, + runtimeVersion: $runtimeVersion, + runtimeArchMode: $runtimeArchMode, + runtimeArch: $runtimeArch, + runtimeOsMode: $runtimeOsMode, + runtimeOs: $runtimeOs, + probeVersionMode: $probeVersionMode, + probeVersion: $probeVersion + ); + } + + /** + * Get merged continuous profiling data for an application + * This method retrieves merged profiling data for an application, combining multiple profiles into a single view. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId, environmentId, or application name is invalid + */ + public function getContinuousProfilingApplicationMerge( + string $projectId, + string $environmentId, + string $applicationName, + ?int $from = null, + ?int $to = null, + ?string $profileType = null, + ?string $out = null, + ?int $runtimeMode = null, + ?array $runtime = null, + ?int $runtimeVersionMode = null, + ?array $runtimeVersion = null, + ?int $runtimeArchMode = null, + ?array $runtimeArch = null, + ?int $runtimeOsMode = null, + ?array $runtimeOs = null, + ?int $probeVersionMode = null, + ?array $probeVersion = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + if (empty($applicationName)) { + throw new InvalidArgumentException('Application name cannot be empty'); + } + + return $this->continuousProfilingApi->getApplicationMerge( + projectId: $projectId, + envId: $environmentId, + app: $applicationName, + from: $from, + to: $to, + profileType: $profileType, + out: $out, + runtimeMode: $runtimeMode, + runtime: $runtime, + runtimeVersionMode: $runtimeVersionMode, + runtimeVersion: $runtimeVersion, + runtimeArchMode: $runtimeArchMode, + runtimeArch: $runtimeArch, + runtimeOsMode: $runtimeOsMode, + runtimeOs: $runtimeOs, + probeVersionMode: $probeVersionMode, + probeVersion: $probeVersion + ); + } + + /** + * Get continuous profiling application timeline + * This method retrieves a timeline of profiling data for an application over the specified period. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId, environmentId, or application name is invalid + */ + public function getContinuousProfilingApplicationTimeline( + string $projectId, + string $environmentId, + string $applicationName, + ?int $from = null, + ?int $to = null, + ?string $profileType = null, + ?int $runtimeMode = null, + ?array $runtime = null, + ?int $runtimeVersionMode = null, + ?array $runtimeVersion = null, + ?int $runtimeArchMode = null, + ?array $runtimeArch = null, + ?int $runtimeOsMode = null, + ?array $runtimeOs = null, + ?int $probeVersionMode = null, + ?array $probeVersion = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + if (empty($applicationName)) { + throw new InvalidArgumentException('Application name cannot be empty'); + } + + return $this->continuousProfilingApi->getApplicationTimeline( + projectId: $projectId, + envId: $environmentId, + app: $applicationName, + from: $from, + to: $to, + profileType: $profileType, + runtimeMode: $runtimeMode, + runtime: $runtime, + runtimeVersionMode: $runtimeVersionMode, + runtimeVersion: $runtimeVersion, + runtimeArchMode: $runtimeArchMode, + runtimeArch: $runtimeArch, + runtimeOsMode: $runtimeOsMode, + runtimeOs: $runtimeOs, + probeVersionMode: $probeVersionMode, + probeVersion: $probeVersion + ); + } + + // Blackfire Profiling Methods + + /** + * Get the call graph for a Blackfire profile. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function blackfireProfileGraph( + string $projectId, + string $environmentId, + string $uuid + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->blackfireProfilingApi->blackfireProfileGraph( + projectId: $projectId, + environmentId: $environmentId, + uuid: $uuid + ); + } + + /** + * Get the profile details for a Blackfire profile. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function blackfireProfileProfile( + string $projectId, + string $environmentId, + string $uuid + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->blackfireProfilingApi->blackfireProfileProfile( + projectId: $projectId, + environmentId: $environmentId, + uuid: $uuid + ); + } + + /** + * Get the subprofiles for a Blackfire profile. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function blackfireProfileSubprofiles( + string $projectId, + string $environmentId, + string $uuid + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->blackfireProfilingApi->blackfireProfileSubprofiles( + projectId: $projectId, + environmentId: $environmentId, + uuid: $uuid + ); + } + + /** + * Get the timeline for a Blackfire profile. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function blackfireProfileTimeline( + string $projectId, + string $environmentId, + string $uuid + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->blackfireProfilingApi->blackfireProfileTimeline( + projectId: $projectId, + environmentId: $environmentId, + uuid: $uuid + ); + } + + /** + * List Blackfire profiling results. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function blackfireProfilesList( + string $projectId, + string $environmentId, + ?string $transaction = null, + ?int $page = null, + ?int $limit = null, + ?string $url = null, + ?bool $isAuto = null, + ?string $isPublic = null, + ?string $sortBy = null, + ?string $sortOrder = null, + ?DateTime $startDate = null, + ?DateTime $endDate = null, + ?int $statusCode = null, + ?FilterSelect $owner = null, + ?FilterSelect $languages = null, + ?FilterSelect $frameworks = null, + ?int $itemsPerPage = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->blackfireProfilingApi->blackfireProfilesList( + projectId: $projectId, + environmentId: $environmentId, + transaction: $transaction, + page: $page, + limit: $limit, + url: $url, + isAuto: $isAuto, + isPublic: $isPublic, + sortBy: $sortBy, + sortOrder: $sortOrder, + startDate: $startDate, + endDate: $endDate, + statusCode: $statusCode, + owner: $owner, + languages: $languages, + frameworks: $frameworks, + itemsPerPage: $itemsPerPage + ); + } + + /** + * Get Blackfire profiling recommendations. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function blackfireProfilesRecommendations( + string $projectId, + string $environmentId, + ?int $from = null, + ?int $to = null, + ?string $transaction = null, + ?int $limit = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->blackfireProfilingApi->blackfireProfilesRecommendations( + projectId: $projectId, + environmentId: $environmentId, + from: $from, + to: $to, + transaction: $transaction, + limit: $limit + ); + } + + // Observability Entrypoint + + /** + * Get the observability entrypoint for an environment. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function observabilityEntrypoint( + string $projectId, + string $environmentId + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->entrypointApi->observabilityEntrypoint( + projectId: $projectId, + environmentId: $environmentId + ); + } } diff --git a/src/Core/Tasks/MountsTask.php b/src/Core/Tasks/MountsTask.php index 32a8431d..289ec6ab 100644 --- a/src/Core/Tasks/MountsTask.php +++ b/src/Core/Tasks/MountsTask.php @@ -14,7 +14,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class MountsTask extends TaskBase { diff --git a/src/Core/Tasks/OperationsTask.php b/src/Core/Tasks/OperationsTask.php index 9e933947..56499da7 100644 --- a/src/Core/Tasks/OperationsTask.php +++ b/src/Core/Tasks/OperationsTask.php @@ -14,7 +14,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class OperationsTask extends TaskBase { diff --git a/src/Core/Tasks/OrganizationsTask.php b/src/Core/Tasks/OrganizationsTask.php index 27eafa23..9686c397 100644 --- a/src/Core/Tasks/OrganizationsTask.php +++ b/src/Core/Tasks/OrganizationsTask.php @@ -3,30 +3,42 @@ namespace Upsun\Core\Tasks; use DateTime; +use Generator; use InvalidArgumentException; use Psr\Http\Client\ClientExceptionInterface; use Upsun\Api\AddOnsApi; +use Upsun\Api\AlertsApi; use Upsun\Api\ApiException; +use Upsun\Api\DefaultApi; +use Upsun\Api\DiscountsApi; use Upsun\Api\InvoicesApi; use Upsun\Api\MfaApi; use Upsun\Api\OrdersApi; use Upsun\Api\OrganizationMembersApi; use Upsun\Api\OrganizationProjectsApi; use Upsun\Api\OrganizationsApi; -use Upsun\Api\ProfilesApi; use Upsun\Api\RecordsApi; +use Upsun\Api\ReferencesApi; use Upsun\Api\SubscriptionsApi; +use Upsun\Api\UserProfilesApi; use Upsun\Api\VouchersApi; use Upsun\Model\AcceptedResponse; use Upsun\Model\Address; use Upsun\Model\ApplyOrgVoucherRequest; use Upsun\Model\ArrayFilter; +use Upsun\Model\CanAffordSubscriptionRequest; use Upsun\Model\CanCreateNewOrgSubscription200Response; +use Upsun\Model\CanUpdateSubscription200Response; use Upsun\Model\CreateAuthorizationCredentials200Response; use Upsun\Model\CreateOrgMemberRequest; +use Upsun\Model\CreateOrgProjectRequest; use Upsun\Model\CreateOrgRequest; use Upsun\Model\DateTimeFilter; +use Upsun\Model\Discount; use Upsun\Model\EstimationObject; +use Upsun\Model\GetAddress200Response; +use Upsun\Model\GetTypeAllowance200Response; +use Upsun\Model\GetUsageAlerts200Response; use Upsun\Model\Invoice; use Upsun\Model\ListOrgInvoices200Response; use Upsun\Model\ListOrgMembers200Response; @@ -41,19 +53,25 @@ use Upsun\Model\Order; use Upsun\Model\Organization; use Upsun\Model\OrganizationAddonsObject; +use Upsun\Model\OrganizationCarbon; use Upsun\Model\OrganizationMember; use Upsun\Model\OrganizationMFAEnforcement; +use Upsun\Model\OrganizationProject; use Upsun\Model\Profile; use Upsun\Model\Project; -use Upsun\Model\SendOrgMfaReminders200ResponseValue; // only mentionned in PHPDocs +use Upsun\Model\ProjectCarbon; +// only mentionned in PHPDocs +use Upsun\Model\SendOrgMfaReminders200ResponseValue; use Upsun\Model\SendOrgMfaRemindersRequest; use Upsun\Model\StringFilter; use Upsun\Model\Subscription; use Upsun\Model\SubscriptionCurrentUsageObject; use Upsun\Model\UpdateOrgAddonsRequest; use Upsun\Model\UpdateOrgMemberRequest; -use Upsun\Model\UpdateOrgProfileRequest; use Upsun\Model\UpdateOrgRequest; +use Upsun\Model\UpdateOrgSubscriptionRequest; +use Upsun\Model\UpdateProfileRequest; +use Upsun\Model\UpdateUsageAlertsRequest; use Upsun\Model\Vouchers; use Upsun\UpsunClient; @@ -62,7 +80,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class OrganizationsTask extends TaskBase { @@ -77,10 +95,14 @@ public function __construct( private readonly InvoicesApi $invoicesApi, private readonly MfaApi $mfaApi, private readonly OrdersApi $ordersApi, - private readonly ProfilesApi $profilesApi, + private readonly UserProfilesApi $userProfilesApi, private readonly RecordsApi $recordsApi, private readonly VouchersApi $vouchersApi, private readonly AddOnsApi $addOnsApi, + private readonly DiscountsApi $discountsApi, + private readonly ReferencesApi $referencesApi, + private readonly DefaultApi $defaultApi, + private readonly AlertsApi $alertsApi, ) { parent::__construct($client); } @@ -249,6 +271,86 @@ public function listSubscriptions(string $organizationId): ListOrgSubscriptions2 return $this->subscriptionsApi->listOrgSubscriptions($organizationId); } + /** + * Check whether a subscription can be updated to the given plan and resources. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + */ + public function canUpdateSubscription( + string $subscriptionId, + ?string $plan = null, + ?int $environments = null, + ?int $storage = null, + ?int $userLicenses = null + ): CanUpdateSubscription200Response { + $this->checkSubscriptionId($subscriptionId); + + return $this->subscriptionsApi->canUpdateSubscription( + subscriptionId: $subscriptionId, + plan: $plan, + environments: $environments, + storage: $storage, + userLicenses: $userLicenses + ); + } + + /** + * Update an organization subscription. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + */ + public function updateOrgSubscription( + string $organizationId, + string $subscriptionId, + UpdateOrgSubscriptionRequest $updateOrgSubscriptionRequest + ): Subscription { + $this->checkOrganizationId($organizationId); + $this->checkSubscriptionId($subscriptionId); + + return $this->subscriptionsApi->updateOrgSubscription( + organizationId: $organizationId, + subscriptionId: $subscriptionId, + updateOrgSubscriptionRequest: $updateOrgSubscriptionRequest + ); + } + + /** + * Get usage alerts for a subscription. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + */ + public function getSubscriptionUsageAlerts(string $subscriptionId): GetUsageAlerts200Response + { + $this->checkSubscriptionId($subscriptionId); + + return $this->alertsApi->getUsageAlerts(subscriptionId: $subscriptionId); + } + + /** + * Update usage alerts for a subscription. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + */ + public function updateSubscriptionUsageAlerts( + string $subscriptionId, + ?UpdateUsageAlertsRequest $updateUsageAlertsRequest = null + ): GetUsageAlerts200Response { + $this->checkSubscriptionId($subscriptionId); + + return $this->alertsApi->updateUsageAlerts( + subscriptionId: $subscriptionId, + updateUsageAlertsRequest: $updateUsageAlertsRequest + ); + } + /** * Create an organization member * @@ -604,6 +706,7 @@ public function deleteProject(string $projectId): void * Estimate the cost of creating a new project within the specified organization, based on parameters such as the * number of environments, storage, and user licenses. * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -631,6 +734,7 @@ public function estimateNewProject( * Estimate the cost of a project within the specified organization, based on parameters such as the * number of environments, storage, and user licenses. * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -663,6 +767,7 @@ public function estimateProject( /** * Get current usage for a project * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -770,6 +875,7 @@ public function sendMfaReminders(string $organizationId, ?array $userIds = null) /** * Get invoice * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -785,6 +891,7 @@ public function getInvoice(string $invoiceId, string $organizationId): Invoice /** * List invoices * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -810,6 +917,7 @@ public function listInvoices( /** * Download an invoice. * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface */ @@ -821,6 +929,7 @@ public function downloadInvoice(string $token): string /** * Create confirmation credentials for 3D-Secure * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -838,6 +947,7 @@ public function createAuthorizationCredentials( /** * Get an order * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -853,6 +963,7 @@ public function getOrder(string $organizationId, string $orderId, ?string $mode /** * List orders * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface */ @@ -879,11 +990,11 @@ public function listOrders( * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid */ - public function getAddress(string $organizationId): Address + public function getAddress(string $userId): GetAddress200Response { - $this->checkOrganizationId($organizationId); + $this->checkUserId($userId); - return $this->profilesApi->getOrgAddress(organizationId: $organizationId); + return $this->userProfilesApi->getAddress(userId: $userId); } /** @@ -893,11 +1004,11 @@ public function getAddress(string $organizationId): Address * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid */ - public function getProfile(string $organizationId): Profile + public function getProfile(string $userId): Profile { - $this->checkOrganizationId($organizationId); + $this->checkUserId($userId); - return $this->profilesApi->getOrgProfile(organizationId: $organizationId); + return $this->userProfilesApi->getProfile(userId: $userId); } /** @@ -908,7 +1019,7 @@ public function getProfile(string $organizationId): Profile * @throws InvalidArgumentException if required parameters are missing or invalid */ public function updateAddress( - string $organizationId, + string $userId, ?string $country = null, ?string $nameLine = null, ?string $premise = null, @@ -919,11 +1030,11 @@ public function updateAddress( ?string $locality = null, ?string $dependentLocality = null, ?string $postalCode = null, - ): Address { - $this->checkOrganizationId($organizationId); + ): GetAddress200Response { + $this->checkUserId($userId); - return $this->profilesApi->updateOrgAddress( - organizationId: $organizationId, + return $this->userProfilesApi->updateAddress( + userId: $userId, address: new Address( country: $country, nameLine: $nameLine, @@ -947,23 +1058,21 @@ public function updateAddress( * @throws InvalidArgumentException if required parameters are missing or invalid */ public function updateProfile( - string $organizationId, + string $userId, ?string $defaultCatalog = null, ?string $projectOptionsUrl = null, ?string $companyName = null, ?string $vatNumber = null, - ?string $billingContact = null, ): Profile { - $this->checkOrganizationId($organizationId); + $this->checkUserId($userId); - return $this->profilesApi->updateOrgProfile( - organizationId: $organizationId, - updateOrgProfileRequest: new UpdateOrgProfileRequest( - defaultCatalog: $defaultCatalog, - projectOptionsUrl: $projectOptionsUrl, + return $this->userProfilesApi->updateProfile( + userId: $userId, + updateProfileRequest: new UpdateProfileRequest( companyName: $companyName, vatNumber: $vatNumber, - billingContact: $billingContact, + defaultCatalog: $defaultCatalog, + projectOptionsUrl: $projectOptionsUrl, ) ); } @@ -971,6 +1080,7 @@ public function updateProfile( /** * List plan records * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -1012,6 +1122,7 @@ public function listRecords( /** * List usage records * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -1049,6 +1160,7 @@ public function listUsageRecords( * organization's account, which may result in discounts, credits, or other benefits being applied to the * organization's subscription. * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -1070,6 +1182,7 @@ public function applyVoucher(string $organizationId, string $code): void /** * List vouchers * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -1084,6 +1197,7 @@ public function listVouchers(string $organizationId): Vouchers /** * Get Organization Addons * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -1098,6 +1212,7 @@ public function getAddons(string $organizationId): OrganizationAddonsObject /** * Updates Organization Addons * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface * @throws InvalidArgumentException if required parameters are missing or invalid @@ -1117,4 +1232,240 @@ public function updateAddons( ) ); } + + // Organization Projects Methods + + /** + * Creates a project in an organization. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return OrganizationProject + */ + public function createOrgProject( + string $organizationId, + CreateOrgProjectRequest $createOrgProjectRequest + ): OrganizationProject { + $this->checkOrganizationId($organizationId); + + return $this->projectsApi->createOrgProject( + organizationId: $organizationId, + createOrgProjectRequest: $createOrgProjectRequest + ); + } + + /** + * Deletes a project from an organization. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + */ + public function deleteOrgProject(string $organizationId, string $projectId): void + { + $this->checkOrganizationId($organizationId); + $this->checkProjectId($projectId); + + $this->projectsApi->deleteOrgProject( + organizationId: $organizationId, + projectId: $projectId + ); + } + + /** + * Gets a project from an organization. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return OrganizationProject + */ + public function getOrgProject(string $organizationId, string $projectId): OrganizationProject + { + $this->checkOrganizationId($organizationId); + $this->checkProjectId($projectId); + + return $this->projectsApi->getOrgProject( + organizationId: $organizationId, + projectId: $projectId + ); + } + + /** + * Queries project carbon emissions metrics. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return ProjectCarbon + */ + public function getCarbonEmissions( + string $organizationId, + string $projectId, + ?DateTimeFilter $from = null, + ?DateTimeFilter $to = null, + ?string $interval = null + ): ProjectCarbon { + $this->checkOrganizationId($organizationId); + $this->checkProjectId($projectId); + if ($interval !== null && trim($interval) === '') { + throw new InvalidArgumentException('interval cannot be empty when provided.'); + } + + return $this->projectsApi->queryProjectCarbon( + organizationId: $organizationId, + projectId: $projectId, + from: $from, + to: $to, + interval: $interval ?? null + ); + } + + // Subscriptions Methods + + /** + * Checks whether the subscription can afford requested resources. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the subscription ID is invalid + */ + public function canAffordSubscription( + string $subscriptionId, + CanAffordSubscriptionRequest $canAffordSubscriptionRequest + ): void { + $this->checkSubscriptionId($subscriptionId); + + $this->subscriptionsApi->canAffordSubscription( + subscriptionId: $subscriptionId, + canAffordSubscriptionRequest: $canAffordSubscriptionRequest + ); + } + + // Discounts Methods + + /** + * Gets a discount by ID. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the discount ID is empty + * @return Discount + */ + public function getDiscount(string $id): Discount + { + if (trim($id) === '') { + throw new InvalidArgumentException('id cannot be empty.'); + } + + return $this->discountsApi->getDiscount(id: $id); + } + + /** + * Gets the First Project Incentive type allowance. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @return GetTypeAllowance200Response + */ + public function getTypeAllowance(): GetTypeAllowance200Response + { + return $this->discountsApi->getTypeAllowance(); + } + + // References Methods + + /** + * Lists referenced organizations. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return array + */ + public function listReferencedOrgs(string $in, string $sig): array + { + if (trim($in) === '') { + throw new InvalidArgumentException('in cannot be empty.'); + } + + if (trim($sig) === '') { + throw new InvalidArgumentException('sig cannot be empty.'); + } + + return $this->referencesApi->listReferencedOrgs( + in: $in, + sig: $sig + ); + } + + /** + * Lists referenced projects. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return array + */ + public function listReferencedProjects(string $in, string $sig): array + { + if (trim($in) === '') { + throw new InvalidArgumentException('in cannot be empty.'); + } + + if (trim($sig) === '') { + throw new InvalidArgumentException('sig cannot be empty.'); + } + + return $this->referencesApi->listReferencedProjects( + in: $in, + sig: $sig + ); + } + + // Organization Carbon Methods + + /** + * Queries organization carbon emissions metrics. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return OrganizationCarbon + */ + public function queryOrganiationCarbon( + string $organizationId, + ?DateTimeFilter $from = null, + ?DateTimeFilter $to = null, + ?string $interval = null + ): OrganizationCarbon { + $this->checkOrganizationId($organizationId); + if ($interval !== null && trim($interval) === '') { + throw new InvalidArgumentException('interval cannot be empty when provided.'); + } + + return $this->defaultApi->queryOrganiationCarbon( + organizationId: $organizationId, + from: $from, + to: $to, + interval: $interval ?? null + ); + } + + /** + * Stream the provisioning progress for a new project in an organization. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the organizationId is invalid + */ + public function streamOrgProjectProvisioning(string $organizationId): Generator + { + $this->checkOrganizationId($organizationId); + + return $this->projectsApi->streamOrgProjectProvisioning( + organizationId: $organizationId + ); + } } diff --git a/src/Core/Tasks/ProjectsTask.php b/src/Core/Tasks/ProjectsTask.php index 013da962..30d89cf2 100644 --- a/src/Core/Tasks/ProjectsTask.php +++ b/src/Core/Tasks/ProjectsTask.php @@ -4,9 +4,13 @@ use InvalidArgumentException; use Psr\Http\Client\ClientExceptionInterface; +use Upsun\Api\AlertsApi; use Upsun\Api\ApiException; +use Upsun\Api\DeploymentTargetApi; +use Upsun\Api\DomainClaimApi; use Upsun\Api\OrganizationProjectsApi; use Upsun\Api\ProjectApi; +use Upsun\Api\ProjectsApi; use Upsun\Api\ProjectSettingsApi; use Upsun\Api\SubscriptionsApi; use Upsun\Model\AcceptedResponse; @@ -17,10 +21,16 @@ use Upsun\Model\Certificate; use Upsun\Model\Commit; use Upsun\Model\CreateOrgSubscriptionRequest; +use Upsun\Model\DateTimeFilter; +use Upsun\Model\DeploymentTarget; +use Upsun\Model\DeploymentTargetCreateInput; +use Upsun\Model\DeploymentTargetPatch; use Upsun\Model\Domain; +use Upsun\Model\DomainClaim; use Upsun\Model\DomainCreateInput; use Upsun\Model\DomainPatch; use Upsun\Model\Environment; +use Upsun\Model\GetUsageAlerts200Response; use Upsun\Model\Integration; use Upsun\Model\IntegrationCreateCreateInput; use Upsun\Model\IntegrationPatch; @@ -29,6 +39,7 @@ use Upsun\Model\ListProjectUserAccess200Response; use Upsun\Model\Project; use Upsun\Model\ProjectCapabilities; +use Upsun\Model\ProjectCarbon; use Upsun\Model\ProjectInvitation; use Upsun\Model\ProjectSettings; use Upsun\Model\ProjectSettingsPatch; @@ -39,6 +50,7 @@ use Upsun\Model\TeamProjectAccess; use Upsun\Model\Tree; use Upsun\Model\UpdateOrgProjectRequest; +use Upsun\Model\UpdateUsageAlertsRequest; use Upsun\Model\UserProjectAccess; use Upsun\UpsunClient; @@ -47,7 +59,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class ProjectsTask extends TaskBase { @@ -57,6 +69,10 @@ public function __construct( private readonly OrganizationProjectsApi $organizationApi, private readonly ProjectSettingsApi $settingsApi, private readonly SubscriptionsApi $subscriptionsApi, + private readonly DeploymentTargetApi $deploymentTargetApi, + private readonly AlertsApi $alertsApi, + private readonly DomainClaimApi $domainClaimApi, + private readonly ProjectsApi $projectsApi, ) { parent::__construct($client); } @@ -114,6 +130,7 @@ public function create( /** * Checks if the user is able to create a new project in the organization. * + * @deprecated The underlying API endpoint is deprecated. * @throws ApiException on non-2xx response or if the response body is not in the expected format * @throws ClientExceptionInterface on network errors * @throws InvalidArgumentException if the organization ID is invalid @@ -1349,4 +1366,318 @@ public function updateIntegration( integrationUpdateInput: $integrationPatch ); } + + // Deployment Target Methods + + /** + * Creates a deployment target for a project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if the project ID is invalid + * @return AcceptedResponse + */ + public function createProjectsDeployments( + string $projectId, + DeploymentTargetCreateInput $deploymentTargetCreateInput + ): AcceptedResponse { + $this->checkProjectId($projectId); + + return $this->deploymentTargetApi->createProjectsDeployments( + projectId: $projectId, + deploymentTargetCreateInput: $deploymentTargetCreateInput + ); + } + + /** + * Deletes a deployment target from a project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return AcceptedResponse + */ + public function deleteProjectsDeployments( + string $projectId, + string $deploymentTargetConfigurationId + ): AcceptedResponse { + $this->checkProjectId($projectId); + if (trim($deploymentTargetConfigurationId) === '') { + throw new InvalidArgumentException('deploymentTargetConfigurationId cannot be empty.'); + } + + return $this->deploymentTargetApi->deleteProjectsDeployments( + projectId: $projectId, + deploymentTargetConfigurationId: $deploymentTargetConfigurationId + ); + } + + /** + * Gets a single deployment target for a project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return DeploymentTarget + */ + public function getProjectsDeployments( + string $projectId, + string $deploymentTargetConfigurationId + ): DeploymentTarget { + $this->checkProjectId($projectId); + if (trim($deploymentTargetConfigurationId) === '') { + throw new InvalidArgumentException('deploymentTargetConfigurationId cannot be empty.'); + } + + return $this->deploymentTargetApi->getProjectsDeployments( + projectId: $projectId, + deploymentTargetConfigurationId: $deploymentTargetConfigurationId + ); + } + + /** + * Lists deployment targets for a project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if the project ID is invalid + * @return DeploymentTarget[] + */ + public function listProjectsDeployments(string $projectId): array + { + $this->checkProjectId($projectId); + + return $this->deploymentTargetApi->listProjectsDeployments(projectId: $projectId); + } + + /** + * Updates a deployment target for a project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return AcceptedResponse + */ + public function updateProjectsDeployments( + string $projectId, + string $deploymentTargetConfigurationId, + DeploymentTargetPatch $deploymentTargetPatch + ): AcceptedResponse { + $this->checkProjectId($projectId); + if (trim($deploymentTargetConfigurationId) === '') { + throw new InvalidArgumentException('deploymentTargetConfigurationId cannot be empty.'); + } + + return $this->deploymentTargetApi->updateProjectsDeployments( + projectId: $projectId, + deploymentTargetConfigurationId: $deploymentTargetConfigurationId, + deploymentTargetPatch: $deploymentTargetPatch + ); + } + + // Alerts Methods + + /** + * Gets usage alerts for a subscription. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if the subscription ID is invalid + * @return GetUsageAlerts200Response + */ + public function getUsageAlerts(string $subscriptionId): GetUsageAlerts200Response + { + $this->checkSubscriptionId($subscriptionId); + + return $this->alertsApi->getUsageAlerts(subscriptionId: $subscriptionId); + } + + /** + * Updates usage alerts for a subscription. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if the subscription ID is invalid + * @return GetUsageAlerts200Response + */ + public function updateUsageAlerts( + string $subscriptionId, + UpdateUsageAlertsRequest $updateUsageAlertsRequest + ): GetUsageAlerts200Response { + $this->checkSubscriptionId($subscriptionId); + + return $this->alertsApi->updateUsageAlerts( + subscriptionId: $subscriptionId, + updateUsageAlertsRequest: $updateUsageAlertsRequest + ); + } + + // Domain Claim Methods + + /** + * Creates a domain claim for a project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if the project ID is invalid + * @return AcceptedResponse + */ + public function createProjectsDomainClaims(string $projectId, object $body): AcceptedResponse + { + $this->checkProjectId($projectId); + + return $this->domainClaimApi->createProjectsDomainClaims( + projectId: $projectId, + body: $body + ); + } + + /** + * Deletes a domain claim from a project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return AcceptedResponse + */ + public function deleteProjectsDomainClaims(string $projectId, string $domainClaimId): AcceptedResponse + { + $this->checkProjectId($projectId); + if (trim($domainClaimId) === '') { + throw new InvalidArgumentException('domainClaimId cannot be empty.'); + } + + return $this->domainClaimApi->deleteProjectsDomainClaims( + projectId: $projectId, + domainClaimId: $domainClaimId + ); + } + + /** + * Gets a single domain claim for a project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return DomainClaim + */ + public function getProjectsDomainClaims(string $projectId, string $domainClaimId): DomainClaim + { + $this->checkProjectId($projectId); + if (trim($domainClaimId) === '') { + throw new InvalidArgumentException('domainClaimId cannot be empty.'); + } + + return $this->domainClaimApi->getProjectsDomainClaims( + projectId: $projectId, + domainClaimId: $domainClaimId + ); + } + + /** + * Lists domain claims for a project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if the project ID is invalid + * @return DomainClaim[] + */ + public function listProjectsDomainClaims(string $projectId): array + { + $this->checkProjectId($projectId); + + return $this->domainClaimApi->listProjectsDomainClaims(projectId: $projectId); + } + + /** + * Updates a domain claim for a project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return AcceptedResponse + */ + public function updateProjectsDomainClaims( + string $projectId, + string $domainClaimId, + object $body + ): AcceptedResponse { + $this->checkProjectId($projectId); + if (trim($domainClaimId) === '') { + throw new InvalidArgumentException('domainClaimId cannot be empty.'); + } + + return $this->domainClaimApi->updateProjectsDomainClaims( + projectId: $projectId, + domainClaimId: $domainClaimId, + body: $body + ); + } + + // Carbon And Maintenance Methods + + /** + * Queries project carbon emissions metrics. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if required parameters are missing or invalid + * @return ProjectCarbon + */ + public function queryProjectCarbon( + string $organizationId, + string $projectId, + ?DateTimeFilter $from = null, + ?DateTimeFilter $to = null, + ?string $interval = null + ): ProjectCarbon { + $this->checkOrganizationId($organizationId); + $this->checkProjectId($projectId); + if ($interval !== null && trim($interval) === '') { + throw new InvalidArgumentException('interval cannot be empty when provided.'); + } + + return $this->organizationApi->queryProjectCarbon( + organizationId: $organizationId, + projectId: $projectId, + from: $from, + to: $to, + interval: $interval ?? null + ); + } + + /** + * Triggers a maintenance redeploy for a project. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface on network errors + * @throws InvalidArgumentException if the project ID is invalid + * @return AcceptedResponse + */ + public function maintenanceRedeployProject(string $projectId): AcceptedResponse + { + $this->checkProjectId($projectId); + + return $this->prjApi->maintenanceRedeployProject(projectId: $projectId); + } + + /** + * List project history for an organization. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the organizationId or projectId is invalid + */ + public function listOrgProjectHistory( + string $organizationId, + string $projectId + ): mixed { + $this->checkOrganizationId($organizationId); + $this->checkProjectId($projectId); + + return $this->projectsApi->listOrgProjectHistory( + organizationId: $organizationId, + projectId: $projectId + ); + } } diff --git a/src/Core/Tasks/RegionsTask.php b/src/Core/Tasks/RegionsTask.php index 1c4bdf41..effff731 100644 --- a/src/Core/Tasks/RegionsTask.php +++ b/src/Core/Tasks/RegionsTask.php @@ -2,8 +2,10 @@ namespace Upsun\Core\Tasks; +use InvalidArgumentException; use Psr\Http\Client\ClientExceptionInterface; use Upsun\Api\ApiException; +use Upsun\Api\ReferencesApi; use Upsun\Api\RegionsApi; use Upsun\Model\ListRegions200Response; use Upsun\Model\Region; @@ -15,13 +17,14 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class RegionsTask extends TaskBase { public function __construct( UpsunClient $client, private readonly RegionsApi $api, + private readonly ReferencesApi $referencesApi, ) { parent::__construct($client); } @@ -63,4 +66,25 @@ public function list( sort: $sort ); } + + /** + * List referenced regions + * This method retrieves a list of regions that are referenced by a specific signature. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the in or sig parameter is invalid or empty + * @return array + */ + public function listReferencedRegions(string $in, string $sig): array + { + if (empty($in)) { + throw new InvalidArgumentException('In parameter cannot be empty'); + } + if (empty($sig)) { + throw new InvalidArgumentException('Sig parameter cannot be empty'); + } + + return $this->referencesApi->listReferencedRegions(in: $in, sig: $sig); + } } diff --git a/src/Core/Tasks/RepositoriesTask.php b/src/Core/Tasks/RepositoriesTask.php index 1b9edc85..08b004eb 100644 --- a/src/Core/Tasks/RepositoriesTask.php +++ b/src/Core/Tasks/RepositoriesTask.php @@ -2,8 +2,10 @@ namespace Upsun\Core\Tasks; +use InvalidArgumentException; use Psr\Http\Client\ClientExceptionInterface; use Upsun\Api\ApiException; +use Upsun\Api\DiffApi; use Upsun\Api\RepositoryApi; use Upsun\Api\SystemInformationApi; use Upsun\Model\Blob; @@ -18,7 +20,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class RepositoriesTask extends TaskBase { @@ -26,6 +28,7 @@ public function __construct( UpsunClient $client, private readonly RepositoryApi $repositoryApi, private readonly SystemInformationApi $systemInformationApi, + private readonly DiffApi $diffApi, ) { parent::__construct($client); } @@ -134,4 +137,34 @@ public function getGitInfo(string $projectId): SystemInformation return $this->systemInformationApi->getProjectsSystem(projectId: $projectId); } + + /** + * List Git diffs between two references + * This method retrieves the differences between two Git references (branches, tags, or commits), + * showing what changed between them. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId, diffBaseId, or diffTargetId is invalid + * @return array + */ + public function listGitDiffs( + string $projectId, + string $diffBaseId, + string $diffTargetId + ): array { + $this->checkProjectId($projectId); + if (empty($diffBaseId)) { + throw new InvalidArgumentException('Diff base ID cannot be empty'); + } + if (empty($diffTargetId)) { + throw new InvalidArgumentException('Diff target ID cannot be empty'); + } + + return $this->diffApi->listProjectsGitDiffs( + projectId: $projectId, + diffBaseId: $diffBaseId, + diffTargetId: $diffTargetId + ); + } } diff --git a/src/Core/Tasks/ResourcesTask.php b/src/Core/Tasks/ResourcesTask.php index 4df9a927..5272bf5f 100644 --- a/src/Core/Tasks/ResourcesTask.php +++ b/src/Core/Tasks/ResourcesTask.php @@ -7,6 +7,7 @@ use Upsun\Api\ApiException; use Upsun\Api\AutoscalingApi; use Upsun\Api\DeploymentApi; +use Upsun\Api\ResourcesApi; use Upsun\Model\AcceptedResponse; use Upsun\Model\AutoscalerSettings; use Upsun\Model\Resources; @@ -18,14 +19,15 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class ResourcesTask extends TaskBase { public function __construct( UpsunClient $client, private readonly DeploymentApi $deploymentApi, - private readonly AutoscalingApi $autoscalingApi + private readonly AutoscalingApi $autoscalingApi, + private readonly ResourcesApi $resourcesApi, ) { parent::__construct($client); } @@ -216,7 +218,123 @@ public function updateAutoscalerSettings( return $this->autoscalingApi->postAutoscalerSettings( projectId: $projectId, environmentId: $environmentId, - autoscalerSettings: new AutoscalerSettings(...$services), + autoscalerSettings: new AutoscalerSettings(...($services ?? [])), + ); + } + + /** + * Patch (partially update) the autoscaler settings for the environment. + * This method allows you to make targeted updates to specific autoscaler settings without replacing the entire + * configuration. Use this when you want to update only certain aspects of the autoscaler settings. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function patchAutoscalerSettings( + string $projectId, + string $environmentId, + ?AutoscalerSettings $autoscalerSettings = null, + ): AutoscalerSettings { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->autoscalingApi->patchAutoscalerSettings( + projectId: $projectId, + environmentId: $environmentId, + autoscalerSettings: $autoscalerSettings, + ); + } + + /** + * Get resource metrics broken down by service. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function resourcesByService( + string $projectId, + string $environmentId, + string $service, + int $from, + int $to, + ?array $aggs = null, + ?array $types = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->resourcesApi->resourcesByService( + projectId: $projectId, + environmentId: $environmentId, + service: $service, + from: $from, + to: $to, + aggs: $aggs, + types: $types + ); + } + + /** + * Get an overview of resource metrics. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function resourcesOverview( + string $projectId, + string $environmentId, + int $from, + int $to, + ?string $service = null, + ?array $services = null, + ?string $servicesMode = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->resourcesApi->resourcesOverview( + projectId: $projectId, + environmentId: $environmentId, + from: $from, + to: $to, + service: $service, + services: $services, + servicesMode: $servicesMode + ); + } + + /** + * Get a summary of resource metrics. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + */ + public function resourcesSummary( + string $projectId, + string $environmentId, + int $from, + int $to, + ?array $aggs = null, + ?array $types = null, + ?array $services = null, + ?string $servicesMode = null + ): mixed { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->resourcesApi->resourcesSummary( + projectId: $projectId, + environmentId: $environmentId, + from: $from, + to: $to, + aggs: $aggs, + types: $types, + services: $services, + servicesMode: $servicesMode ); } } diff --git a/src/Core/Tasks/RoutesTask.php b/src/Core/Tasks/RoutesTask.php index e997cbe2..56d41b5c 100644 --- a/src/Core/Tasks/RoutesTask.php +++ b/src/Core/Tasks/RoutesTask.php @@ -14,7 +14,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class RoutesTask extends TaskBase { diff --git a/src/Core/Tasks/ServicesTask.php b/src/Core/Tasks/ServicesTask.php index 4cfac2ab..3addd7cc 100644 --- a/src/Core/Tasks/ServicesTask.php +++ b/src/Core/Tasks/ServicesTask.php @@ -13,7 +13,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class ServicesTask extends TaskBase { diff --git a/src/Core/Tasks/SourceOperationsTask.php b/src/Core/Tasks/SourceOperationsTask.php index f7cb7b00..e983167d 100644 --- a/src/Core/Tasks/SourceOperationsTask.php +++ b/src/Core/Tasks/SourceOperationsTask.php @@ -16,7 +16,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class SourceOperationsTask extends TaskBase { diff --git a/src/Core/Tasks/SshTask.php b/src/Core/Tasks/SshTask.php index 4e9851eb..9ff40a82 100644 --- a/src/Core/Tasks/SshTask.php +++ b/src/Core/Tasks/SshTask.php @@ -15,7 +15,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class SshTask extends TaskBase { diff --git a/src/Core/Tasks/SupportTicketsTask.php b/src/Core/Tasks/SupportTicketsTask.php index 64dc3b16..b3f1217a 100644 --- a/src/Core/Tasks/SupportTicketsTask.php +++ b/src/Core/Tasks/SupportTicketsTask.php @@ -21,7 +21,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class SupportTicketsTask extends TaskBase { @@ -118,10 +118,12 @@ public function create( */ public function listCategories(?string $organizationId = null, ?string $projectId = null): array { - $this->checkOrganizationId($organizationId); + if ($organizationId !== null) { + $this->checkOrganizationId($organizationId); + } $project = $projectId ? $this->client->projects->get($projectId) : null; - $subscriptionId = $projectId ? + $subscriptionId = $project ? $this->extractSubscriptionId($project->getSubscription()->getLicenseUri()) : null; return $this->supportApi->listTicketCategories( @@ -141,8 +143,8 @@ public function listCategories(?string $organizationId = null, ?string $projectI public function listPriorities(?string $projectId = null, ?string $category = null): array { $project = $projectId ? $this->client->projects->get($projectId) : null; - $path = parse_url($project?->getSubscription()->getLicenseUri(), PHP_URL_PATH); - $subscriptionId = basename($path); + $subscriptionId = $project ? + $this->extractSubscriptionId($project->getSubscription()->getLicenseUri()) : null; return $this->supportApi->listTicketPriorities(subscriptionId: $subscriptionId, category: $category); } diff --git a/src/Core/Tasks/SystemOperationsTask.php b/src/Core/Tasks/SystemOperationsTask.php new file mode 100644 index 00000000..4e486b6b --- /dev/null +++ b/src/Core/Tasks/SystemOperationsTask.php @@ -0,0 +1,43 @@ +checkProjectId($projectId); + + return $this->systemInformationApi->actionProjectsSystemRestart( + projectId: $projectId + ); + } +} diff --git a/src/Core/Tasks/TaskBase.php b/src/Core/Tasks/TaskBase.php index f77d1ac1..ec30eb8a 100644 --- a/src/Core/Tasks/TaskBase.php +++ b/src/Core/Tasks/TaskBase.php @@ -12,7 +12,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ abstract class TaskBase { @@ -242,6 +242,13 @@ protected static function checkTicketId(string $ticketId): void } } + protected static function checkTaskId(string $taskId): void + { + if (trim($taskId) === '') { + throw new InvalidArgumentException('Task ID is required'); + } + } + protected function normalizeFilter(array|string|int|DateTime|null $value): array { if ($value === null) { diff --git a/src/Core/Tasks/TaskContainersTask.php b/src/Core/Tasks/TaskContainersTask.php new file mode 100644 index 00000000..c70442d6 --- /dev/null +++ b/src/Core/Tasks/TaskContainersTask.php @@ -0,0 +1,152 @@ +checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + if (empty($taskId)) { + throw new InvalidArgumentException('Task ID cannot be empty'); + } + + return $this->taskApi->getProjectsEnvironmentsTasks( + projectId: $projectId, + environmentId: $environmentId, + taskId: $taskId + ); + } + + /** + * List all tasks in an environment + * This method retrieves a list of all asynchronous tasks that have been run in an environment, + * allowing you to review the history of tasks and their statuses. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId or environmentId is invalid + * @return Task[] + */ + public function list( + string $projectId, + string $environmentId + ): array { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + + return $this->taskApi->listProjectsEnvironmentsTasks( + projectId: $projectId, + environmentId: $environmentId + ); + } + + /** + * Run a task in an environment + * This method executes an asynchronous task in the specified environment. Tasks are long-running operations + * that can be executed in the background and tracked via the get() method. + * + * The API expects variables to be a nested map grouped by namespace (e.g. `env`): + * + * variables: [ + * 'env' => [ + * 'INCIDENT_JSON' => json_encode($incident), + * 'INCIDENT_SIGNATURE' => $signature, + * ], + * ] + * + * For convenience a flat map of `NAME => value` is also accepted and will be + * wrapped automatically under the `env` group. + * + * @param array $variables + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the projectId, environmentId, or taskId is invalid + */ + public function run( + string $projectId, + string $environmentId, + string $taskId, + array $variables = [] + ): AcceptedResponse { + $this->checkProjectId($projectId); + $this->checkEnvironmentId($environmentId); + $this->checkTaskId($taskId); + + return $this->taskApi->runTask( + projectId: $projectId, + environmentId: $environmentId, + taskId: $taskId, + taskTriggerInput: new TaskTriggerInput( + variables: $this->normalizeVariables($variables) + ) + ); + } + + /** + * Normalize task variables into the nested map the API expects: `{ group: { NAME: value } }`. + * + * Already-grouped input (every top-level value is an array) is returned untouched. + * A flat `NAME => value` map is wrapped under the `env` group. + * + * @param array $variables + * + * @return array> + */ + private function normalizeVariables(array $variables): array + { + if ($variables === []) { + return []; + } + + $allGroups = true; + foreach ($variables as $value) { + if (!is_array($value)) { + $allGroups = false; + break; + } + } + + if ($allGroups) { + return $variables; + } + + return ['env' => $variables]; + } +} diff --git a/src/Core/Tasks/TeamsTask.php b/src/Core/Tasks/TeamsTask.php index 4ca877ea..9085f982 100644 --- a/src/Core/Tasks/TeamsTask.php +++ b/src/Core/Tasks/TeamsTask.php @@ -5,6 +5,7 @@ use InvalidArgumentException; use Psr\Http\Client\ClientExceptionInterface; use Upsun\Api\ApiException; +use Upsun\Api\ReferencesApi; use Upsun\Api\TeamAccessApi; use Upsun\Api\TeamsApi; use Upsun\Model\CreateTeamMemberRequest; @@ -25,7 +26,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class TeamsTask extends TaskBase { @@ -33,6 +34,7 @@ public function __construct( UpsunClient $client, private readonly TeamsApi $teamsApi, private readonly TeamAccessApi $accessApi, + private readonly ReferencesApi $referencesApi, ) { parent::__construct($client); } @@ -381,7 +383,6 @@ public function grantProjectTeamAccess(string $projectId, array $access): void public function grantTeamProjectAccessToTeam(string $teamId, array $access): void { $this->checkTeamId($teamId); - $this->checkProjectId($teamId); $this->accessApi->grantTeamProjectAccess(teamId: $teamId, grantTeamProjectAccessRequestInner: $access); } @@ -552,4 +553,25 @@ public function revokeTeamProjectAccess(string $teamId, string $projectId): void { $this->revokeTeamProjectAccessByTeam(teamId: $teamId, projectId: $projectId); } + + /** + * List referenced teams + * This method retrieves a list of teams that are referenced by a specific signature. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the in or sig parameter is invalid or empty + * @return array + */ + public function listReferencedTeams(string $in, string $sig): array + { + if (empty($in)) { + throw new InvalidArgumentException('In parameter cannot be empty'); + } + if (empty($sig)) { + throw new InvalidArgumentException('Sig parameter cannot be empty'); + } + + return $this->referencesApi->listReferencedTeams(in: $in, sig: $sig); + } } diff --git a/src/Core/Tasks/UsersInvitationsTask.php b/src/Core/Tasks/UsersInvitationsTask.php index 8f3a2bce..3b4ed8e5 100644 --- a/src/Core/Tasks/UsersInvitationsTask.php +++ b/src/Core/Tasks/UsersInvitationsTask.php @@ -19,7 +19,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class UsersInvitationsTask extends TaskBase { diff --git a/src/Core/Tasks/UsersTask.php b/src/Core/Tasks/UsersTask.php index 201f97ac..aa34daf8 100644 --- a/src/Core/Tasks/UsersTask.php +++ b/src/Core/Tasks/UsersTask.php @@ -5,12 +5,14 @@ use BadMethodCallException; use InvalidArgumentException; use Psr\Http\Client\ClientExceptionInterface; +use SplFileObject; use Upsun\Api\ApiException; use Upsun\Api\ApiTokensApi; use Upsun\Api\ConnectionsApi; use Upsun\Api\GrantsApi; use Upsun\Api\MfaApi; use Upsun\Api\PhoneNumberApi; +use Upsun\Api\ReferencesApi; use Upsun\Api\UserAccessApi; use Upsun\Api\UserProfilesApi; use Upsun\Api\UsersApi; @@ -21,6 +23,7 @@ use Upsun\Model\ConfirmTotpEnrollmentRequest; use Upsun\Model\Connection; use Upsun\Model\CreateApiTokenRequest; +use Upsun\Model\CreateProfilePicture200Response; use Upsun\Model\GetAddress200Response; use Upsun\Model\GetCurrentUserVerificationStatus200Response; use Upsun\Model\GetCurrentUserVerificationStatusFull200Response; @@ -45,7 +48,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class UsersTask extends TaskBase { @@ -59,6 +62,7 @@ public function __construct( private readonly GrantsApi $grantsApi, private readonly MfaApi $mfaApi, private readonly PhoneNumberApi $phoneNumberApi, + private readonly ReferencesApi $referencesApi, ) { parent::__construct($client); } @@ -1031,4 +1035,59 @@ public function verifyPhoneNumber( ) ); } + + /** + * Create a profile picture for a user + * This method allows you to upload and set a profile picture for a user. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the user ID is invalid + */ + public function createProfilePicture( + string $userId, + ?SplFileObject $file = null + ): CreateProfilePicture200Response { + $this->checkUserId($userId); + + return $this->profilesApi->createProfilePicture(uuid: $userId, file: $file); + } + + /** + * Get an access document + * This method retrieves an access document associated with the specified access ID. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the access ID is invalid or empty + */ + public function getAccessDocument(string $accessId): object + { + if (empty($accessId)) { + throw new InvalidArgumentException('Access ID cannot be empty'); + } + + return $this->grantsApi->getAccessDocument(accessId: $accessId); + } + + /** + * List referenced users + * This method retrieves a list of users that are referenced by a specific signature. + * + * @throws ApiException on non-2xx response or if the response body is not in the expected format + * @throws ClientExceptionInterface + * @throws InvalidArgumentException if the in or sig parameter is invalid or empty + * @return array + */ + public function listReferencedUsers(string $in, string $sig): array + { + if (empty($in)) { + throw new InvalidArgumentException('In parameter cannot be empty'); + } + if (empty($sig)) { + throw new InvalidArgumentException('Sig parameter cannot be empty'); + } + + return $this->referencesApi->listReferencedUsers(in: $in, sig: $sig); + } } diff --git a/src/Core/Tasks/VariablesTask.php b/src/Core/Tasks/VariablesTask.php index 1a474577..a2f27107 100644 --- a/src/Core/Tasks/VariablesTask.php +++ b/src/Core/Tasks/VariablesTask.php @@ -21,7 +21,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class VariablesTask extends TaskBase { diff --git a/src/Core/Tasks/WorkersTask.php b/src/Core/Tasks/WorkersTask.php index 91998f1a..66cef14b 100644 --- a/src/Core/Tasks/WorkersTask.php +++ b/src/Core/Tasks/WorkersTask.php @@ -13,7 +13,7 @@ * * @author Upsun Advocacy Team * @license MIT - * @see https://docs.upsun.com + * @see https://developer.upsun.com */ class WorkersTask extends TaskBase { diff --git a/src/Model/AllowedDomainsInner.php b/src/Model/AllowedDomainsInner.php new file mode 100644 index 00000000..eefb91ec --- /dev/null +++ b/src/Model/AllowedDomainsInner.php @@ -0,0 +1,49 @@ + $this->host, + 'port' => $this->port, + ]; + } + + public function __toString(): string + { + return json_encode($this->jsonSerialize(), JSON_PRETTY_PRINT); + } + + public function getHost(): string + { + return $this->host; + } + + public function getPort(): ?int + { + return $this->port; + } +} diff --git a/src/Model/Autoscaling.php b/src/Model/Autoscaling.php index c4e1b488..5df5a799 100644 --- a/src/Model/Autoscaling.php +++ b/src/Model/Autoscaling.php @@ -15,6 +15,7 @@ final class Autoscaling implements Model, JsonSerializable { public function __construct( private readonly bool $enabled, + private readonly bool $supportsHorizontalScalingServices, ) { } @@ -27,6 +28,7 @@ public function jsonSerialize(): array { return [ 'enabled' => $this->enabled, + 'supportsHorizontalScalingServices' => $this->supportsHorizontalScalingServices, ]; } @@ -42,4 +44,12 @@ public function getEnabled(): bool { return $this->enabled; } + + /** + * Allow autoscaling to be configured for services (not just apps and workers) + */ + public function getSupportsHorizontalScalingServices(): bool + { + return $this->supportsHorizontalScalingServices; + } } diff --git a/src/Model/Backup.php b/src/Model/Backup.php index 1cbae5df..cc07c6ab 100644 --- a/src/Model/Backup.php +++ b/src/Model/Backup.php @@ -32,6 +32,7 @@ public function __construct( private readonly ?int $index, private readonly ?int $sizeOfVolumes, private readonly ?int $sizeUsed, + private readonly ?int $sizeObjectStorage, private readonly ?string $deployment, ) { } @@ -56,6 +57,7 @@ public function jsonSerialize(): array 'safe' => $this->safe, 'sizeOfVolumes' => $this->sizeOfVolumes, 'sizeUsed' => $this->sizeUsed, + 'sizeObjectStorage' => $this->sizeObjectStorage, 'deployment' => $this->deployment, 'restorable' => $this->restorable, 'automated' => $this->automated, @@ -160,6 +162,14 @@ public function getSizeUsed(): ?int return $this->sizeUsed; } + /** + * Total object-storage quota configured on webapps at backup time + */ + public function getSizeObjectStorage(): ?int + { + return $this->sizeObjectStorage; + } + /** * The current deployment at the time of backup */ diff --git a/src/Model/BasicAuth.php b/src/Model/BasicAuth.php index 7a6dfcc9..0a0216da 100644 --- a/src/Model/BasicAuth.php +++ b/src/Model/BasicAuth.php @@ -9,7 +9,7 @@ * Basic Auth for the container registry * * @license Apache-2.0 - * @see https://docs.upsun.com + * @see https://developer.upsun.com * @generated This file was generated by OpenAPI Generator. Do not edit manually. */ final class BasicAuth implements Model, JsonSerializable diff --git a/src/Model/BuildPhaseEgress.php b/src/Model/BuildPhaseEgress.php new file mode 100644 index 00000000..5c4d872e --- /dev/null +++ b/src/Model/BuildPhaseEgress.php @@ -0,0 +1,44 @@ + $this->allowedDomains, + ]; + } + + public function __toString(): string + { + return json_encode($this->jsonSerialize(), JSON_PRETTY_PRINT); + } + /** + * @return AllowedDomainsInner[] + */ + public function getAllowedDomains(): array + { + return $this->allowedDomains; + } +} diff --git a/src/Model/Egress.php b/src/Model/Egress.php new file mode 100644 index 00000000..38131d2c --- /dev/null +++ b/src/Model/Egress.php @@ -0,0 +1,49 @@ + $this->build, + 'runtime' => $this->runtime, + ]; + } + + public function __toString(): string + { + return json_encode($this->jsonSerialize(), JSON_PRETTY_PRINT); + } + + public function getBuild(): ?BuildPhaseEgress + { + return $this->build; + } + + public function getRuntime(): ?RuntimePhaseEgress + { + return $this->runtime; + } +} diff --git a/src/Model/EgressProxy.php b/src/Model/EgressProxy.php new file mode 100644 index 00000000..3a4ce068 --- /dev/null +++ b/src/Model/EgressProxy.php @@ -0,0 +1,46 @@ + $this->enabled, + ]; + } + + public function __toString(): string + { + return json_encode($this->jsonSerialize(), JSON_PRETTY_PRINT); + } + + /** + * If true, the egress proxy is on for every container in the project: outbound traffic is denied by default and + * must traverse the proxy. Applies project-wide regardless of whether an application declares an egress allowlist. + */ + public function getEnabled(): bool + { + return $this->enabled; + } +} diff --git a/src/Model/ProjectCapabilities.php b/src/Model/ProjectCapabilities.php index bfd1ca06..3dc03072 100644 --- a/src/Model/ProjectCapabilities.php +++ b/src/Model/ProjectCapabilities.php @@ -28,6 +28,7 @@ public function __construct( private readonly ?SourceOperations $sourceOperations = null, private readonly ?RuntimeOperations $runtimeOperations = null, private readonly ?OutboundFirewall $outboundFirewall = null, + private readonly ?EgressProxy $egressProxy = null, private readonly ?Integrations $integrations = null, ) { } @@ -54,6 +55,7 @@ public function jsonSerialize(): array 'sourceOperations' => $this->sourceOperations, 'runtimeOperations' => $this->runtimeOperations, 'outboundFirewall' => $this->outboundFirewall, + 'egressProxy' => $this->egressProxy, 'integrations' => $this->integrations, ]; } @@ -136,6 +138,11 @@ public function getOutboundFirewall(): ?OutboundFirewall return $this->outboundFirewall; } + public function getEgressProxy(): ?EgressProxy + { + return $this->egressProxy; + } + public function getIntegrations(): ?Integrations { return $this->integrations; diff --git a/src/Model/RegistryCredential.php b/src/Model/RegistryCredential.php index 3cdabe24..f332be52 100644 --- a/src/Model/RegistryCredential.php +++ b/src/Model/RegistryCredential.php @@ -9,7 +9,7 @@ * Low level RegistryCredential (auto-generated) * * @license Apache-2.0 - * @see https://docs.upsun.com + * @see https://developer.upsun.com * @generated This file was generated by OpenAPI Generator. Do not edit manually. */ final class RegistryCredential implements Model, JsonSerializable diff --git a/src/Model/RegistryCredentialCreateInput.php b/src/Model/RegistryCredentialCreateInput.php index 172fcba9..6fdc1416 100644 --- a/src/Model/RegistryCredentialCreateInput.php +++ b/src/Model/RegistryCredentialCreateInput.php @@ -8,7 +8,7 @@ * Low level RegistryCredentialCreateInput (auto-generated) * * @license Apache-2.0 - * @see https://docs.upsun.com + * @see https://developer.upsun.com * @generated This file was generated by OpenAPI Generator. Do not edit manually. */ final class RegistryCredentialCreateInput implements Model, JsonSerializable diff --git a/src/Model/RegistryCredentialPatch.php b/src/Model/RegistryCredentialPatch.php index 4536da8e..7c147731 100644 --- a/src/Model/RegistryCredentialPatch.php +++ b/src/Model/RegistryCredentialPatch.php @@ -8,7 +8,7 @@ * Low level RegistryCredentialPatch (auto-generated) * * @license Apache-2.0 - * @see https://docs.upsun.com + * @see https://developer.upsun.com * @generated This file was generated by OpenAPI Generator. Do not edit manually. */ final class RegistryCredentialPatch implements Model, JsonSerializable diff --git a/src/Model/RuntimePhaseEgress.php b/src/Model/RuntimePhaseEgress.php new file mode 100644 index 00000000..9e36aea5 --- /dev/null +++ b/src/Model/RuntimePhaseEgress.php @@ -0,0 +1,44 @@ + $this->allowedDomains, + ]; + } + + public function __toString(): string + { + return json_encode($this->jsonSerialize(), JSON_PRETTY_PRINT); + } + /** + * @return AllowedDomainsInner[] + */ + public function getAllowedDomains(): array + { + return $this->allowedDomains; + } +} diff --git a/src/Model/Task.php b/src/Model/Task.php index d985b3e1..e67f92d8 100644 --- a/src/Model/Task.php +++ b/src/Model/Task.php @@ -15,20 +15,20 @@ final class Task implements Model, JsonSerializable { public function __construct( private readonly string $id, - private readonly string $type, - private readonly SourceCodeConfiguration1 $source, - private readonly Hooks1 $hooks, + private readonly array $authorizations, private readonly array $relationships, private readonly array $additionalHosts, private readonly array $mounts, private readonly array $variables, + private readonly string $type, + private readonly SourceCodeConfiguration1 $source, + private readonly Hooks1 $hooks, private readonly array $dependencies, private readonly object $runtime, - private readonly array $authorizations, private readonly RunConfiguration $run, private readonly string $name, - private readonly ?string $timezone, private readonly ?Resources9 $resources, + private readonly ?string $timezone, private readonly ?string $containerProfile, ) { } @@ -42,20 +42,20 @@ public function jsonSerialize(): array { return [ 'id' => $this->id, - 'type' => $this->type, - 'source' => $this->source, - 'hooks' => $this->hooks, + 'resources' => $this->resources, + 'authorizations' => $this->authorizations, 'relationships' => $this->relationships, 'additionalHosts' => $this->additionalHosts, 'mounts' => $this->mounts, 'timezone' => $this->timezone, 'variables' => $this->variables, + 'containerProfile' => $this->containerProfile, + 'type' => $this->type, + 'source' => $this->source, + 'hooks' => $this->hooks, 'dependencies' => $this->dependencies, 'runtime' => $this->runtime, - 'authorizations' => $this->authorizations, 'run' => $this->run, - 'resources' => $this->resources, - 'containerProfile' => $this->containerProfile, 'name' => $this->name, ]; } @@ -74,31 +74,24 @@ public function getId(): string } /** - * The runtime type and version for the task (e.g., python:3.8) - */ - public function getType(): string - { - return $this->type; - } - - /** - * Configuration related to the source code of the task + * Resources configuration (base memory and memory ratio) */ - public function getSource(): SourceCodeConfiguration1 + public function getResources(): ?Resources9 { - return $this->source; + return $this->resources; } /** - * Scripts executed at various points in the lifecycle of the task + * Authorizations available to this container + * @return AuthorizationsInner[] */ - public function getHooks(): Hooks1 + public function getAuthorizations(): array { - return $this->hooks; + return $this->authorizations; } /** - * The relationships of the task to defined services and applications + * The relationships of the container to defined services * @return ServiceRelationshipsValue[] */ public function getRelationships(): array @@ -112,7 +105,7 @@ public function getAdditionalHosts(): array } /** - * Filesystem mounts of this task + * Filesystem mounts of this container. If not specified the container will have no writeable disk space * @return MountsValue[] */ public function getMounts(): array @@ -133,50 +126,57 @@ public function getVariables(): array return $this->variables; } - public function getDependencies(): array + /** + * Selected container profile for this container + */ + public function getContainerProfile(): ?string { - return $this->dependencies; + return $this->containerProfile; } /** - * Runtime-specific configuration + * The runtime type and version for the task (e.g., python:3.8) */ - public function getRuntime(): object + public function getType(): string { - return $this->runtime; + return $this->type; } /** - * Authorizations available to this task - * @return AuthorizationsInner[] + * Configuration related to the source code of the task */ - public function getAuthorizations(): array + public function getSource(): SourceCodeConfiguration1 { - return $this->authorizations; + return $this->source; } /** - * Configuration for task execution + * Scripts executed at various points in the lifecycle of the task */ - public function getRun(): RunConfiguration + public function getHooks(): Hooks1 { - return $this->run; + return $this->hooks; + } + + public function getDependencies(): array + { + return $this->dependencies; } /** - * Resources configuration (base memory and memory ratio) + * Runtime-specific configuration */ - public function getResources(): ?Resources9 + public function getRuntime(): object { - return $this->resources; + return $this->runtime; } /** - * Selected container profile for the task + * Configuration for task execution */ - public function getContainerProfile(): ?string + public function getRun(): RunConfiguration { - return $this->containerProfile; + return $this->run; } /** diff --git a/src/Model/WebApplicationsValue.php b/src/Model/WebApplicationsValue.php index 5ee65bce..116fd431 100644 --- a/src/Model/WebApplicationsValue.php +++ b/src/Model/WebApplicationsValue.php @@ -26,13 +26,13 @@ final class WebApplicationsValue implements Model, JsonSerializable public const ACCESS_VIEWER = 'viewer'; public function __construct( - private readonly string $size, - private readonly array $access, private readonly array $authorizations, private readonly array $relationships, private readonly array $additionalHosts, private readonly array $mounts, private readonly array $variables, + private readonly string $size, + private readonly array $access, private readonly array $operations, private readonly string $name, private readonly string $type, @@ -52,10 +52,11 @@ public function __construct( private readonly string $slugId, private readonly bool $supportsHorizontalScaling, private readonly ?Resources1 $resources, - private readonly ?int $disk, private readonly ?string $timezone, - private readonly ?Firewall $firewall, private readonly ?string $containerProfile, + private readonly ?int $disk, + private readonly ?Firewall $firewall, + private readonly ?Egress $egress, private readonly ?object $endpoints, private readonly ?OCIImage $image, private readonly ?int $instanceCount, @@ -71,17 +72,18 @@ public function jsonSerialize(): array { return [ 'resources' => $this->resources, - 'size' => $this->size, - 'disk' => $this->disk, - 'access' => $this->access, 'authorizations' => $this->authorizations, 'relationships' => $this->relationships, 'additionalHosts' => $this->additionalHosts, 'mounts' => $this->mounts, 'timezone' => $this->timezone, 'variables' => $this->variables, - 'firewall' => $this->firewall, 'containerProfile' => $this->containerProfile, + 'size' => $this->size, + 'disk' => $this->disk, + 'access' => $this->access, + 'firewall' => $this->firewall, + 'egress' => $this->egress, 'operations' => $this->operations, 'name' => $this->name, 'type' => $this->type, @@ -116,21 +118,6 @@ public function getResources(): ?Resources1 return $this->resources; } - public function getSize(): string - { - return $this->size; - } - - public function getDisk(): ?int - { - return $this->disk; - } - - public function getAccess(): array - { - return $this->access; - } - /** * @return AuthorizationsInner[] */ @@ -170,14 +157,34 @@ public function getVariables(): array return $this->variables; } + public function getContainerProfile(): ?string + { + return $this->containerProfile; + } + + public function getSize(): string + { + return $this->size; + } + + public function getDisk(): ?int + { + return $this->disk; + } + + public function getAccess(): array + { + return $this->access; + } + public function getFirewall(): ?Firewall { return $this->firewall; } - public function getContainerProfile(): ?string + public function getEgress(): ?Egress { - return $this->containerProfile; + return $this->egress; } /** diff --git a/src/Model/WorkersValue.php b/src/Model/WorkersValue.php index 8e869b9f..c6bfb6b3 100644 --- a/src/Model/WorkersValue.php +++ b/src/Model/WorkersValue.php @@ -26,13 +26,13 @@ final class WorkersValue implements Model, JsonSerializable public const ACCESS_VIEWER = 'viewer'; public function __construct( - private readonly string $size, - private readonly array $access, private readonly array $authorizations, private readonly array $relationships, private readonly array $additionalHosts, private readonly array $mounts, private readonly array $variables, + private readonly string $size, + private readonly array $access, private readonly array $operations, private readonly string $name, private readonly string $type, @@ -46,10 +46,11 @@ public function __construct( private readonly string $slugId, private readonly bool $supportsHorizontalScaling, private readonly ?Resources $resources, - private readonly ?int $disk, private readonly ?string $timezone, - private readonly ?Firewall $firewall, private readonly ?string $containerProfile, + private readonly ?int $disk, + private readonly ?Firewall $firewall, + private readonly ?Egress $egress, private readonly ?object $endpoints, private readonly ?OCIImage $image, private readonly ?int $instanceCount, @@ -65,17 +66,18 @@ public function jsonSerialize(): array { return [ 'resources' => $this->resources, - 'size' => $this->size, - 'disk' => $this->disk, - 'access' => $this->access, 'authorizations' => $this->authorizations, 'relationships' => $this->relationships, 'additionalHosts' => $this->additionalHosts, 'mounts' => $this->mounts, 'timezone' => $this->timezone, 'variables' => $this->variables, - 'firewall' => $this->firewall, 'containerProfile' => $this->containerProfile, + 'size' => $this->size, + 'disk' => $this->disk, + 'access' => $this->access, + 'firewall' => $this->firewall, + 'egress' => $this->egress, 'operations' => $this->operations, 'name' => $this->name, 'type' => $this->type, @@ -104,21 +106,6 @@ public function getResources(): ?Resources return $this->resources; } - public function getSize(): string - { - return $this->size; - } - - public function getDisk(): ?int - { - return $this->disk; - } - - public function getAccess(): array - { - return $this->access; - } - /** * @return AuthorizationsInner[] */ @@ -158,14 +145,34 @@ public function getVariables(): array return $this->variables; } + public function getContainerProfile(): ?string + { + return $this->containerProfile; + } + + public function getSize(): string + { + return $this->size; + } + + public function getDisk(): ?int + { + return $this->disk; + } + + public function getAccess(): array + { + return $this->access; + } + public function getFirewall(): ?Firewall { return $this->firewall; } - public function getContainerProfile(): ?string + public function getEgress(): ?Egress { - return $this->containerProfile; + return $this->egress; } /** diff --git a/src/UpsunClient.php b/src/UpsunClient.php index b9ee2887..07aed40e 100644 --- a/src/UpsunClient.php +++ b/src/UpsunClient.php @@ -8,20 +8,30 @@ use Psr\Http\Client\ClientInterface; use RuntimeException; use Upsun\Api\AddOnsApi; +use Upsun\Api\AlertsApi; use Upsun\Api\ApiConfiguration; use Upsun\Api\ApiTokensApi; use Upsun\Api\AutoscalingApi; +use Upsun\Api\BlackfireMonitoringApi; +use Upsun\Api\BlackfireProfilingApi; use Upsun\Api\CertManagementApi; use Upsun\Api\ConnectionsApi; +use Upsun\Api\ContinuousProfilingApi; use Upsun\Api\DefaultApi; use Upsun\Api\DeploymentApi; +use Upsun\Api\DeploymentTargetApi; +use Upsun\Api\DiffApi; +use Upsun\Api\DiscountsApi; +use Upsun\Api\DomainClaimApi; use Upsun\Api\DomainManagementApi; +use Upsun\Api\EntrypointApi; use Upsun\Api\EnvironmentActivityApi; use Upsun\Api\EnvironmentApi; use Upsun\Api\EnvironmentBackupsApi; use Upsun\Api\EnvironmentTypeApi; use Upsun\Api\EnvironmentVariablesApi; use Upsun\Api\GrantsApi; +use Upsun\Api\HttpTrafficApi; use Upsun\Api\InvoicesApi; use Upsun\Api\MfaApi; use Upsun\Api\OrdersApi; @@ -30,15 +40,17 @@ use Upsun\Api\OrganizationProjectsApi; use Upsun\Api\OrganizationsApi; use Upsun\Api\PhoneNumberApi; -use Upsun\Api\ProfilesApi; use Upsun\Api\ProjectActivityApi; use Upsun\Api\ProjectApi; use Upsun\Api\ProjectInvitationsApi; +use Upsun\Api\ProjectsApi; use Upsun\Api\ProjectSettingsApi; use Upsun\Api\ProjectVariablesApi; use Upsun\Api\RecordsApi; +use Upsun\Api\ReferencesApi; use Upsun\Api\RegionsApi; use Upsun\Api\RepositoryApi; +use Upsun\Api\ResourcesApi; use Upsun\Api\RoutingApi; use Upsun\Api\RuntimeOperationsApi; use Upsun\Api\SourceOperationsApi; @@ -46,6 +58,7 @@ use Upsun\Api\SubscriptionsApi; use Upsun\Api\SupportApi; use Upsun\Api\SystemInformationApi; +use Upsun\Api\TaskApi; use Upsun\Api\TeamAccessApi; use Upsun\Api\TeamsApi; use Upsun\Api\ThirdPartyIntegrationsApi; @@ -74,6 +87,7 @@ use Upsun\Core\Tasks\SourceOperationsTask; use Upsun\Core\Tasks\SshTask; use Upsun\Core\Tasks\SupportTicketsTask; +use Upsun\Core\Tasks\TaskContainersTask; use Upsun\Core\Tasks\TeamsTask; use Upsun\Core\Tasks\UsersInvitationsTask; use Upsun\Core\Tasks\UsersTask; @@ -140,6 +154,8 @@ class UpsunClient public SshTask $sshTask; + public TaskContainersTask $taskContainers; + public TeamsTask $teams; public SupportTicketsTask $supportTickets; @@ -159,7 +175,12 @@ public function __construct(protected UpsunConfig $upsunConfig, ?ClientInterface $requestFactory = Psr17FactoryDiscovery::findRequestFactory(); - if ($upsunConfig->apiToken !== '') { + $isLocalAuth = $this->upsunConfig->auth_url === UpsunConfig::LOCAL_AUTH; + $grantType = $isLocalAuth + ? OAuthProvider::GRANT_CLIENT_CREDENTIALS + : OAuthProvider::GRANT_API_TOKEN; + + if ($upsunConfig->apiToken !== '' || $isLocalAuth) { $this->auth = new OAuthProvider( httpClient: $this->apiClient, requestFactory: $requestFactory, @@ -167,6 +188,8 @@ public function __construct(protected UpsunConfig $upsunConfig, ?ClientInterface clientId: $this->upsunConfig->clientId, clientSecret: $this->upsunConfig->apiToken, refreshEndpoint: $this->upsunConfig->auth_url . '/' . $this->upsunConfig->refresh_endpoint, + grantType: $grantType, + tokenTtl: $this->upsunConfig->tokenTtl, ); } @@ -194,11 +217,16 @@ public function __invoke(bool $force = false): string // Init used API classes $addOnsApi = new AddOnsApi(...$taskParams); $apiTokensApi = new ApiTokensApi(...$taskParams); + $alertsApi = new AlertsApi(...$taskParams); $autoscalingApi = new AutoscalingApi(...$taskParams); $certManagementApi = new CertManagementApi(...$taskParams); $connectionsApi = new ConnectionsApi(...$taskParams); $defaultApi = new DefaultApi(...$taskParams); $deploymentApi = new DeploymentApi(...$taskParams); + $deploymentTargetApi = new DeploymentTargetApi(...$taskParams); + $diffApi = new DiffApi(...$taskParams); + $discountsApi = new DiscountsApi(...$taskParams); + $domainClaimApi = new DomainClaimApi(...$taskParams); $domainManagementApi = new DomainManagementApi(...$taskParams); $environmentActivityApi = new EnvironmentActivityApi(...$taskParams); $environmentApi = new EnvironmentApi(...$taskParams); @@ -206,6 +234,7 @@ public function __invoke(bool $force = false): string $environmentTypeApi = new EnvironmentTypeApi(...$taskParams); $environmentVariablesApi = new EnvironmentVariablesApi(...$taskParams); $grantsApi = new GrantsApi(...$taskParams); + $httpTrafficApi = new HttpTrafficApi(...$taskParams); $invoicesApi = new InvoicesApi(...$taskParams); $mfaApi = new MfaApi(...$taskParams); $ordersApi = new OrdersApi(...$taskParams); @@ -214,22 +243,29 @@ public function __invoke(bool $force = false): string $organizationMembersApi = new OrganizationMembersApi(...$taskParams); $organizationProjectsApi = new OrganizationProjectsApi(...$taskParams); $phoneNumberApi = new PhoneNumberApi(...$taskParams); - $profilesApi = new ProfilesApi(...$taskParams); + $blackfireMonitoringApi = new BlackfireMonitoringApi(...$taskParams); + $blackfireProfilingApi = new BlackfireProfilingApi(...$taskParams); $projectActivityApi = new ProjectActivityApi(...$taskParams); $projectApi = new ProjectApi(...$taskParams); $projectInvitationsApi = new ProjectInvitationsApi(...$taskParams); $projectSettingsApi = new ProjectSettingsApi(...$taskParams); $projectVariablesApi = new ProjectVariablesApi(...$taskParams); + $continuousProfilingApi = new ContinuousProfilingApi(...$taskParams); + $entrypointApi = new EntrypointApi(...$taskParams); + $referencesApi = new ReferencesApi(...$taskParams); $recordsApi = new RecordsApi(...$taskParams); $regionsApi = new RegionsApi(...$taskParams); $repositoryApi = new RepositoryApi(...$taskParams); $routingApi = new RoutingApi(...$taskParams); $runtimeOperationsApi = new RuntimeOperationsApi(...$taskParams); + $resourcesApi = new ResourcesApi(...$taskParams); $sourceOperationsApi = new SourceOperationsApi(...$taskParams); $subscriptionsApi = new SubscriptionsApi(...$taskParams); + $projectsApi = new ProjectsApi(...$taskParams); $supportApi = new SupportApi(...$taskParams); $systemInformationApi = new SystemInformationApi(...$taskParams); $sshKeysApi = new SshKeysApi(...$taskParams); + $taskApi = new TaskApi(...$taskParams); $teamAccessApi = new TeamAccessApi(...$taskParams); $teamsApi = new TeamsApi(...$taskParams); $thirdPartyIntegrationsApi = new ThirdPartyIntegrationsApi(...$taskParams); @@ -274,7 +310,14 @@ public function __invoke(bool $force = false): string $organizationInvitationsApi, $projectInvitationsApi, ); - $this->metrics = new MetricsTask($this); + $this->metrics = new MetricsTask( + $this, + $httpTrafficApi, + $blackfireMonitoringApi, + $continuousProfilingApi, + $blackfireProfilingApi, + $entrypointApi, + ); $this->mounts = new MountsTask($this); $this->operations = new OperationsTask( $this, @@ -289,10 +332,14 @@ public function __invoke(bool $force = false): string $invoicesApi, $mfaApi, $ordersApi, - $profilesApi, + $userProfilesApi, $recordsApi, $vouchersApi, $addOnsApi, + $discountsApi, + $referencesApi, + $defaultApi, + $alertsApi, ); $this->projects = new ProjectsTask( $this, @@ -300,21 +347,28 @@ public function __invoke(bool $force = false): string $organizationProjectsApi, $projectSettingsApi, $subscriptionsApi, + $deploymentTargetApi, + $alertsApi, + $domainClaimApi, + $projectsApi, ); $this->regions = new RegionsTask( $this, - $regionsApi + $regionsApi, + $referencesApi, ); $this->repositories = new RepositoriesTask( $this, $repositoryApi, $systemInformationApi, + $diffApi, ); $this->resources = new ResourcesTask( $this, $deploymentApi, - $autoscalingApi + $autoscalingApi, + $resourcesApi, ); $this->routes = new RoutesTask( $this, @@ -327,10 +381,15 @@ public function __invoke(bool $force = false): string $this, $sourceOperationsApi ); + $this->taskContainers = new TaskContainersTask( + $this, + $taskApi, + ); $this->teams = new TeamsTask( $this, $teamsApi, $teamAccessApi, + $referencesApi, ); $this->supportTickets = new SupportTicketsTask( $this, @@ -351,6 +410,7 @@ public function __invoke(bool $force = false): string $grantsApi, $mfaApi, $phoneNumberApi, + $referencesApi, ); $this->variables = new VariablesTask( $this, diff --git a/src/UpsunConfig.php b/src/UpsunConfig.php index 2d22374a..d32348a1 100644 --- a/src/UpsunConfig.php +++ b/src/UpsunConfig.php @@ -14,6 +14,9 @@ */ final class UpsunConfig { + /** Local OAuth2 token service exposed inside an Upsun runtime container. */ + public const LOCAL_AUTH = 'http://localhost:8200'; + public function __construct( public readonly string $base_url = "https://api.upsun.com", public readonly string $auth_url = "https://auth.upsun.com", @@ -21,6 +24,7 @@ public function __construct( public readonly string $token_endpoint = "oauth2/token", public readonly string $refresh_endpoint = "oauth2/token", public readonly string $clientId = "sdk-php-client-id", + public readonly ?int $tokenTtl = null, ) { } } diff --git a/templates/php/ObjectSerializer.mustache b/templates/php/ObjectSerializer.mustache index 5d757f12..12877f41 100644 --- a/templates/php/ObjectSerializer.mustache +++ b/templates/php/ObjectSerializer.mustache @@ -354,6 +354,8 @@ class ObjectSerializer case 'mixed': return $data; + case 'object': + return (object)$data; case 'array': error_log("Warning: Deserializing generic 'array' type - should use specific model class"); return (array)$data; diff --git a/templates/php/README.mustache b/templates/php/README.mustache index c08bd1fd..f4ebdc89 100644 --- a/templates/php/README.mustache +++ b/templates/php/README.mustache @@ -15,6 +15,14 @@ Install the SDK via Composer: composer require {{composerPackageName}} ``` +> **Important**: +> This SDK relies on PSR interfaces and requires a **PSR-18 HTTP client** implementation at runtime. +> If your project does not already provide one, install a compatible client, for example: +> +> ```bash +> composer require symfony/http-client +> ``` + Then include Composer's autoloader in your PHP application: ```php @@ -23,9 +31,13 @@ require __DIR__ . '/vendor/autoload.php'; ## Authentication -You will need an [Upsun API token](https://docs.upsun.com/administration/cli/api-tokens.html) to use this SDK. +You will need an [Upsun API token](https://developer.upsun.com/cli/api-tokens) to use this SDK. Store it securely, preferably in an environment variable. +### With an API token (default) + +Use this outside of an Upsun runtime, authenticating with your API token. + ```php use Upsun\UpsunConfig; use Upsun\UpsunClient; @@ -34,6 +46,24 @@ $config = new UpsunConfig(apiToken: getenv('UPSUN_API_TOKEN')); $upsunClient = new UpsunClient($config); ``` +### With the local token service (inside an Upsun container) + +Inside an Upsun runtime container, a local token service is exposed on +`http://localhost:8200`. Point `auth_url` to `UpsunConfig::LOCAL_AUTH` and the SDK +authenticates with the `client_credentials` grant automatically (no API token +required). You may optionally request a token lifetime via `tokenTtl` (60-900 seconds). + +```php +use Upsun\UpsunConfig; +use Upsun\UpsunClient; + +$config = new UpsunConfig( + auth_url: UpsunConfig::LOCAL_AUTH, + tokenTtl: 900, // optional, 60-900 seconds +); +$upsunClient = new UpsunClient($config); +``` + ## Usage ### Example: List organizations @@ -69,7 +99,7 @@ composer install The SDK is built as follows: -* From the [JSON specs of our API](https://docs.upsun.com/api/openapispec-upsun.json) +* From the [JSON specs of our API](https://meta.upsun.com/openapi-spec) * Using [``@openapitools/openapi-generator-cli``](https://www.npmjs.com/package/%40openapitools/openapi-generator-cli) * Which generates: * PHP **Models** (in `src/Model/`) @@ -79,7 +109,7 @@ The SDK is built as follows: ### Regenerating API & Model classes API and Model classes are generated using [openapi-generator-cli](https://openapi-generator.tech) -from the [Upsun OpenAPI spec](https://docs.upsun.com/api/openapispec-upsun.json). +from the [Upsun OpenAPI spec](https://meta.upsun.com/openapi-spec). ```bash composer run spec:install diff --git a/templates/php/READMEDOC.mustache b/templates/php/READMEDOC.mustache index 2f6a7ec5..269e8d1e 100644 --- a/templates/php/READMEDOC.mustache +++ b/templates/php/READMEDOC.mustache @@ -8,7 +8,7 @@ All URIs are relative to *{{basePath}}* | Class | Method | HTTP request | Description | Upsun API Doc | | ------------ | ------------- | ------------- | ------------- | ------------- | -{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}| **{{classname}}** | [**{{operationId}}**]({{apiDocPath}}/{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{summary}} | https://docs.upsun.com/api/#tag/{{vendorExtensions.x-tag-id-kebab}}/operation/{{vendorExtensions.x-property-id-kebab}} | +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}| **{{classname}}** | [**{{operationId}}**]({{apiDocPath}}/{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{summary}} | https://developer.upsun.com/api/#tag/{{vendorExtensions.x-tag-id-kebab}}/operation/{{vendorExtensions.x-property-id-kebab}} | {{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} ## Models diff --git a/templates/php/abstract_api.mustache b/templates/php/abstract_api.mustache index a13695b1..0b8680c0 100644 --- a/templates/php/abstract_api.mustache +++ b/templates/php/abstract_api.mustache @@ -265,6 +265,63 @@ abstract class AbstractApi return ObjectSerializer::deserialize($content, $dataType, []); } + /** + * Stream newline-delimited JSON responses as model instances. + * + * @return \Generator + * @throws ApiException + */ + protected function handleNdjsonStreamWithDataType( + string $itemType, + RequestInterface $request, + ResponseInterface $response + ): \Generator { + $stream = $response->getBody(); + $buffer = ''; + + while (!$stream->eof()) { + $buffer .= $stream->read(8192); + + while (($lineBreakPos = strpos($buffer, "\n")) !== false) { + $line = trim(substr($buffer, 0, $lineBreakPos)); + $buffer = substr($buffer, $lineBreakPos + 1); + + if ($line === '') { + continue; + } + + try { + $decoded = json_decode($line, false, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new ApiException( + sprintf('Error JSON decoding NDJSON line (%s)', $request->getUri()), + $request, + $response, + $exception + ); + } + + yield ObjectSerializer::deserialize($decoded, $itemType, []); + } + } + + $tail = trim($buffer); + if ($tail !== '') { + try { + $decoded = json_decode($tail, false, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $exception) { + throw new ApiException( + sprintf('Error JSON decoding NDJSON tail (%s)', $request->getUri()), + $request, + $response, + $exception + ); + } + + yield ObjectSerializer::deserialize($decoded, $itemType, []); + } + } + /** * Deserialize generic types array * @throws Exception diff --git a/templates/php/api_doc.mustache b/templates/php/api_doc.mustache index ef459c9f..6f3697b5 100644 --- a/templates/php/api_doc.mustache +++ b/templates/php/api_doc.mustache @@ -6,7 +6,7 @@ All URIs are relative to {{basePath}}, except if the operation defines another b | Method | HTTP request | Description | Upsun API Doc | | ------------- | ------------- | ------------- | ------------- | -{{#operations}}{{#operation}}| [**{{operationId}}()**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{summary}} | https://docs.upsun.com/api/#tag/{{vendorExtensions.x-tag-id-kebab}}/operation/{{vendorExtensions.x-property-id-kebab}} | +{{#operations}}{{#operation}}| [**{{operationId}}()**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{summary}} | https://developer.upsun.com/api/#tag/{{vendorExtensions.x-tag-id-kebab}}/operation/{{vendorExtensions.x-property-id-kebab}} | {{/operation}}{{/operations}}{{#operations}}{{#operation}} ## `{{{operationId}}}()` diff --git a/templates/php/libraries/psr-18/ApiException.mustache b/templates/php/libraries/psr-18/ApiException.mustache index 0a4e65ff..107fa604 100644 --- a/templates/php/libraries/psr-18/ApiException.mustache +++ b/templates/php/libraries/psr-18/ApiException.mustache @@ -27,7 +27,7 @@ class ApiException extends RequestException /** * The deserialized response object */ - private mixed $responseObject; + private mixed $responseObject = null; public function __construct( $message, @@ -40,6 +40,17 @@ class ApiException extends RequestException $this->responseHeaders = $response->getHeaders(); $this->responseBody = (string) $response->getBody(); $this->code = $response->getStatusCode(); + } elseif ($previous instanceof self) { + // No response in this scope (the operation-level catch in the + // generated *Api.php re-wraps the original ApiException with a + // null response). Inherit the wrapped exception's context so the + // HTTP status, body, headers and deserialized error are preserved. + $this->responseHeaders = $previous->getResponseHeaders(); + $this->responseBody = $previous->getResponseBody(); + $this->responseObject = $previous->getResponseObject(); + if ($previous->getCode() !== 0) { + $this->code = $previous->getCode(); + } } } diff --git a/templates/php/libraries/psr-18/api.mustache b/templates/php/libraries/psr-18/api.mustache index dcc5169c..6fce5416 100644 --- a/templates/php/libraries/psr-18/api.mustache +++ b/templates/php/libraries/psr-18/api.mustache @@ -12,6 +12,7 @@ use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamFactoryInterface; use {{apiPackage}}\Serializer\ObjectSerializer; +use {{invokerPackage}}\FormDataProcessor; use {{invokerPackage}}\Core\TokenProvider; /** @@ -68,7 +69,12 @@ use {{invokerPackage}}\Core\TokenProvider; * @throws ClientExceptionInterface {{#vendorExtensions.x-return-types-displayReturn}} * +{{#vendorExtensions.x-ndjson-stream}} + * @return \Generator +{{/vendorExtensions.x-ndjson-stream}} +{{^vendorExtensions.x-ndjson-stream}} * @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}{{vendorExtensions.x-return-types-union}}{{/returnType}} +{{/vendorExtensions.x-ndjson-stream}} {{/vendorExtensions.x-return-types-displayReturn}} * @see {{docUrl}}/api/#tag/{{vendorExtensions.x-tag-id-kebab}}/operation/{{vendorExtensions.x-property-id-kebab}} {{#vendorExtensions.x-deprecated}} @@ -107,7 +113,12 @@ use {{invokerPackage}}\Core\TokenProvider; * @throws ClientExceptionInterface {{#vendorExtensions.x-return-types-displayReturn}} * +{{#vendorExtensions.x-ndjson-stream}} + * @return \Generator +{{/vendorExtensions.x-ndjson-stream}} +{{^vendorExtensions.x-ndjson-stream}} * @return {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}{{vendorExtensions.x-return-types-union}}{{/returnType}} +{{/vendorExtensions.x-ndjson-stream}} {{/vendorExtensions.x-return-types-displayReturn}} */ private function {{operationId}}WithHttpInfo({{#allParams}} @@ -126,11 +137,17 @@ use {{invokerPackage}}\Core\TokenProvider; $request->getBody() );{{#vendorExtensions.x-returnable}} +{{#vendorExtensions.x-ndjson-stream}} + return $this->handleNdjsonStreamWithDataType( + '{{vendorExtensions.x-stream-item-type}}', + $request, + $response + );{{/vendorExtensions.x-ndjson-stream}}{{^vendorExtensions.x-ndjson-stream}} return $this->handleResponseWithDataType( '{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}{{vendorExtensions.x-return-types-union}}{{/returnType}}', $request, $response - );{{/vendorExtensions.x-returnable}} + );{{/vendorExtensions.x-ndjson-stream}}{{/vendorExtensions.x-returnable}} } catch (Exception $exception) { throw new ApiException( sprintf( diff --git a/templates/php/oauth_provider.mustache b/templates/php/oauth_provider.mustache index ad52bebc..ba2d90a7 100644 --- a/templates/php/oauth_provider.mustache +++ b/templates/php/oauth_provider.mustache @@ -21,6 +21,18 @@ use Psr\Http\Message\RequestFactoryInterface; {{> partial_internal_header }} */ class OAuthProvider implements TokenProvider { + /** Custom grant exchanging a long-lived API token for an access token. */ + public const GRANT_API_TOKEN = 'api_token'; + + /** Grant used by the local token service inside an Upsun runtime container. */ + public const GRANT_CLIENT_CREDENTIALS = 'client_credentials'; + + /** Minimum access-token lifetime (seconds) accepted by the local token service. */ + public const TOKEN_TTL_MIN = 60; + + /** Maximum access-token lifetime (seconds) accepted by the local token service. */ + public const TOKEN_TTL_MAX = 900; + private ?string $accessToken = null; private ?string $refreshToken = null; private ?string $typeToken = null; @@ -45,29 +57,50 @@ class OAuthProvider implements TokenProvider private readonly string $clientId, private readonly string $clientSecret, ?string $refreshEndpoint = null, + private readonly string $grantType = self::GRANT_API_TOKEN, + private readonly ?int $tokenTtl = null, ) { $this->effectiveRefreshEndpoint = $refreshEndpoint ?? $this->tokenEndpoint; } /** - * Exchanges the API token for an access token using the custom api_token grant. - * Uses tokenEndpoint (not refreshEndpoint). + * Exchanges credentials for an access token. Uses tokenEndpoint (not refreshEndpoint). + * + * - api_token grant (default): exchanges the configured API token, authenticating + * the confidential client with HTTP Basic. + * - client_credentials grant: used by the local Upsun container token service, + * which requires neither an API token nor Basic auth. * * @throws Exception */ public function exchangeCodeForToken(): bool { try { - $body = http_build_query([ - 'grant_type' => 'api_token', - 'api_token' => $this->clientSecret, - 'client_id' => $this->clientId, - ]); - - $request = $this->requestFactory->createRequest('POST', $this->tokenEndpoint) - ->withHeader('Authorization', 'Basic ' . base64_encode('{{basicUser}}:')) - ->withHeader('Content-Type', 'application/x-www-form-urlencoded') - ->withBody(Stream::create($body)); + if ($this->grantType === self::GRANT_CLIENT_CREDENTIALS) { + $body = http_build_query([ + 'grant_type' => 'client_credentials', + ]); + + $request = $this->requestFactory->createRequest('POST', $this->tokenEndpoint) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody(Stream::create($body)); + + if ($this->tokenTtl !== null) { + $ttl = max(self::TOKEN_TTL_MIN, min(self::TOKEN_TTL_MAX, $this->tokenTtl)); + $request = $request->withHeader('x-token-ttl', (string)$ttl); + } + } else { + $body = http_build_query([ + 'grant_type' => 'api_token', + 'api_token' => $this->clientSecret, + 'client_id' => $this->clientId, + ]); + + $request = $this->requestFactory->createRequest('POST', $this->tokenEndpoint) + ->withHeader('Authorization', 'Basic ' . base64_encode('{{basicUser}}:')) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody(Stream::create($body)); + } {{#headersLoop}} foreach ($headers as $name => $value) { diff --git a/tests/Core/OAuthProviderTest.php b/tests/Core/OAuthProviderTest.php index b382dd9f..03567183 100644 --- a/tests/Core/OAuthProviderTest.php +++ b/tests/Core/OAuthProviderTest.php @@ -546,7 +546,7 @@ public function testForceRefreshPrefersRefreshTokenIfAvailable(): void }); $this->oauthProvider->getAuthorization(); // prime cache with refresh_token stored - $auth = $this->oauthProvider->forceRefresh(); // must use refresh_token grant + $this->oauthProvider->forceRefresh(); // must use refresh_token grant $authorization = $this->oauthProvider->getAuthorization(); $this->assertEquals('Bearer force-refresh-via-refresh-grant', $authorization); @@ -983,4 +983,115 @@ public function testExchangeCodeForTokenWithMissingExpiresIn() $this->assertEquals('Bearer refreshed-token', $authorization); } + + /** + * @throws Exception + */ + public function testExchangeCodeForTokenWithClientCredentialsGrant() + { + $provider = new OAuthProvider( + $this->httpClient, + $this->requestFactory, + 'http://localhost:8200/oauth2/token', + $this->clientId, + $this->clientSecret, + null, + OAuthProvider::GRANT_CLIENT_CREDENTIALS, + ); + + $responseBody = json_encode([ + 'access_token' => 'task-access-token', + 'expires_in' => 899, + 'token_type' => 'bearer', + ]); + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('http://localhost:8200/oauth2/token', (string)$request->getUri()); + $this->assertFalse($request->hasHeader('Authorization')); + + $body = (string)$request->getBody(); + $this->assertStringContainsString('grant_type=client_credentials', $body); + $this->assertStringNotContainsString('api_token', $body); + + return true; + })) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], $responseBody)); + + $this->assertTrue($provider->exchangeCodeForToken()); + $this->assertEquals('Bearer task-access-token', $provider->getAuthorization()); + } + + /** + * @throws Exception + */ + public function testClientCredentialsGrantSendsTokenTtlHeader() + { + $provider = new OAuthProvider( + $this->httpClient, + $this->requestFactory, + 'http://localhost:8200/oauth2/token', + $this->clientId, + $this->clientSecret, + null, + OAuthProvider::GRANT_CLIENT_CREDENTIALS, + 900, + ); + + $responseBody = json_encode([ + 'access_token' => 'task-access-token', + 'expires_in' => 900, + 'token_type' => 'bearer', + ]); + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->with($this->callback(function (RequestInterface $request) { + $this->assertEquals('900', $request->getHeaderLine('x-token-ttl')); + + return true; + })) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], $responseBody)); + + $this->assertTrue($provider->exchangeCodeForToken()); + } + + /** + * @throws Exception + */ + public function testClientCredentialsGrantClampsTokenTtlToMaximum() + { + $provider = new OAuthProvider( + $this->httpClient, + $this->requestFactory, + 'http://localhost:8200/oauth2/token', + $this->clientId, + $this->clientSecret, + null, + OAuthProvider::GRANT_CLIENT_CREDENTIALS, + 5000, + ); + + $responseBody = json_encode([ + 'access_token' => 'task-access-token', + 'expires_in' => 900, + 'token_type' => 'bearer', + ]); + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->with($this->callback(function (RequestInterface $request) { + $this->assertEquals((string)OAuthProvider::TOKEN_TTL_MAX, $request->getHeaderLine('x-token-ttl')); + + return true; + })) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], $responseBody)); + + $this->assertTrue($provider->exchangeCodeForToken()); + } } diff --git a/tests/Core/Tasks/CertificatesTaskTest.php b/tests/Core/Tasks/CertificatesTaskTest.php index 6c11a6c5..59ed9663 100644 --- a/tests/Core/Tasks/CertificatesTaskTest.php +++ b/tests/Core/Tasks/CertificatesTaskTest.php @@ -14,6 +14,8 @@ use Upsun\Core\TokenProvider; use Upsun\Model\AcceptedResponse; use Upsun\Model\Certificate; +use Upsun\Model\CertificateProvisioner; +use Upsun\Model\CertificateProvisionerPatch; use Upsun\UpsunClient; class CertificatesTaskTest extends BaseTestCase @@ -311,4 +313,125 @@ public function testUpdateCertificateError() $this->task->update(projectId: $projectId, certificateId: $certificateId, chain: $data); } + + /** + * @throws ClientExceptionInterface + */ + public function testAddSuccess(): void + { + $fakeResponse = [ + 'status' => 'accepted', + 'code' => 201, + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 201, + ['Content-Type' => 'application/json'], + json_encode($fakeResponse) + )); + + $result = $this->task->add( + projectId: 'proj_123', + certificate: 'cert-content', + key: 'key-content', + chain: ['chain1'], + isInvalid: false + ); + + $this->assertInstanceOf(AcceptedResponse::class, $result); + $this->assertObjectProperties($result, $fakeResponse); + } + + public function testAddWithMissingCertificate(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->task->add(projectId: 'proj_123', certificate: '', key: 'key-content'); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetProvisionerSuccess(): void + { + $expected = [ + 'id' => 'letsencrypt', + 'directoryUrl' => 'https://acme-v02.api.letsencrypt.org/directory', + 'email' => 'ops@example.com', + 'eabKid' => null, + 'eabHmacKey' => null, + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expected))); + + $result = $this->task->getProvisioner('proj_123', 'letsencrypt'); + + $this->assertInstanceOf(CertificateProvisioner::class, $result); + $this->assertObjectProperties($result, $expected); + } + + /** + * @throws ClientExceptionInterface + */ + public function testListProvisionersSuccess(): void + { + $expected = [[ + 'id' => 'letsencrypt', + 'directoryUrl' => 'https://acme-v02.api.letsencrypt.org/directory', + 'email' => 'ops@example.com', + 'eabKid' => null, + 'eabHmacKey' => null, + ]]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expected))); + + $result = $this->task->listProvisioners('proj_123'); + + $this->assertContainsOnlyInstancesOf(CertificateProvisioner::class, $result); + $this->assertObjectMatchesArray($result, $expected); + } + + /** + * @throws ClientExceptionInterface + */ + public function testUpdateProvisionerSuccess(): void + { + $patch = new CertificateProvisionerPatch(email: 'ops@example.com'); + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 202, + ['Content-Type' => 'application/json'], + json_encode(['status' => 'accepted', 'code' => 202]) + )); + + $result = $this->task->updateProvisioner('proj_123', 'letsencrypt', $patch); + + $this->assertEquals(new AcceptedResponse('accepted', 202), $result); + } + + public function testGetProvisionerWithEmptyDocumentId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->task->getProvisioner('proj_123', ''); + } + + public function testUpdateProvisionerWithEmptyDocumentId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->task->updateProvisioner('proj_123', ''); + } } diff --git a/tests/Core/Tasks/EnvironmentsTaskTest.php b/tests/Core/Tasks/EnvironmentsTaskTest.php index 3cce0fca..cd685f14 100644 --- a/tests/Core/Tasks/EnvironmentsTaskTest.php +++ b/tests/Core/Tasks/EnvironmentsTaskTest.php @@ -3057,6 +3057,44 @@ public function testRedeployUnauthorizedError(): void $this->environmentTask->redeploy(projectId: $projectId, environmentId: $environmentId); } + /** + * @throws ClientExceptionInterface + */ + public function testDeploySuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 202, + ['Content-Type' => 'application/json'], + json_encode(['status' => 'accepted', 'code' => 202]) + )); + + $result = $this->environmentTask->deploy(projectId: 'project-123', environmentId: 'env-456'); + + $this->assertEquals(new AcceptedResponse('accepted', 202), $result); + } + + /** + * @throws ClientExceptionInterface + */ + public function testMaintenanceRedeploySuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 202, + ['Content-Type' => 'application/json'], + json_encode(['status' => 'accepted', 'code' => 202]) + )); + + $result = $this->environmentTask->maintenanceRedeploy(projectId: 'project-123', environmentId: 'env-456'); + + $this->assertEquals(new AcceptedResponse('accepted', 202), $result); + } + /** * @throws ClientExceptionInterface */ diff --git a/tests/Core/Tasks/MetricsTaskTest.php b/tests/Core/Tasks/MetricsTaskTest.php index 3f0987e2..b037358b 100644 --- a/tests/Core/Tasks/MetricsTaskTest.php +++ b/tests/Core/Tasks/MetricsTaskTest.php @@ -2,21 +2,59 @@ namespace Upsun\Tests\Core\Tasks; +use Nyholm\Psr7\Factory\Psr17Factory; +use Nyholm\Psr7\Response; +use Psr\Http\Client\ClientExceptionInterface; +use Psr\Http\Client\ClientInterface; +use Upsun\Api\ApiConfiguration; +use Upsun\Api\ApiException; +use Upsun\Api\BlackfireMonitoringApi; +use Upsun\Api\BlackfireProfilingApi; +use Upsun\Api\ContinuousProfilingApi; +use Upsun\Api\EntrypointApi; +use Upsun\Api\HttpTrafficApi; +use Upsun\Core\OAuthProvider; use Upsun\Core\Tasks\MetricsTask; use Upsun\Core\Tasks\TaskBase; +use Upsun\Model\BlackfireProfileGraph200Response; +use Upsun\Model\BlackfireProfileProfile200Response; +use Upsun\Model\BlackfireProfilesList200Response; +use Upsun\Model\BlackfireProfilesRecommendations200Response; +use Upsun\Model\BlackfireProfileSubprofiles200Response; +use Upsun\Model\BlackfireProfileTimeline200Response; +use Upsun\Model\ListApplications200Response; +use Upsun\Model\ObservabilityEntrypoint200Response; use Upsun\UpsunClient; class MetricsTaskTest extends BaseTestCase { private MetricsTask $metricsTask; - private UpsunClient $upsunClient; + + /** + * @var ClientInterface&\PHPUnit\Framework\MockObject\MockObject + */ + private ClientInterface $httpClient; protected function setUp(): void { - $this->upsunClient = $this->createMock(UpsunClient::class); + $this->httpClient = $this->createMock(ClientInterface::class); + + $upsunClient = $this->createMock(UpsunClient::class); + + $apiClassParams = [ + $this->createMock(OAuthProvider::class), + $this->httpClient, + new Psr17Factory(), + new ApiConfiguration() + ]; $this->metricsTask = new class ( - $this->upsunClient + $upsunClient, + new HttpTrafficApi(...$apiClassParams), + new BlackfireMonitoringApi(...$apiClassParams), + new ContinuousProfilingApi(...$apiClassParams), + new BlackfireProfilingApi(...$apiClassParams), + new EntrypointApi(...$apiClassParams), ) extends MetricsTask { }; } @@ -31,18 +69,538 @@ public function testMetricsTaskExtendsTaskBase(): void $this->assertInstanceOf(TaskBase::class, $this->metricsTask); } - /** - * This test is a placeholder for future metrics functionality. - * Once metrics methods are implemented, additional tests should be added. - */ public function testMetricsTaskStructure(): void { - // Verify the task is properly constructed with UpsunClient $reflection = new \ReflectionClass($this->metricsTask); $constructor = $reflection->getConstructor(); $this->assertNotNull($constructor); - $this->assertCount(1, $constructor->getParameters()); + $this->assertCount(6, $constructor->getParameters()); $this->assertEquals('client', $constructor->getParameters()[0]->getName()); } + + public function testHttpMetricsTimelineIpsMethod(): void + { + $reflection = new \ReflectionClass($this->metricsTask); + $this->assertTrue($reflection->hasMethod('httpMetricsTimelineIps')); + $this->assertTrue($reflection->getMethod('httpMetricsTimelineIps')->isPublic()); + } + + public function testHttpMetricsTimelineUrlsMethod(): void + { + $reflection = new \ReflectionClass($this->metricsTask); + $this->assertTrue($reflection->hasMethod('httpMetricsTimelineUrls')); + $this->assertTrue($reflection->getMethod('httpMetricsTimelineUrls')->isPublic()); + } + + public function testHttpMetricsTimelineUserAgentsMethod(): void + { + $reflection = new \ReflectionClass($this->metricsTask); + $this->assertTrue($reflection->hasMethod('httpMetricsTimelineUserAgents')); + $this->assertTrue($reflection->getMethod('httpMetricsTimelineUserAgents')->isPublic()); + } + + public function testBlackfirePhpServerCachesMethod(): void + { + $reflection = new \ReflectionClass($this->metricsTask); + $this->assertTrue($reflection->hasMethod('blackfirePhpServerCaches')); + $this->assertTrue($reflection->getMethod('blackfirePhpServerCaches')->isPublic()); + } + + public function testBlackfireServerGlobalMethod(): void + { + $reflection = new \ReflectionClass($this->metricsTask); + $this->assertTrue($reflection->hasMethod('blackfireServerGlobal')); + $this->assertTrue($reflection->getMethod('blackfireServerGlobal')->isPublic()); + } + + public function testBlackfireServerTopSpansMethod(): void + { + $reflection = new \ReflectionClass($this->metricsTask); + $this->assertTrue($reflection->hasMethod('blackfireServerTopSpans')); + $this->assertTrue($reflection->getMethod('blackfireServerTopSpans')->isPublic()); + } + + public function testBlackfireServerTransactionsBreakdownMethod(): void + { + $reflection = new \ReflectionClass($this->metricsTask); + $this->assertTrue($reflection->hasMethod('blackfireServerTransactionsBreakdown')); + $this->assertTrue($reflection->getMethod('blackfireServerTransactionsBreakdown')->isPublic()); + } + + public function testListContinuousProfilingApplicationsMethod(): void + { + $reflection = new \ReflectionClass($this->metricsTask); + $this->assertTrue($reflection->hasMethod('listContinuousProfilingApplications')); + $this->assertTrue($reflection->getMethod('listContinuousProfilingApplications')->isPublic()); + } + + public function testGetContinuousProfilingApplicationFilterMethod(): void + { + $reflection = new \ReflectionClass($this->metricsTask); + $this->assertTrue($reflection->hasMethod('getContinuousProfilingApplicationFilter')); + $this->assertTrue($reflection->getMethod('getContinuousProfilingApplicationFilter')->isPublic()); + } + + public function testGetContinuousProfilingApplicationMergeMethod(): void + { + $reflection = new \ReflectionClass($this->metricsTask); + $this->assertTrue($reflection->hasMethod('getContinuousProfilingApplicationMerge')); + $this->assertTrue($reflection->getMethod('getContinuousProfilingApplicationMerge')->isPublic()); + } + + public function testGetContinuousProfilingApplicationTimelineMethod(): void + { + $reflection = new \ReflectionClass($this->metricsTask); + $this->assertTrue($reflection->hasMethod('getContinuousProfilingApplicationTimeline')); + $this->assertTrue($reflection->getMethod('getContinuousProfilingApplicationTimeline')->isPublic()); + } + + public function testHttpMetricsTimelineIpsWithInvalidProjectId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->metricsTask->httpMetricsTimelineIps('', 'env456', 1, 2); + } + + public function testBlackfireServerGlobalWithEmptyKeys(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->metricsTask->blackfireServerGlobal('project123', 'env456', 1, 2, []); + } + + public function testGetContinuousProfilingApplicationFilterWithEmptyApplicationName(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->metricsTask->getContinuousProfilingApplicationFilter('project123', 'env456', ''); + } + + /** + * @throws ClientExceptionInterface + */ + public function testBlackfireProfileGraphSuccess(): void + { + $payload = $this->createBlackfireGraphPayload(); + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($payload))); + + $result = $this->metricsTask->blackfireProfileGraph( + 'project123', + 'env456', + '123e4567-e89b-12d3-a456-426614174000' + ); + + $this->assertInstanceOf(BlackfireProfileGraph200Response::class, $result); + $this->assertObjectProperties($result, $payload); + } + + /** + * @throws ClientExceptionInterface + */ + public function testBlackfireProfileProfileSuccess(): void + { + $payload = [ + 'projectId' => 'project123', + 'environmentId' => 'env456', + 'branchMachineName' => 'main-abc123', + 'agent' => 'blackfire', + 'uuid' => '123e4567-e89b-12d3-a456-426614174000', + 'profile' => (object) ['wt' => 12.3] + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($payload))); + + $result = $this->metricsTask->blackfireProfileProfile( + 'project123', + 'env456', + '123e4567-e89b-12d3-a456-426614174000' + ); + + $this->assertInstanceOf(BlackfireProfileProfile200Response::class, $result); + $this->assertObjectProperties($result, $payload); + } + + /** + * @throws ClientExceptionInterface + */ + public function testBlackfireProfileSubprofilesSuccess(): void + { + $payload = [ + 'projectId' => 'project123', + 'environmentId' => 'env456', + 'branchMachineName' => 'main-abc123', + 'agent' => 'blackfire', + 'uuid' => '123e4567-e89b-12d3-a456-426614174000', + 'subprofiles' => (object) ['child' => (object) ['uuid' => 'uuid-child']] + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($payload))); + + $result = $this->metricsTask->blackfireProfileSubprofiles( + 'project123', + 'env456', + '123e4567-e89b-12d3-a456-426614174000' + ); + + $this->assertInstanceOf(BlackfireProfileSubprofiles200Response::class, $result); + $this->assertObjectProperties($result, $payload); + } + + /** + * @throws ClientExceptionInterface + */ + public function testBlackfireProfileTimelineSuccess(): void + { + $payload = [ + 'projectId' => 'project123', + 'environmentId' => 'env456', + 'branchMachineName' => 'main-abc123', + 'agent' => 'blackfire', + 'uuid' => '123e4567-e89b-12d3-a456-426614174000', + 'timeline' => (object) ['samples' => []], + 'dimension' => 'wt', + 'time' => 123, + 'language' => 'php' + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($payload))); + + $result = $this->metricsTask->blackfireProfileTimeline( + 'project123', + 'env456', + '123e4567-e89b-12d3-a456-426614174000' + ); + + $this->assertInstanceOf(BlackfireProfileTimeline200Response::class, $result); + $this->assertObjectProperties($result, $payload); + } + + /** + * @throws ClientExceptionInterface + */ + public function testBlackfireProfilesListSuccess(): void + { + $payload = [ + 'projectId' => 'project123', + 'environmentId' => 'env456', + 'branchMachineName' => 'main-abc123', + 'agent' => 'blackfire', + 'profiles' => [], + 'page' => 1, + 'pages' => 1, + 'total' => 0 + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($payload))); + + $result = $this->metricsTask->blackfireProfilesList('project123', 'env456'); + + $this->assertInstanceOf(BlackfireProfilesList200Response::class, $result); + $this->assertObjectProperties($result, $payload); + } + + /** + * @throws ClientExceptionInterface + */ + public function testBlackfireProfilesRecommendationsSuccess(): void + { + $payload = [ + 'projectId' => 'project123', + 'environmentId' => 'env456', + 'branchMachineName' => 'main-abc123', + 'agent' => 'blackfire', + 'from' => 100, + 'to' => 200, + 'recommendations' => [], + 'total' => 0, + 'transaction' => 'GET /', + 'testedTransactions' => [], + 'untestedTopTransactions' => [] + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($payload))); + + $result = $this->metricsTask->blackfireProfilesRecommendations('project123', 'env456', 100, 200); + + $this->assertInstanceOf(BlackfireProfilesRecommendations200Response::class, $result); + $this->assertObjectProperties($result, $payload); + } + + /** + * @throws ClientExceptionInterface + */ + public function testObservabilityEntrypointSuccess(): void + { + $payload = [ + 'message' => 'Observability entrypoint', + 'projectId' => 'project123', + 'branchMachineName' => 'main-abc123', + 'environmentId' => 'env456', + 'environmentType' => 'production', + 'vendor' => 'upsun', + 'pshUserIdentifier' => 'user-123', + 'links' => [ + 'self' => ['href' => 'https://api.example.test/observability'], + 'resourcesByService' => [ + ['name' => 'app', 'href' => 'https://api.example.test/observability/resources/app'] + ], + 'resourcesOverview' => ['href' => 'https://api.example.test/resources-overview'], + 'resourcesSummary' => ['href' => 'https://api.example.test/resources-summary'], + 'blackfirePhpServerCaches' => ['href' => 'https://api.example.test/php-caches'], + 'blackfireServerGlobal' => ['href' => 'https://api.example.test/server-global'], + 'blackfireServerTransactionsBreakdown' => ['href' => 'https://api.example.test/server-breakdown'], + 'logsQuery' => ['href' => 'https://api.example.test/logs-query'], + 'logsOverview' => ['href' => 'https://api.example.test/logs-overview'], + 'httpMetricsTimelineUrls' => ['href' => 'https://api.example.test/http-urls'], + 'httpMetricsTimelineIps' => ['href' => 'https://api.example.test/http-ips'], + 'httpMetricsTimelineUserAgents' => ['href' => 'https://api.example.test/http-uas'], + 'consoleSandboxAccess' => ['href' => 'https://api.example.test/console'], + 'conprofApplications' => ['href' => 'https://api.example.test/conprof-apps'], + 'conprofApplicationFilters' => ['href' => 'https://api.example.test/conprof-filters'], + 'conprofTimeline' => ['href' => 'https://api.example.test/conprof-timeline'], + 'conprofFlamegraph' => ['href' => 'https://api.example.test/conprof-flamegraph'] + ], + 'retention' => [ + 'resources' => 60, + 'logs' => 60, + 'httpTraffic' => 60, + 'continuousProfiling' => 60 + ], + 'dataRetention' => [ + 'unit' => 'minute', + 'unitInSeconds' => 60, + 'resources' => ['retentionPeriod' => 60, 'maxRange' => 60, 'recommendedDefaultRange' => 15], + 'serverMonitoring' => ['retentionPeriod' => 60, 'maxRange' => 60, 'recommendedDefaultRange' => 15], + 'logs' => ['retentionPeriod' => 60, 'maxRange' => 60, 'recommendedDefaultRange' => 15], + 'httpTraffic' => ['retentionPeriod' => 60, 'maxRange' => 60, 'recommendedDefaultRange' => 15], + 'continuousProfiling' => ['retentionPeriod' => 60, 'maxRange' => 60, 'recommendedDefaultRange' => 15] + ] + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($payload))); + + $result = $this->metricsTask->observabilityEntrypoint('project123', 'env456'); + + $this->assertInstanceOf(ObservabilityEntrypoint200Response::class, $result); + $this->assertObjectProperties($result, $payload); + } + + /** + * @throws ClientExceptionInterface + */ + public function testHttpMetricsTimelineIpsSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '{}')); + + $this->expectException(ApiException::class); + + $this->metricsTask->httpMetricsTimelineIps('project123', 'env456', 100, 200); + } + + /** + * @throws ClientExceptionInterface + */ + public function testHttpMetricsTimelineUrlsSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '{}')); + + $this->expectException(ApiException::class); + + $this->metricsTask->httpMetricsTimelineUrls('project123', 'env456', 100, 200); + } + + /** + * @throws ClientExceptionInterface + */ + public function testHttpMetricsTimelineUserAgentsSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '{}')); + + $this->expectException(ApiException::class); + + $this->metricsTask->httpMetricsTimelineUserAgents('project123', 'env456', 100, 200); + } + + /** + * @throws ClientExceptionInterface + */ + public function testBlackfirePhpServerCachesSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '{}')); + + $this->expectException(ApiException::class); + + $this->metricsTask->blackfirePhpServerCaches('project123', 'env456', 100, 200); + } + + /** + * @throws ClientExceptionInterface + */ + public function testBlackfireServerGlobalSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '{}')); + + $this->expectException(ApiException::class); + + $this->metricsTask->blackfireServerGlobal('project123', 'env456', 100, 200, ['cpu']); + } + + /** + * @throws ClientExceptionInterface + */ + public function testBlackfireServerTopSpansSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '{}')); + + $this->expectException(ApiException::class); + + $this->metricsTask->blackfireServerTopSpans('project123', 'env456', 100, 200); + } + + /** + * @throws ClientExceptionInterface + */ + public function testBlackfireServerTransactionsBreakdownSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '{}')); + + $this->expectException(ApiException::class); + + $this->metricsTask->blackfireServerTransactionsBreakdown('project123', 'env456', 100, 200); + } + + /** + * @throws ClientExceptionInterface + */ + public function testListContinuousProfilingApplicationsSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '{}')); + + $result = $this->metricsTask->listContinuousProfilingApplications('project123', 'env456'); + + $this->assertInstanceOf(ListApplications200Response::class, $result); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetContinuousProfilingApplicationFilterSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '{}')); + + $this->expectException(ApiException::class); + + $this->metricsTask->getContinuousProfilingApplicationFilter('project123', 'env456', 'app'); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetContinuousProfilingApplicationMergeSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/octet-stream'], 'binary-merge-data')); + + $this->expectException(ApiException::class); + + $this->metricsTask->getContinuousProfilingApplicationMerge('project123', 'env456', 'app'); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetContinuousProfilingApplicationTimelineSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '{}')); + + $this->expectException(ApiException::class); + + $this->metricsTask->getContinuousProfilingApplicationTimeline('project123', 'env456', 'app'); + } + + public function testGetContinuousProfilingApplicationMergeWithEmptyApplicationName(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->metricsTask->getContinuousProfilingApplicationMerge('project123', 'env456', ''); + } + + public function testGetContinuousProfilingApplicationTimelineWithEmptyApplicationName(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->metricsTask->getContinuousProfilingApplicationTimeline('project123', 'env456', ''); + } + + private function createBlackfireGraphPayload(): array + { + return [ + 'projectId' => 'project123', + 'environmentId' => 'env456', + 'branchMachineName' => 'main-abc123', + 'agent' => 'blackfire', + 'uuid' => '123e4567-e89b-12d3-a456-426614174000', + 'dimensions' => (object) ['wt' => 'Wall Time'], + 'root' => 'n1', + 'nodes' => (object) ['n1' => (object) ['label' => 'main']], + 'edges' => (object) [], + 'comparison' => false, + 'language' => 'php' + ]; + } } diff --git a/tests/Core/Tasks/OrganizationsTaskTest.php b/tests/Core/Tasks/OrganizationsTaskTest.php index adf4e905..34ddfd92 100644 --- a/tests/Core/Tasks/OrganizationsTaskTest.php +++ b/tests/Core/Tasks/OrganizationsTaskTest.php @@ -8,10 +8,15 @@ use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; use Upsun\Api\AddOnsApi; +use Upsun\Api\AlertsApi; use Upsun\Api\ApiConfiguration; use Upsun\Api\ApiException; use Upsun\Api\ApiTokensApi; use Upsun\Api\ConnectionsApi; +use Upsun\Api\DefaultApi; +use Upsun\Api\DeploymentTargetApi; +use Upsun\Api\DiscountsApi; +use Upsun\Api\DomainClaimApi; use Upsun\Api\GrantsApi; use Upsun\Api\InvoicesApi; use Upsun\Api\MfaApi; @@ -20,10 +25,11 @@ use Upsun\Api\OrganizationProjectsApi; use Upsun\Api\OrganizationsApi; use Upsun\Api\PhoneNumberApi; -use Upsun\Api\ProfilesApi; use Upsun\Api\ProjectApi; +use Upsun\Api\ProjectsApi; use Upsun\Api\ProjectSettingsApi; use Upsun\Api\RecordsApi; +use Upsun\Api\ReferencesApi; use Upsun\Api\SubscriptionsApi; use Upsun\Api\TeamAccessApi; use Upsun\Api\TeamsApi; @@ -37,28 +43,43 @@ use Upsun\Core\Tasks\UsersTask; use Upsun\Core\TokenProvider; use Upsun\Model\AcceptedResponse; -use Upsun\Model\Address; +use Upsun\Model\CanAffordSubscriptionRequest; use Upsun\Model\CanCreateNewOrgSubscription200Response; +use Upsun\Model\CanUpdateSubscription200Response; use Upsun\Model\CreateAuthorizationCredentials200Response; +use Upsun\Model\CreateOrgProjectRequest; +use Upsun\Model\Discount; use Upsun\Model\EstimationObject; +use Upsun\Model\GetAddress200Response; +use Upsun\Model\GetTypeAllowance200Response; +use Upsun\Model\GetUsageAlerts200Response; use Upsun\Model\Invoice; use Upsun\Model\ListOrgInvoices200Response; use Upsun\Model\ListOrgOrders200Response; use Upsun\Model\ListOrgPlanRecords200Response; +use Upsun\Model\ListOrgSubscriptions200Response; use Upsun\Model\ListOrgUsageRecords200Response; +use Upsun\Model\ListTeams200Response; use Upsun\Model\Order; use Upsun\Model\Organization; use Upsun\Model\OrganizationAddonsObject; +use Upsun\Model\OrganizationCarbon; use Upsun\Model\OrganizationMember; use Upsun\Model\OrganizationMFAEnforcement; use Upsun\Model\OrganizationProject; +use Upsun\Model\OrganizationReference; use Upsun\Model\PlanRecords; use Upsun\Model\Profile; use Upsun\Model\Project; +use Upsun\Model\ProjectCarbon; +use Upsun\Model\ProjectReference; +use Upsun\Model\ProvisionEvent; use Upsun\Model\SendOrgMfaReminders200ResponseValue; use Upsun\Model\Subscription; use Upsun\Model\SubscriptionCurrentUsageObject; use Upsun\Model\Team; +use Upsun\Model\UpdateOrgSubscriptionRequest; +use Upsun\Model\UpdateUsageAlertsRequest; use Upsun\Model\Vouchers; use Upsun\UpsunClient; @@ -100,7 +121,8 @@ public function __invoke(bool $force = false): string new ConnectionsApi(...$apiClassParams), new GrantsApi(...$apiClassParams), new MfaApi(...$apiClassParams), - new PhoneNumberApi(...$apiClassParams) + new PhoneNumberApi(...$apiClassParams), + new ReferencesApi(...$apiClassParams) ) extends UsersTask { }; $upsunClient->users = $usersTask; @@ -112,6 +134,10 @@ public function __invoke(bool $force = false): string new OrganizationProjectsApi(...$apiClassParams), new ProjectSettingsApi(...$apiClassParams), new SubscriptionsApi(...$apiClassParams), + new DeploymentTargetApi(...$apiClassParams), + new AlertsApi(...$apiClassParams), + new DomainClaimApi(...$apiClassParams), + new ProjectsApi(...$apiClassParams), ) extends ProjectsTask { }; @@ -121,7 +147,8 @@ public function __invoke(bool $force = false): string $teamsTask = new class ( $upsunClient, new TeamsApi(...$apiClassParams), - new TeamAccessApi(...$apiClassParams) + new TeamAccessApi(...$apiClassParams), + new ReferencesApi(...$apiClassParams) ) extends TeamsTask { }; @@ -137,10 +164,14 @@ public function __invoke(bool $force = false): string new InvoicesApi(...$apiClassParams), new MfaApi(...$apiClassParams), new OrdersApi(...$apiClassParams), - new ProfilesApi(...$apiClassParams), + new UserProfilesApi(...$apiClassParams), new RecordsApi(...$apiClassParams), new VouchersApi(...$apiClassParams), - new AddOnsApi(...$apiClassParams) + new AddOnsApi(...$apiClassParams), + new DiscountsApi(...$apiClassParams), + new ReferencesApi(...$apiClassParams), + new DefaultApi(...$apiClassParams), + new AlertsApi(...$apiClassParams) ) extends OrganizationsTask { }; } @@ -824,7 +855,7 @@ public function testUpdateOrganization() * @throws ClientExceptionInterface * @throws Exception */ - public function testCreateMember() + public function testAddMember() { $orgId = 'org_98765'; $userId = 'user_54321'; @@ -871,7 +902,7 @@ public function testCreateMember() json_encode($organizationMemberData) )); - $result = $this->organizationsTask->createMember( + $result = $this->organizationsTask->addMember( organizationId: $orgId, userId: $userId, permissions: $permissions @@ -2014,8 +2045,8 @@ public function testGetAddress(): void json_encode($data) )); - $result = $this->organizationsTask->getAddress(organizationId: 'org-123'); - $this->assertInstanceOf(Address::class, $result); + $result = $this->organizationsTask->getAddress(userId: 'org-123'); + $this->assertInstanceOf(GetAddress200Response::class, $result); $this->assertObjectProperties($result, $data); } @@ -2681,7 +2712,7 @@ public function testGetProfile(): void json_encode($data) )); - $result = $this->organizationsTask->getProfile(organizationId: 'org-123'); + $result = $this->organizationsTask->getProfile(userId: 'org-123'); $this->assertInstanceOf(Profile::class, $result); $this->assertObjectProperties($result, $data); } @@ -2710,7 +2741,7 @@ public function testUpdateAddress(): void )); $result = $this->organizationsTask->updateAddress( - organizationId: 'org-123', + userId: 'org-123', country: $fakeAddressData['country'], nameLine: $fakeAddressData['nameLine'], premise: $fakeAddressData['premise'], @@ -2722,7 +2753,7 @@ public function testUpdateAddress(): void dependentLocality: $fakeAddressData['dependentLocality'], postalCode: $fakeAddressData['postalCode'], ); - $this->assertInstanceOf(Address::class, $result); + $this->assertInstanceOf(GetAddress200Response::class, $result); $this->assertObjectProperties($result, $fakeAddressData); } @@ -2799,12 +2830,11 @@ public function testUpdateProfile(): void )); $result = $this->organizationsTask->updateProfile( - organizationId: 'org-123', + userId: 'org-123', defaultCatalog: $fakeUpdateOrgProfileRequestData['defaultCatalog'], projectOptionsUrl: $fakeUpdateOrgProfileRequestData['projectOptionsUrl'], companyName: $fakeUpdateOrgProfileRequestData['companyName'], vatNumber: $fakeUpdateOrgProfileRequestData['vatNumber'], - billingContact: $fakeUpdateOrgProfileRequestData['billingContact'], ); $this->assertInstanceOf(Profile::class, $result); $this->assertObjectProperties($result, $data); @@ -3118,4 +3148,509 @@ public function testDownloadInvoiceError(): void $this->organizationsTask->downloadInvoice(token: $token); } + + /** + * @throws ClientExceptionInterface + * @throws Exception + */ + public function testStreamOrgProjectProvisioningSuccess(): void + { + $payload = [ + 'type' => 'data', + 'projectId' => 'project123', + 'stage' => 'build', + 'label' => 'Building project', + 'reason' => null, + 'timestamp' => '2025-01-01T00:00:00+00:00', + 'steps' => [] + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn( + new Response( + 200, + ['Content-Type' => 'application/x-ndjson'], + json_encode($payload) . "\n" + ) + ); + + $result = $this->organizationsTask->streamOrgProjectProvisioning('org123'); + + $this->assertInstanceOf(\Generator::class, $result); + + $events = iterator_to_array($result, false); + $this->assertCount(1, $events); + $this->assertInstanceOf(ProvisionEvent::class, $events[0]); + $this->assertObjectProperties($events[0], $payload); + } + + public function testStreamOrgProjectProvisioningWithInvalidOrganizationId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->organizationsTask->streamOrgProjectProvisioning(''); + } + + /** + * @throws ClientExceptionInterface + * @throws Exception + */ + public function testStreamOrgProjectProvisioningYieldsMultipleEventsAndSkipsBlankLines(): void + { + $first = [ + 'type' => 'data', + 'projectId' => 'project123', + 'stage' => 'build', + 'label' => 'Building project', + 'reason' => null, + 'timestamp' => '2025-01-01T00:00:00+00:00', + 'steps' => [] + ]; + $second = [ + 'type' => 'done', + 'projectId' => 'project123', + 'stage' => 'finished', + 'label' => 'Provisioning complete', + 'reason' => null, + 'timestamp' => '2025-01-01T00:01:00+00:00', + 'steps' => [] + ]; + + // Blank lines between/around events must be skipped. + $body = "\n" . json_encode($first) . "\n\n" . json_encode($second) . "\n"; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/x-ndjson'], $body)); + + $events = iterator_to_array( + $this->organizationsTask->streamOrgProjectProvisioning('org123'), + false + ); + + $this->assertCount(2, $events); + $this->assertInstanceOf(ProvisionEvent::class, $events[0]); + $this->assertInstanceOf(ProvisionEvent::class, $events[1]); + $this->assertObjectProperties($events[0], $first); + $this->assertObjectProperties($events[1], $second); + } + + /** + * @throws ClientExceptionInterface + * @throws Exception + */ + public function testStreamOrgProjectProvisioningYieldsTailWithoutTrailingNewline(): void + { + $payload = [ + 'type' => 'data', + 'projectId' => 'project123', + 'stage' => 'build', + 'label' => 'Building project', + 'reason' => null, + 'timestamp' => '2025-01-01T00:00:00+00:00', + 'steps' => [] + ]; + + // No trailing newline: the last (and only) event lives in the buffer tail. + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/x-ndjson'], json_encode($payload))); + + $events = iterator_to_array( + $this->organizationsTask->streamOrgProjectProvisioning('org123'), + false + ); + + $this->assertCount(1, $events); + $this->assertInstanceOf(ProvisionEvent::class, $events[0]); + $this->assertObjectProperties($events[0], $payload); + } + + /** + * @throws ClientExceptionInterface + */ + public function testStreamOrgProjectProvisioningThrowsOnInvalidLine(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/x-ndjson'], "{invalid-json}\n")); + + $this->expectException(ApiException::class); + + iterator_to_array( + $this->organizationsTask->streamOrgProjectProvisioning('org123'), + false + ); + } + + /** + * @throws ClientExceptionInterface + */ + public function testStreamOrgProjectProvisioningThrowsOnInvalidTail(): void + { + // No trailing newline so the invalid JSON is parsed in the tail branch. + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/x-ndjson'], '{invalid-json}')); + + $this->expectException(ApiException::class); + + iterator_to_array( + $this->organizationsTask->streamOrgProjectProvisioning('org123'), + false + ); + } + + private function expectJsonRequest(string $body = '{}'): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], $body)); + } + + /** + * @throws ClientExceptionInterface + */ + public function testInfoWithUpdateParameters(): void + { + $data = ['id' => 'org123', 'name' => 'New Name', 'label' => 'Acme', 'country' => 'FR', 'status' => 'active']; + $this->expectJsonRequest(json_encode($data)); + + $result = $this->organizationsTask->info('org123', name: 'New Name'); + + $this->assertInstanceOf(Organization::class, $result); + $this->assertSame('New Name', $result->getName()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testInfoWithoutUpdateParameters(): void + { + $data = ['id' => 'org123', 'name' => 'Acme', 'status' => 'active']; + $this->expectJsonRequest(json_encode($data)); + + $result = $this->organizationsTask->info('org123'); + + $this->assertInstanceOf(Organization::class, $result); + $this->assertSame('Acme', $result->getName()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testListSubscriptions(): void + { + $this->expectJsonRequest(json_encode(['count' => 0, 'items' => []])); + + $result = $this->organizationsTask->listSubscriptions('org123'); + + $this->assertInstanceOf(ListOrgSubscriptions200Response::class, $result); + $this->assertSame(0, $result->getCount()); + $this->assertIsArray($result->getItems()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testCanUpdateSubscription(): void + { + $this->expectJsonRequest(json_encode(['canUpdate' => true, 'message' => ''])); + + $result = $this->organizationsTask->canUpdateSubscription('sub123'); + + $this->assertInstanceOf(CanUpdateSubscription200Response::class, $result); + $this->assertTrue($result->getCanUpdate()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testUpdateOrgSubscription(): void + { + $this->expectJsonRequest(json_encode(['id' => 'sub123', 'plan' => 'upsun/flexible', 'status' => 'active'])); + + $result = $this->organizationsTask->updateOrgSubscription('org123', 'sub123', new UpdateOrgSubscriptionRequest()); + + $this->assertInstanceOf(Subscription::class, $result); + $this->assertSame('upsun/flexible', $result->getPlan()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetSubscriptionUsageAlerts(): void + { + $this->expectJsonRequest(json_encode(['subscriptionId' => 'sub123'])); + + $result = $this->organizationsTask->getSubscriptionUsageAlerts('sub123'); + + $this->assertInstanceOf(GetUsageAlerts200Response::class, $result); + } + + /** + * @throws ClientExceptionInterface + */ + public function testUpdateSubscriptionUsageAlerts(): void + { + $this->expectJsonRequest(json_encode(['subscriptionId' => 'sub123'])); + + $result = $this->organizationsTask->updateSubscriptionUsageAlerts('sub123', new UpdateUsageAlertsRequest()); + + $this->assertInstanceOf(GetUsageAlerts200Response::class, $result); + } + + /** + * @throws ClientExceptionInterface + */ + public function testCreateMember(): void + { + $data = [ + 'id' => 'member123', + 'organizationId' => 'org123', + 'userId' => 'user123', + 'permissions' => ['admin'], + 'owner' => false, + ]; + $this->expectJsonRequest(json_encode($data)); + + $result = $this->organizationsTask->createMember('org123', 'user123', ['admin']); + + $this->assertInstanceOf(OrganizationMember::class, $result); + $this->assertSame('user123', $result->getUserId()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testListTeamsByMember(): void + { + $data = [ + 'count' => 1, + 'items' => [[ + 'id' => 'team123', + 'label' => 'Observability Team', + 'organization_id' => 'org123', + 'project_permissions' => ['admin'], + 'created_at' => '2023-10-05T13:30:43.073757Z', + 'updated_at' => '2023-11-15T09:22:18.451321Z', + ]], + ]; + $this->expectJsonRequest(json_encode($data)); + + $result = $this->organizationsTask->listTeamsByMember('user123'); + + $this->assertInstanceOf(ListTeams200Response::class, $result); + $this->assertContainsOnlyInstancesOf(Team::class, $result->getItems()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testCreateOrgProject(): void + { + $data = ['id' => 'project123', 'organization_id' => 'org123', 'title' => 'My Project', 'region' => 'eu-1.platform.sh']; + $this->expectJsonRequest(json_encode($data)); + + $result = $this->organizationsTask->createOrgProject('org123', new CreateOrgProjectRequest('eu-1.platform.sh')); + + $this->assertInstanceOf(OrganizationProject::class, $result); + $this->assertSame('project123', $result->getId()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testDeleteOrgProject(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(204, ['Content-Type' => 'application/json'])); + + $this->organizationsTask->deleteOrgProject('org123', 'project123'); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetOrgProject(): void + { + $data = ['id' => 'project123', 'organization_id' => 'org123', 'title' => 'My Project']; + $this->expectJsonRequest(json_encode($data)); + + $result = $this->organizationsTask->getOrgProject('org123', 'project123'); + + $this->assertInstanceOf(OrganizationProject::class, $result); + $this->assertSame('project123', $result->getId()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetCarbonEmissions(): void + { + $data = ['projectId' => 'project123', 'projectTitle' => 'My Project', 'values' => [], 'total' => 12.5]; + $this->expectJsonRequest(json_encode($data)); + + $result = $this->organizationsTask->getCarbonEmissions('org123', 'project123'); + + $this->assertInstanceOf(ProjectCarbon::class, $result); + $this->assertSame('project123', $result->getProjectId()); + $this->assertSame(12.5, $result->getTotal()); + } + + public function testGetCarbonEmissionsWithEmptyInterval(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->organizationsTask->getCarbonEmissions('org123', 'project123', interval: ' '); + } + + /** + * @throws ClientExceptionInterface + */ + public function testCanAffordSubscription(): void + { + $this->expectJsonRequest(); + + $this->organizationsTask->canAffordSubscription('sub123', new CanAffordSubscriptionRequest()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetDiscount(): void + { + $this->expectJsonRequest(json_encode(['id' => 42, 'organizationId' => 'org123', 'type' => 'allowance', 'status' => 'active'])); + + $result = $this->organizationsTask->getDiscount('discount123'); + + $this->assertInstanceOf(Discount::class, $result); + $this->assertSame('allowance', $result->getType()); + } + + public function testGetDiscountWithEmptyId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->organizationsTask->getDiscount(' '); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetTypeAllowance(): void + { + $this->expectJsonRequest(); + + $result = $this->organizationsTask->getTypeAllowance(); + + $this->assertInstanceOf(GetTypeAllowance200Response::class, $result); + } + + /** + * @throws ClientExceptionInterface + */ + public function testListReferencedOrgs(): void + { + $data = ['org1' => [ + 'id' => 'org1', + 'type' => 'enterprise', + 'name' => 'Acme', + 'label' => 'Acme Corp', + 'vendor' => 'upsun', + 'createdAt' => '2023-01-01T00:00:00+00:00', + 'updatedAt' => '2023-02-01T00:00:00+00:00', + ]]; + $this->expectJsonRequest(json_encode($data)); + + $result = $this->organizationsTask->listReferencedOrgs('id1,id2', 'signature'); + + $this->assertIsArray($result); + $this->assertContainsOnlyInstancesOf(OrganizationReference::class, $result); + $this->assertSame('org1', $result['org1']->getId()); + } + + public function testListReferencedOrgsWithEmptyIn(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->organizationsTask->listReferencedOrgs(' ', 'signature'); + } + + public function testListReferencedOrgsWithEmptySig(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->organizationsTask->listReferencedOrgs('id1', ' '); + } + + /** + * @throws ClientExceptionInterface + */ + public function testListReferencedProjects(): void + { + $data = ['proj1' => [ + 'id' => 'proj1', + 'organizationId' => 'org123', + 'subscriptionId' => 'sub123', + 'region' => 'eu-1.platform.sh', + 'title' => 'My Project', + 'type' => 'grid', + 'plan' => 'upsun/flexible', + 'status' => 'active', + 'createdAt' => '2023-01-01T00:00:00+00:00', + 'updatedAt' => '2023-02-01T00:00:00+00:00', + ]]; + $this->expectJsonRequest(json_encode($data)); + + $result = $this->organizationsTask->listReferencedProjects('id1,id2', 'signature'); + + $this->assertIsArray($result); + $this->assertContainsOnlyInstancesOf(ProjectReference::class, $result); + $this->assertSame('proj1', $result['proj1']->getId()); + } + + public function testListReferencedProjectsWithEmptyIn(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->organizationsTask->listReferencedProjects(' ', 'signature'); + } + + public function testListReferencedProjectsWithEmptySig(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->organizationsTask->listReferencedProjects('id1', ' '); + } + + /** + * @throws ClientExceptionInterface + */ + public function testQueryOrganiationCarbon(): void + { + $data = ['organizationId' => 'org123', 'projects' => [], 'total' => 99.9]; + $this->expectJsonRequest(json_encode($data)); + + $result = $this->organizationsTask->queryOrganiationCarbon('org123'); + + $this->assertInstanceOf(OrganizationCarbon::class, $result); + $this->assertSame('org123', $result->getOrganizationId()); + } + + public function testQueryOrganiationCarbonWithEmptyInterval(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->organizationsTask->queryOrganiationCarbon('org123', interval: ' '); + } } diff --git a/tests/Core/Tasks/ProjectsTaskTest.php b/tests/Core/Tasks/ProjectsTaskTest.php index 89295b2a..b4d0c723 100644 --- a/tests/Core/Tasks/ProjectsTaskTest.php +++ b/tests/Core/Tasks/ProjectsTaskTest.php @@ -8,22 +8,31 @@ use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; use Upsun\Api\AddOnsApi; +use Upsun\Api\AlertsApi; use Upsun\Api\ApiConfiguration; use Upsun\Api\ApiException; use Upsun\Api\ApiTokensApi; use Upsun\Api\AutoscalingApi; +use Upsun\Api\BlackfireMonitoringApi; +use Upsun\Api\BlackfireProfilingApi; use Upsun\Api\CertManagementApi; use Upsun\Api\ConnectionsApi; +use Upsun\Api\ContinuousProfilingApi; use Upsun\Api\DefaultApi; use Upsun\Api\DeploymentApi; use Upsun\Api\DeploymentTargetApi; +use Upsun\Api\DiffApi; +use Upsun\Api\DiscountsApi; +use Upsun\Api\DomainClaimApi; use Upsun\Api\DomainManagementApi; +use Upsun\Api\EntrypointApi; use Upsun\Api\EnvironmentActivityApi; use Upsun\Api\EnvironmentApi; use Upsun\Api\EnvironmentBackupsApi; use Upsun\Api\EnvironmentTypeApi; use Upsun\Api\EnvironmentVariablesApi; use Upsun\Api\GrantsApi; +use Upsun\Api\HttpTrafficApi; use Upsun\Api\InvoicesApi; use Upsun\Api\MfaApi; use Upsun\Api\OrdersApi; @@ -32,15 +41,17 @@ use Upsun\Api\OrganizationProjectsApi; use Upsun\Api\OrganizationsApi; use Upsun\Api\PhoneNumberApi; -use Upsun\Api\ProfilesApi; use Upsun\Api\ProjectActivityApi; use Upsun\Api\ProjectApi; use Upsun\Api\ProjectInvitationsApi; +use Upsun\Api\ProjectsApi; use Upsun\Api\ProjectSettingsApi; use Upsun\Api\ProjectVariablesApi; use Upsun\Api\RecordsApi; +use Upsun\Api\ReferencesApi; use Upsun\Api\RegionsApi; use Upsun\Api\RepositoryApi; +use Upsun\Api\ResourcesApi; use Upsun\Api\RoutingApi; use Upsun\Api\RuntimeOperationsApi; use Upsun\Api\SourceOperationsApi; @@ -81,8 +92,15 @@ use Upsun\Model\AcceptedResponse; use Upsun\Model\Activity; use Upsun\Model\Certificate; +use Upsun\Model\DeploymentTargetCreateInput; +use Upsun\Model\DeploymentTargetPatch; use Upsun\Model\Domain; +use Upsun\Model\DomainCreateInput; +use Upsun\Model\DomainPatch; use Upsun\Model\Environment; +use Upsun\Model\IntegrationCreateCreateInput; +use Upsun\Model\IntegrationPatch; +use Upsun\Model\ListOrgProjectHistory200Response; use Upsun\Model\ListProjectTeamAccess200Response; use Upsun\Model\ListProjectUserAccess200Response; use Upsun\Model\Project; @@ -93,6 +111,7 @@ use Upsun\Model\ProjectVariable; use Upsun\Model\Subscription; use Upsun\Model\TeamProjectAccess; +use Upsun\Model\UpdateUsageAlertsRequest; use Upsun\Model\UserProjectAccess; use Upsun\UpsunClient; @@ -130,6 +149,10 @@ public function __invoke(bool $force = false): string new OrganizationProjectsApi(...$apiClassParams), new ProjectSettingsApi(...$apiClassParams), new SubscriptionsApi(...$apiClassParams), + new DeploymentTargetApi(...$apiClassParams), + new AlertsApi(...$apiClassParams), + new DomainClaimApi(...$apiClassParams), + new ProjectsApi(...$apiClassParams), ) extends ProjectsTask { }; @@ -192,7 +215,12 @@ public function __invoke(bool $force = false): string }; $upsunClient->metrics = new class ( - $upsunClient + $upsunClient, + new HttpTrafficApi(...$apiClassParams), + new BlackfireMonitoringApi(...$apiClassParams), + new ContinuousProfilingApi(...$apiClassParams), + new BlackfireProfilingApi(...$apiClassParams), + new EntrypointApi(...$apiClassParams), ) extends MetricsTask { }; @@ -216,10 +244,14 @@ public function __invoke(bool $force = false): string new InvoicesApi(...$apiClassParams), new MfaApi(...$apiClassParams), new OrdersApi(...$apiClassParams), - new ProfilesApi(...$apiClassParams), + new UserProfilesApi(...$apiClassParams), new RecordsApi(...$apiClassParams), new VouchersApi(...$apiClassParams), - new AddOnsApi(...$apiClassParams) + new AddOnsApi(...$apiClassParams), + new DiscountsApi(...$apiClassParams), + new ReferencesApi(...$apiClassParams), + new DefaultApi(...$apiClassParams), + new AlertsApi(...$apiClassParams) ) extends OrganizationsTask { }; @@ -227,14 +259,16 @@ public function __invoke(bool $force = false): string $upsunClient->regions = new class ( $upsunClient, - new RegionsApi(...$apiClassParams) + new RegionsApi(...$apiClassParams), + new ReferencesApi(...$apiClassParams) ) extends RegionsTask { }; $upsunClient->repositories = new class ( $upsunClient, new RepositoryApi(...$apiClassParams), - new SystemInformationApi(...$apiClassParams) + new SystemInformationApi(...$apiClassParams), + new DiffApi(...$apiClassParams) ) extends RepositoriesTask { }; @@ -242,6 +276,7 @@ public function __invoke(bool $force = false): string $upsunClient, new DeploymentApi(...$apiClassParams), new AutoscalingApi(...$apiClassParams), + new ResourcesApi(...$apiClassParams), ) extends ResourcesTask { }; @@ -260,7 +295,8 @@ public function __invoke(bool $force = false): string $upsunClient->teams = new class ( $upsunClient, new TeamsApi(...$apiClassParams), - new TeamAccessApi(...$apiClassParams) + new TeamAccessApi(...$apiClassParams), + new ReferencesApi(...$apiClassParams), ) extends TeamsTask { }; @@ -280,7 +316,8 @@ public function __invoke(bool $force = false): string new ConnectionsApi(...$apiClassParams), new GrantsApi(...$apiClassParams), new MfaApi(...$apiClassParams), - new PhoneNumberApi(...$apiClassParams) + new PhoneNumberApi(...$apiClassParams), + new ReferencesApi(...$apiClassParams), ) extends UsersTask { }; @@ -444,6 +481,7 @@ public function testGetCapabilities() ], 'autoscaling' => [ 'enabled' => true, + 'supportsHorizontalScalingServices' => true, ], 'guaranteedResources' => [ 'enabled' => true, @@ -2536,9 +2574,6 @@ public function testUpdateSettingsWithError() ); } - /** - * @throws ClientExceptionInterface - */ /** * @throws ClientExceptionInterface */ @@ -2995,4 +3030,769 @@ public function testListEnvironmentsWithError() $this->expectException(ApiException::class); $this->projectsTask->listEnvironments(projectId: $projectId); } + + /** + * @throws ClientExceptionInterface + * @throws Exception + */ + public function testListOrgProjectHistorySuccess(): void + { + $payload = [ + 'count' => 1, + 'items' => [ + [ + 'id' => 'hist-1', + 'projectId' => 'project123', + 'eventType' => 'deploy', + 'eventId' => 'evt-1', + 'resource' => 'cpu_app', + 'environment' => 'main', + 'quantity' => '1', + 'timestamp' => '2025-01-01T00:00:00+00:00', + 'user' => 'user-1' + ] + ], + 'links' => [ + 'self' => ['href' => 'https://api.example.test/history'] + ] + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($payload))); + + $result = $this->projectsTask->listOrgProjectHistory('org123', 'project123'); + + $this->assertInstanceOf(ListOrgProjectHistory200Response::class, $result); + $this->assertObjectProperties($result, $payload); + } + + private function expectJsonRequest(string $body = '{}'): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], $body)); + } + + /** + * @throws ClientExceptionInterface + */ + public function testClearBuildCache(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->clearBuildCache('project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testListProjects(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->list('org123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetSubscription(): void + { + $this->httpClient + ->expects($this->exactly(2)) + ->method('sendRequest') + ->willReturnOnConsecutiveCalls( + new Response(200, ['Content-Type' => 'application/json'], json_encode($this->getFakeProject('project123'))), + new Response(200, ['Content-Type' => 'application/json'], '{}'), + ); + + try { + $this->projectsTask->getSubscription('project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetGitBlob(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getGitBlob('project123', 'blob123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetGitCommit(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getGitCommit('project123', 'commit123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetGitRef(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getGitRef('project123', 'ref123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetGitTree(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getGitTree('project123', 'tree123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testListGitRefs(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->listGitRefs('project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetGitInfo(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getGitInfo('project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testAddDomain(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->addDomain('project123', $this->createMock(DomainCreateInput::class)); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testUpdateDomain(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->updateDomain('project123', 'domain123', $this->createMock(DomainPatch::class)); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testAddCertificate(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->addCertificate('project123', 'cert-content', 'key-content'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetProjectTeamAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getProjectTeamAccess('project123', 'team123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetTeamProjectAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getTeamProjectAccess('team123', 'project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGrantProjectTeamAccessDelegates(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->grantProjectTeamAccess('project123', ['team_id' => 'team123']); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGrantTeamProjectAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->grantTeamProjectAccess('team123', ['project_id' => 'project123']); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testListProjectTeamAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->listProjectTeamAccess('project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testListTeamProjectAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->listTeamProjectAccess('team123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testRevokeTeamProjectAccessByProject(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->revokeTeamProjectAccessByProject('project123', 'team123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testRevokeProjectTeamAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->revokeProjectTeamAccess('project123', 'team123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testRevokeTeamProjectAccessByTeam(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->revokeTeamProjectAccessByTeam('team123', 'project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testRevokeTeamProjectAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->revokeTeamProjectAccess('team123', 'project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetProjectUserAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getProjectUserAccess('project123', 'user123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGrantProjectUserAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->grantProjectUserAccess('project123', ['user_id' => 'user123']); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testRevokeUserProjectAccessByProject(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->revokeUserProjectAccessByProject('project123', 'user123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testRevokeProjectUserAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->revokeProjectUserAccess('project123', 'user123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testUpdateProjectUserAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->updateProjectUserAccess('project123', 'user123', ['admin']); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testListProjectUserAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->listProjectUserAccess('project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testListUserProjectAccessByUser(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->listUserProjectAccessByUser('user123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testListUserProjectAccess(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->listUserProjectAccess('user123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testCreateIntegration(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->createIntegration('project123', $this->createMock(IntegrationCreateCreateInput::class)); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testDeleteIntegration(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->deleteIntegration('project123', 'integration123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetIntegration(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getIntegration('project123', 'integration123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testListIntegrations(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->listIntegrations('project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testUpdateIntegration(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->updateIntegration( + 'project123', + 'integration123', + $this->createMock(IntegrationPatch::class) + ); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testCreateProjectsDeployments(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->createProjectsDeployments( + 'project123', + $this->createMock(DeploymentTargetCreateInput::class) + ); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testDeleteProjectsDeployments(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->deleteProjectsDeployments('project123', 'target123'); + } catch (ApiException) { + } + } + + public function testDeleteProjectsDeploymentsWithEmptyConfigurationId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->projectsTask->deleteProjectsDeployments('project123', ' '); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetProjectsDeployments(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getProjectsDeployments('project123', 'target123'); + } catch (ApiException) { + } + } + + public function testGetProjectsDeploymentsWithEmptyConfigurationId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->projectsTask->getProjectsDeployments('project123', ' '); + } + + /** + * @throws ClientExceptionInterface + */ + public function testListProjectsDeployments(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->listProjectsDeployments('project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testUpdateProjectsDeployments(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->updateProjectsDeployments( + 'project123', + 'target123', + $this->createMock(DeploymentTargetPatch::class) + ); + } catch (ApiException) { + } + } + + public function testUpdateProjectsDeploymentsWithEmptyConfigurationId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->projectsTask->updateProjectsDeployments( + 'project123', + ' ', + $this->createMock(DeploymentTargetPatch::class) + ); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetUsageAlerts(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getUsageAlerts('sub123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testUpdateUsageAlerts(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->updateUsageAlerts('sub123', new UpdateUsageAlertsRequest()); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testCreateProjectsDomainClaims(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->createProjectsDomainClaims('project123', new \stdClass()); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testDeleteProjectsDomainClaims(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->deleteProjectsDomainClaims('project123', 'claim123'); + } catch (ApiException) { + } + } + + public function testDeleteProjectsDomainClaimsWithEmptyClaimId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->projectsTask->deleteProjectsDomainClaims('project123', ' '); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetProjectsDomainClaims(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->getProjectsDomainClaims('project123', 'claim123'); + } catch (ApiException) { + } + } + + public function testGetProjectsDomainClaimsWithEmptyClaimId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->projectsTask->getProjectsDomainClaims('project123', ' '); + } + + /** + * @throws ClientExceptionInterface + */ + public function testListProjectsDomainClaims(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->listProjectsDomainClaims('project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testUpdateProjectsDomainClaims(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->updateProjectsDomainClaims('project123', 'claim123', new \stdClass()); + } catch (ApiException) { + } + } + + public function testUpdateProjectsDomainClaimsWithEmptyClaimId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->projectsTask->updateProjectsDomainClaims('project123', ' ', new \stdClass()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testQueryProjectCarbon(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->queryProjectCarbon('org123', 'project123'); + } catch (ApiException) { + } + } + + public function testQueryProjectCarbonWithEmptyInterval(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->projectsTask->queryProjectCarbon('org123', 'project123', interval: ' '); + } + + /** + * @throws ClientExceptionInterface + */ + public function testMaintenanceRedeployProject(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->maintenanceRedeployProject('project123'); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testListOrgProjectHistory(): void + { + $this->expectJsonRequest(); + + try { + $this->projectsTask->listOrgProjectHistory('org123', 'project123'); + } catch (ApiException) { + } + } } diff --git a/tests/Core/Tasks/RegionsTaskTest.php b/tests/Core/Tasks/RegionsTaskTest.php index 1754fac8..03e9149c 100644 --- a/tests/Core/Tasks/RegionsTaskTest.php +++ b/tests/Core/Tasks/RegionsTaskTest.php @@ -9,6 +9,7 @@ use Psr\Http\Client\ClientInterface; use Upsun\Api\ApiConfiguration; use Upsun\Api\ApiException; +use Upsun\Api\ReferencesApi; use Upsun\Api\RegionsApi; use Upsun\Core\Tasks\RegionsTask; use Upsun\Core\TokenProvider; @@ -45,7 +46,8 @@ public function __invoke(bool $force = false): string $this->regionsTask = new class ( $upsunClient, - new RegionsApi(...$apiClassParams) + new RegionsApi(...$apiClassParams), + new ReferencesApi(...$apiClassParams) ) extends RegionsTask { }; } @@ -229,4 +231,33 @@ public function testListRegionsThrowsApiException(): void $this->expectException(ApiException::class); $this->regionsTask->list(); } + + public function testListReferencedRegionsWithEmptyIn(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->regionsTask->listReferencedRegions('', 'sig123'); + } + + public function testListReferencedRegionsWithEmptySig(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->regionsTask->listReferencedRegions('abc', ''); + } + + /** + * @throws ClientExceptionInterface + */ + public function testListReferencedRegionsSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '[]')); + + $result = $this->regionsTask->listReferencedRegions('abc', 'sig123'); + + $this->assertIsArray($result); + } } diff --git a/tests/Core/Tasks/RepositoriesTaskTest.php b/tests/Core/Tasks/RepositoriesTaskTest.php index aa500215..0d29a234 100644 --- a/tests/Core/Tasks/RepositoriesTaskTest.php +++ b/tests/Core/Tasks/RepositoriesTaskTest.php @@ -9,6 +9,7 @@ use Psr\Http\Client\ClientInterface; use Upsun\Api\ApiConfiguration; use Upsun\Api\ApiException; +use Upsun\Api\DiffApi; use Upsun\Api\RepositoryApi; use Upsun\Api\SystemInformationApi; use Upsun\Core\Tasks\RepositoriesTask; @@ -51,7 +52,8 @@ public function __invoke(bool $force = false): string $this->repositoriesTask = new class ( $upsunClient, new RepositoryApi(...$apiClassParams), - new SystemInformationApi(...$apiClassParams) + new SystemInformationApi(...$apiClassParams), + new DiffApi(...$apiClassParams) ) extends RepositoriesTask { }; } @@ -339,6 +341,40 @@ public function testListGitRefsWithInvalidProjectId(string $projectId): void $this->repositoriesTask->listGitRefs($projectId); } + /** + * @throws ClientExceptionInterface + */ + public function testListGitDiffs(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode([]) + )); + + $result = $this->repositoriesTask->listGitDiffs('test-project-id', 'main', 'develop'); + + $this->assertIsArray($result); + $this->assertSame([], $result); + } + + public function testListGitDiffsWithInvalidBaseId(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->repositoriesTask->listGitDiffs('test-project-id', '', 'develop'); + } + + public function testListGitDiffsWithInvalidTargetId(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->repositoriesTask->listGitDiffs('test-project-id', 'main', ''); + } + /** * @throws ClientExceptionInterface */ diff --git a/tests/Core/Tasks/ResourcesTaskTest.php b/tests/Core/Tasks/ResourcesTaskTest.php index 177a3b89..1d83245c 100644 --- a/tests/Core/Tasks/ResourcesTaskTest.php +++ b/tests/Core/Tasks/ResourcesTaskTest.php @@ -10,9 +10,13 @@ use Upsun\Api\ApiException; use Upsun\Api\AutoscalingApi; use Upsun\Api\DeploymentApi; +use Upsun\Api\ResourcesApi; use Upsun\Core\Tasks\ResourcesTask; use Upsun\Core\TokenProvider; use Upsun\Model\AcceptedResponse; +use Upsun\Model\ResourcesByService200Response; +use Upsun\Model\ResourcesOverview200Response; +use Upsun\Model\ResourcesSummary200Response; use Upsun\UpsunClient; class ResourcesTaskTest extends BaseTestCase @@ -47,6 +51,7 @@ public function __invoke(bool $force = false): string $upsunClient, new DeploymentApi(...$apiClassParams), new AutoscalingApi(...$apiClassParams), + new ResourcesApi(...$apiClassParams), ) extends ResourcesTask { }; } @@ -54,7 +59,7 @@ public function __invoke(bool $force = false): string /** * @throws ClientExceptionInterface */ - public function testUpdateSuccess(): void + public function testSetSuccess(): void { $projectId = 'project123'; $environmentId = 'env456'; @@ -97,7 +102,7 @@ public function testUpdateSuccess(): void ]) )); - $response = $this->resourcesTask->update( + $response = $this->resourcesTask->set( projectId: $projectId, environmentId: $environmentId, webapps: $webapps, @@ -110,7 +115,7 @@ public function testUpdateSuccess(): void /** * @throws ClientExceptionInterface */ - public function testUpdateWithDefaultParameters(): void + public function testSetWithDefaultParameters(): void { $projectId = 'project123'; $environmentId = 'env456'; @@ -126,7 +131,7 @@ public function testUpdateWithDefaultParameters(): void ]) )); - $response = $this->resourcesTask->update( + $response = $this->resourcesTask->set( projectId: $projectId, environmentId: $environmentId ); @@ -136,7 +141,7 @@ public function testUpdateWithDefaultParameters(): void /** * @throws ClientExceptionInterface */ - public function testUpdateWithOnlyWebapps(): void + public function testSetWithOnlyWebapps(): void { $projectId = 'project123'; $environmentId = 'env456'; @@ -161,7 +166,7 @@ public function testUpdateWithOnlyWebapps(): void ]) )); - $response = $this->resourcesTask->update( + $response = $this->resourcesTask->set( projectId: $projectId, environmentId: $environmentId, webapps: $webapps @@ -198,7 +203,7 @@ public function testUpdateWithPartialResources(): void ]) )); - $response = $this->resourcesTask->update( + $response = $this->resourcesTask->set( projectId: $projectId, environmentId: $environmentId, webapps: $webapps, @@ -210,7 +215,7 @@ public function testUpdateWithPartialResources(): void /** * @throws ClientExceptionInterface */ - public function testUpdateError(): void + public function testSetError(): void { $projectId = 'project123'; $environmentId = 'env456'; @@ -235,13 +240,13 @@ public function testUpdateError(): void )); $this->expectException(ApiException::class); - $this->resourcesTask->update(projectId: $projectId, environmentId: $environmentId, webapps: $webapps); + $this->resourcesTask->set(projectId: $projectId, environmentId: $environmentId, webapps: $webapps); } /** * @throws ClientExceptionInterface */ - public function testUpdateNotFound(): void + public function testSetNotFound(): void { $projectId = 'invalidProject'; $environmentId = 'env456'; @@ -259,13 +264,13 @@ public function testUpdateNotFound(): void )); $this->expectException(ApiException::class); - $this->resourcesTask->update(projectId: $projectId, environmentId: $environmentId); + $this->resourcesTask->set(projectId: $projectId, environmentId: $environmentId); } /** * @throws ClientExceptionInterface */ - public function testUpdateInvalidResources(): void + public function testSetInvalidResources(): void { $projectId = 'project123'; $environmentId = 'env456'; @@ -290,7 +295,7 @@ public function testUpdateInvalidResources(): void )); $this->expectException(ApiException::class); - $this->resourcesTask->update( + $this->resourcesTask->set( projectId: $projectId, environmentId: $environmentId, webapps: $webapps @@ -300,7 +305,7 @@ public function testUpdateInvalidResources(): void /** * @throws ClientExceptionInterface */ - public function testUpdateInsufficientResources(): void + public function testSetInsufficientResources(): void { $projectId = 'project123'; $environmentId = 'env456'; @@ -326,10 +331,185 @@ public function testUpdateInsufficientResources(): void )); $this->expectException(ApiException::class); - $this->resourcesTask->update( + $this->resourcesTask->set( projectId: $projectId, environmentId: $environmentId, webapps: $webapps ); } + + /** + * @throws ClientExceptionInterface + */ + public function testPatchAutoscalerSettingsSuccess(): void + { + $expectedData = [ + 'services' => [ + 'db' => [ + 'mysql' => [ + 'instances' => ['min' => 1, 'max' => 3] + ] + ] + ] + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode($expectedData) + )); + + $result = $this->resourcesTask->patchAutoscalerSettings( + projectId: 'project123', + environmentId: 'env456' + ); + + $this->assertObjectProperties($result, $expectedData); + } + + /** + * Regression: calling updateAutoscalerSettings() without $services must not + * raise a TypeError from unpacking a null value. + * + * @throws ClientExceptionInterface + */ + public function testUpdateAutoscalerSettingsWithoutServices(): void + { + $capturedBody = null; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturnCallback(function ($request) use (&$capturedBody) { + $capturedBody = (string)$request->getBody(); + return new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode(['services' => null]) + ); + }); + + $result = $this->resourcesTask->updateAutoscalerSettings( + projectId: 'project123', + environmentId: 'env456' + ); + + $this->assertNull($result->getServices()); + $this->assertStringContainsString('"services":[]', $capturedBody); + } + + /** + * @throws ClientExceptionInterface + */ + public function testUpdateAutoscalerSettingsWithServices(): void + { + $services = [ + 'services' => [ + 'db' => [ + 'mysql' => [ + 'instances' => ['min' => 1, 'max' => 3] + ] + ] + ] + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode($services) + )); + + $result = $this->resourcesTask->updateAutoscalerSettings( + projectId: 'project123', + environmentId: 'env456', + services: $services + ); + + $this->assertObjectProperties($result, $services); + } + + /** + * @throws ClientExceptionInterface + */ + public function testResourcesByServiceSuccess(): void + { + $payload = [ + 'grain' => 60, + 'from' => 100, + 'to' => 200, + 'projectId' => 'project123', + 'environmentId' => 'env456', + 'branchMachineName' => 'main-abc123', + 'service' => 'app', + 'data' => [], + 'dg2HostTypesMapping' => [] + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($payload))); + + $result = $this->resourcesTask->resourcesByService('project123', 'env456', 'app', 100, 200); + + $this->assertInstanceOf(ResourcesByService200Response::class, $result); + $this->assertObjectProperties($result, $payload); + } + + /** + * @throws ClientExceptionInterface + */ + public function testResourcesOverviewSuccess(): void + { + $payload = [ + 'grain' => 60, + 'from' => 100, + 'to' => 200, + 'projectId' => 'project123', + 'environmentId' => 'env456', + 'branchMachineName' => 'main-abc123', + 'data' => [] + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($payload))); + + $result = $this->resourcesTask->resourcesOverview('project123', 'env456', 100, 200); + + $this->assertInstanceOf(ResourcesOverview200Response::class, $result); + $this->assertObjectProperties($result, $payload); + } + + /** + * @throws ClientExceptionInterface + */ + public function testResourcesSummarySuccess(): void + { + $payload = [ + 'from' => 100, + 'to' => 200, + 'projectId' => 'project123', + 'environmentId' => 'env456', + 'branchMachineName' => 'main-abc123', + 'data' => ['services' => []] + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($payload))); + + $result = $this->resourcesTask->resourcesSummary('project123', 'env456', 100, 200); + + $this->assertInstanceOf(ResourcesSummary200Response::class, $result); + $this->assertObjectProperties($result, $payload); + } } diff --git a/tests/Core/Tasks/SupportTicketsTaskTest.php b/tests/Core/Tasks/SupportTicketsTaskTest.php index a44cef3b..283e3c68 100644 --- a/tests/Core/Tasks/SupportTicketsTaskTest.php +++ b/tests/Core/Tasks/SupportTicketsTaskTest.php @@ -8,11 +8,14 @@ use Nyholm\Psr7\Response; use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; +use Upsun\Api\AlertsApi; use Upsun\Api\ApiConfiguration; use Upsun\Api\DefaultApi; use Upsun\Api\DeploymentTargetApi; +use Upsun\Api\DomainClaimApi; use Upsun\Api\OrganizationProjectsApi; use Upsun\Api\ProjectApi; +use Upsun\Api\ProjectsApi; use Upsun\Api\ProjectSettingsApi; use Upsun\Api\RepositoryApi; use Upsun\Api\SubscriptionsApi; @@ -62,6 +65,10 @@ public function __invoke(bool $force = false): string new OrganizationProjectsApi(...$apiClassParams), new ProjectSettingsApi(...$apiClassParams), new SubscriptionsApi(...$apiClassParams), + new DeploymentTargetApi(...$apiClassParams), + new AlertsApi(...$apiClassParams), + new DomainClaimApi(...$apiClassParams), + new ProjectsApi(...$apiClassParams), ) extends ProjectsTask { }; @@ -580,4 +587,58 @@ public function testListPriorities(): void $this->assertContainsOnlyInstancesOf(ListTicketPriorities200ResponseInner::class, $result); $this->assertObjectMatchesArray($result, $ticketPriorities); } + + /** + * Regression: listCategories() without an organizationId must not raise a + * TypeError from validating a null organization ID. + * + * @throws ClientExceptionInterface + * @throws Exception + */ + public function testListCategoriesWithoutOrganizationId(): void + { + $ticketCategories = [ + ['id' => 'bug', 'label' => 'Bug Report'], + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode($ticketCategories) + )); + + $result = $this->task->listCategories(); + $this->assertContainsOnlyInstancesOf(ListTicketCategories200ResponseInner::class, $result); + $this->assertObjectMatchesArray($result, $ticketCategories); + } + + /** + * Regression: listPriorities() without a projectId must not call parse_url() + * on a null license URI. + * + * @throws ClientExceptionInterface + * @throws Exception + */ + public function testListPrioritiesWithoutProjectId(): void + { + $ticketPriorities = [ + ['id' => 'low', 'label' => 'Low'], + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode($ticketPriorities) + )); + + $result = $this->task->listPriorities(); + $this->assertContainsOnlyInstancesOf(ListTicketPriorities200ResponseInner::class, $result); + $this->assertObjectMatchesArray($result, $ticketPriorities); + } } diff --git a/tests/Core/Tasks/SystemOperationsTaskTest.php b/tests/Core/Tasks/SystemOperationsTaskTest.php new file mode 100644 index 00000000..260a5515 --- /dev/null +++ b/tests/Core/Tasks/SystemOperationsTaskTest.php @@ -0,0 +1,93 @@ +httpClient = $this->createMock(ClientInterface::class); + + $upsunClient = $this->createMock(UpsunClient::class); + + $apiClassParams = [ + $this->createMock(OAuthProvider::class), + $this->httpClient, + new Psr17Factory(), + new ApiConfiguration() + ]; + + $this->systemOperationsTask = new class ( + $upsunClient, + new SystemInformationApi(...$apiClassParams) + ) extends SystemOperationsTask { + }; + } + + public function testSystemOperationsTaskCanBeInstantiated(): void + { + $this->assertInstanceOf(SystemOperationsTask::class, $this->systemOperationsTask); + } + + public function testSystemOperationsTaskExtendsTaskBase(): void + { + $this->assertInstanceOf(TaskBase::class, $this->systemOperationsTask); + } + + public function testRestartGitServerMethodExists(): void + { + $reflection = new \ReflectionClass($this->systemOperationsTask); + $this->assertTrue($reflection->hasMethod('restartGitServer')); + + $method = $reflection->getMethod('restartGitServer'); + $this->assertTrue($method->isPublic()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testRestartGitServerSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 202, + ['Content-Type' => 'application/json'], + json_encode([ + 'status' => 'accepted', + 'code' => 202, + ]) + )); + + $result = $this->systemOperationsTask->restartGitServer(projectId: 'project123'); + + $this->assertEquals(new AcceptedResponse('accepted', 202), $result); + } + + public function testRestartGitServerWithInvalidProjectId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->systemOperationsTask->restartGitServer(projectId: ''); + } +} diff --git a/tests/Core/Tasks/TaskContainersTaskTest.php b/tests/Core/Tasks/TaskContainersTaskTest.php new file mode 100644 index 00000000..f5aa1ae9 --- /dev/null +++ b/tests/Core/Tasks/TaskContainersTaskTest.php @@ -0,0 +1,291 @@ +httpClient = $this->createMock(ClientInterface::class); + + $upsunClient = $this->createMock(UpsunClient::class); + + $apiClassParams = [ + $this->createMock(OAuthProvider::class), + $this->httpClient, + new Psr17Factory(), + new ApiConfiguration() + ]; + + $this->taskContainersTask = new class ( + $upsunClient, + new TaskApi(...$apiClassParams) + ) extends TaskContainersTask { + }; + } + + public function testTaskContainersTaskCanBeInstantiated(): void + { + $this->assertInstanceOf(TaskContainersTask::class, $this->taskContainersTask); + } + + public function testTaskContainersTaskExtendsTaskBase(): void + { + $this->assertInstanceOf(TaskBase::class, $this->taskContainersTask); + } + + public function testGetMethodExists(): void + { + $reflection = new \ReflectionClass($this->taskContainersTask); + $this->assertTrue($reflection->hasMethod('get')); + + $method = $reflection->getMethod('get'); + $this->assertTrue($method->isPublic()); + } + + public function testListMethodExists(): void + { + $reflection = new \ReflectionClass($this->taskContainersTask); + $this->assertTrue($reflection->hasMethod('list')); + + $method = $reflection->getMethod('list'); + $this->assertTrue($method->isPublic()); + } + + public function testRunMethodExists(): void + { + $reflection = new \ReflectionClass($this->taskContainersTask); + $this->assertTrue($reflection->hasMethod('run')); + + $method = $reflection->getMethod('run'); + $this->assertTrue($method->isPublic()); + } + + /** + * @throws ClientExceptionInterface + */ + public function testRunSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 202, + ['Content-Type' => 'application/json'], + json_encode([ + 'status' => 'accepted', + 'code' => 202, + ]) + )); + + $result = $this->taskContainersTask->run( + projectId: 'project123', + environmentId: 'env456', + taskId: 'task789' + ); + + $this->assertEquals(new AcceptedResponse('accepted', 202), $result); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '{}')); + + try { + $this->taskContainersTask->get( + projectId: 'project123', + environmentId: 'env456', + taskId: 'task789' + ); + } catch (ApiException) { + } + } + + /** + * @throws ClientExceptionInterface + */ + public function testListSuccess(): void + { + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], '[]')); + + $result = $this->taskContainersTask->list( + projectId: 'project123', + environmentId: 'env456' + ); + + $this->assertIsArray($result); + } + + public function testGetMethodWithInvalidProjectId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->taskContainersTask->get( + projectId: '', + environmentId: 'env456', + taskId: 'task789' + ); + } + + public function testGetMethodWithInvalidTaskId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->taskContainersTask->get( + projectId: 'project123', + environmentId: 'env456', + taskId: '' + ); + } + + public function testRunMethodWithInvalidTaskId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->taskContainersTask->run( + projectId: 'project123', + environmentId: 'env456', + taskId: '' + ); + } + + public function testRunMethodWithInvalidProjectId(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->taskContainersTask->run( + projectId: '', + environmentId: 'env456', + taskId: 'task789' + ); + } + + /** + * A flat NAME => value map must be wrapped under the `env` group. + * + * @throws ClientExceptionInterface + */ + public function testRunWrapsFlatVariablesUnderEnvGroup(): void + { + $capturedBody = null; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturnCallback(function ($request) use (&$capturedBody) { + $capturedBody = json_decode((string)$request->getBody(), true); + return new Response( + 202, + ['Content-Type' => 'application/json'], + json_encode(['status' => 'accepted', 'code' => 202]) + ); + }); + + $this->taskContainersTask->run( + projectId: 'project123', + environmentId: 'env456', + taskId: 'task789', + variables: ['FOO' => 'bar', 'BAZ' => 'qux'] + ); + + $this->assertSame( + ['env' => ['FOO' => 'bar', 'BAZ' => 'qux']], + $capturedBody['variables'] + ); + } + + /** + * Already-grouped variables must be passed through untouched. + * + * @throws ClientExceptionInterface + */ + public function testRunKeepsAlreadyGroupedVariables(): void + { + $capturedBody = null; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturnCallback(function ($request) use (&$capturedBody) { + $capturedBody = json_decode((string)$request->getBody(), true); + return new Response( + 202, + ['Content-Type' => 'application/json'], + json_encode(['status' => 'accepted', 'code' => 202]) + ); + }); + + $grouped = ['env' => ['FOO' => 'bar'], 'build' => ['BAZ' => 'qux']]; + + $this->taskContainersTask->run( + projectId: 'project123', + environmentId: 'env456', + taskId: 'task789', + variables: $grouped + ); + + $this->assertSame($grouped, $capturedBody['variables']); + } + + /** + * A map with at least one non-array value is treated as flat and wrapped. + * + * @throws ClientExceptionInterface + */ + public function testRunWrapsMixedVariablesUnderEnvGroup(): void + { + $capturedBody = null; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturnCallback(function ($request) use (&$capturedBody) { + $capturedBody = json_decode((string)$request->getBody(), true); + return new Response( + 202, + ['Content-Type' => 'application/json'], + json_encode(['status' => 'accepted', 'code' => 202]) + ); + }); + + $mixed = ['env' => ['FOO' => 'bar'], 'FLAT' => 'value']; + + $this->taskContainersTask->run( + projectId: 'project123', + environmentId: 'env456', + taskId: 'task789', + variables: $mixed + ); + + $this->assertSame(['env' => $mixed], $capturedBody['variables']); + } +} diff --git a/tests/Core/Tasks/TeamsTaskTest.php b/tests/Core/Tasks/TeamsTaskTest.php index de5540ba..7e9ffdf0 100644 --- a/tests/Core/Tasks/TeamsTaskTest.php +++ b/tests/Core/Tasks/TeamsTaskTest.php @@ -10,6 +10,7 @@ use Psr\Http\Client\ClientInterface; use Upsun\Api\ApiConfiguration; use Upsun\Api\ApiException; +use Upsun\Api\ReferencesApi; use Upsun\Api\TeamAccessApi; use Upsun\Api\TeamsApi; use Upsun\Core\Tasks\TeamsTask; @@ -52,6 +53,7 @@ public function __invoke(bool $force = false): string $upsunClient, new TeamsApi(...$apiClassParams), new TeamAccessApi(...$apiClassParams), + new ReferencesApi(...$apiClassParams), ) extends TeamsTask { }; } @@ -199,6 +201,56 @@ public function testDeleteMemberError(): void $this->task->deleteMember(teamId: $teamId, userId: $userId); } + /** + * @throws ClientExceptionInterface + */ + public function testListReferencedTeams(): void + { + $expected = [ + 'team-1' => [ + 'id' => 'team-1', + 'organizationId' => 'org-1', + 'label' => 'Team One', + 'projectPermissions' => [], + 'counts' => [ + 'memberCount' => 2, + 'projectCount' => 1, + ], + 'createdAt' => '2025-01-01T00:00:00+00:00', + 'updatedAt' => '2025-01-02T00:00:00+00:00', + ], + ]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode($expected) + )); + + $result = $this->task->listReferencedTeams('abc', 'sig123'); + + $this->assertIsArray($result); + $this->assertArrayHasKey('team-1', $result); + $this->assertEquals('Team One', $result['team-1']->getLabel()); + } + + public function testListReferencedTeamsWithEmptySig(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->task->listReferencedTeams('abc', ''); + } + + public function testListReferencedTeamsWithEmptyIn(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->task->listReferencedTeams('', 'sig123'); + } + /** * @throws ClientExceptionInterface * @throws Exception @@ -843,4 +895,36 @@ public function testDeleteConflict(): void $this->expectException(ApiException::class); $this->task->delete(teamId: $teamId); } + + /** + * @throws ClientExceptionInterface + */ + public function testGrantTeamProjectAccessToTeamSuccess(): void + { + $teamId = 'team-123'; + $access = [['projectId' => 'proj-123']]; + + $this->httpClient + ->expects($this->once()) + ->method('sendRequest') + ->willReturn(new Response( + 204, + ['Content-Type' => 'application/json'], + json_encode(['status' => 'No content', 'code' => 204]) + )); + + $this->task->grantTeamProjectAccessToTeam(teamId: $teamId, access: $access); + } + + /** + * Regression: an empty teamId must fail team validation (not project + * validation), confirming the duplicate checkProjectId() was removed. + */ + public function testGrantTeamProjectAccessToTeamRejectsEmptyTeamId(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Team ID is required'); + + $this->task->grantTeamProjectAccessToTeam(teamId: '', access: [['projectId' => 'proj-123']]); + } } diff --git a/tests/Core/Tasks/UsersTaskTest.php b/tests/Core/Tasks/UsersTaskTest.php index 7e3cbabf..f1e4db37 100644 --- a/tests/Core/Tasks/UsersTaskTest.php +++ b/tests/Core/Tasks/UsersTaskTest.php @@ -4,6 +4,7 @@ use BadMethodCallException; use Exception; +use InvalidArgumentException; use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\Response; use Psr\Http\Client\ClientExceptionInterface; @@ -15,6 +16,7 @@ use Upsun\Api\GrantsApi; use Upsun\Api\MfaApi; use Upsun\Api\PhoneNumberApi; +use Upsun\Api\ReferencesApi; use Upsun\Api\UserAccessApi; use Upsun\Api\UserProfilesApi; use Upsun\Api\UsersApi; @@ -23,6 +25,7 @@ use Upsun\Model\ApiToken; use Upsun\Model\ConfirmTotpEnrollment200Response; use Upsun\Model\Connection; +use Upsun\Model\CreateProfilePicture200Response; use Upsun\Model\GetAddress200Response; use Upsun\Model\GetCurrentUserVerificationStatus200Response; use Upsun\Model\GetCurrentUserVerificationStatusFull200Response; @@ -75,6 +78,7 @@ public function __invoke(bool $force = false): string new GrantsApi(...$apiClassParams), new MfaApi(...$apiClassParams), new PhoneNumberApi(...$apiClassParams), + new ReferencesApi(...$apiClassParams), ) extends UsersTask { }; } @@ -304,6 +308,102 @@ public function testResetEmailAddressSuccess() $this->assertTrue(true); } + /** + * @throws ClientExceptionInterface + */ + public function testCreateProfilePictureSuccess(): void + { + $response = ['url' => 'https://example.com/avatar.png']; + + $this->httpClient + ->method('sendRequest') + ->willReturn(new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode($response) + )); + + $result = $this->usersTask->createProfilePicture(userId: 'user_123'); + + $this->assertInstanceOf(CreateProfilePicture200Response::class, $result); + $this->assertObjectProperties($result, $response); + } + + /** + * @throws ClientExceptionInterface + */ + public function testGetAccessDocumentSuccess(): void + { + $response = ['document' => 'granted', 'scope' => 'project']; + + $this->httpClient + ->method('sendRequest') + ->willReturn(new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode($response) + )); + + $result = $this->usersTask->getAccessDocument('access_123'); + + $this->assertIsObject($result); + $this->assertEquals('granted', $result->document); + } + + /** + * @throws ClientExceptionInterface + */ + public function testListReferencedUsersSuccess(): void + { + $response = [ + 'user-1' => [ + 'id' => 'user-1', + 'username' => 'john', + 'email' => 'john@example.com', + 'firstName' => 'John', + 'lastName' => 'Doe', + 'picture' => 'https://example.com/john.png', + 'mfaEnabled' => true, + 'ssoEnabled' => false, + ], + ]; + + $this->httpClient + ->method('sendRequest') + ->willReturn(new Response( + 200, + ['Content-Type' => 'application/json'], + json_encode($response) + )); + + $result = $this->usersTask->listReferencedUsers('abc', 'sig123'); + + $this->assertIsArray($result); + $this->assertArrayHasKey('user-1', $result); + $this->assertEquals('john', $result['user-1']->getUsername()); + } + + public function testListReferencedUsersWithEmptySig(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->usersTask->listReferencedUsers('abc', ''); + } + + public function testListReferencedUsersWithEmptyIn(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->usersTask->listReferencedUsers('', 'sig123'); + } + + public function testGetAccessDocumentWithEmptyId(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->usersTask->getAccessDocument(''); + } + public function testGetCurrentUserVerificationStatusSuccess() { $responseFake = [ diff --git a/tests/UpsunClientTest.php b/tests/UpsunClientTest.php index c5971d8c..754cb3ff 100644 --- a/tests/UpsunClientTest.php +++ b/tests/UpsunClientTest.php @@ -230,6 +230,42 @@ public function testGetTokenThrowsWhenNoAuthMethodSet(): void $client->getToken(); } + /** + * @throws ReflectionException + */ + public function testLocalAuthUrlUsesClientCredentialsGrant(): void + { + $config = new UpsunConfig(auth_url: UpsunConfig::LOCAL_AUTH); + $client = new UpsunClient($config); + + $this->assertInstanceOf(OAuthProvider::class, $client->auth); + + $grantType = (new \ReflectionObject($client->auth))->getProperty('grantType'); + $tokenEndpoint = (new \ReflectionObject($client->auth))->getProperty('tokenEndpoint'); + + $this->assertEquals( + OAuthProvider::GRANT_CLIENT_CREDENTIALS, + $grantType->getValue($client->auth) + ); + $this->assertEquals( + 'http://localhost:8200/oauth2/token', + $tokenEndpoint->getValue($client->auth) + ); + } + + /** + * @throws ReflectionException + */ + public function testPublicAuthUrlUsesApiTokenGrant(): void + { + $grantType = (new \ReflectionObject($this->upsunClient->auth))->getProperty('grantType'); + + $this->assertEquals( + OAuthProvider::GRANT_API_TOKEN, + $grantType->getValue($this->upsunClient->auth) + ); + } + public function testUserIdIsNullByDefault() { $this->assertNull($this->upsunClient->userId);