diff --git a/integration-tests/config.yaml b/integration-tests/config.yaml index 6ef9db184..c7d14baee 100644 --- a/integration-tests/config.yaml +++ b/integration-tests/config.yaml @@ -12,6 +12,7 @@ service: env_prefix: 'PLATFORM_' project_config_dir: '.platform' console_url: 'https://console.cli-tests.example.com' + fixed_docs_url: 'https://docs.cli-tests.example.com/fixed/' api: # Placeholder URLs which can be replaced during tests. diff --git a/integration-tests/resources_sizing_disabled_test.go b/integration-tests/resources_sizing_disabled_test.go new file mode 100644 index 000000000..61d955e7d --- /dev/null +++ b/integration-tests/resources_sizing_disabled_test.go @@ -0,0 +1,101 @@ +package tests + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/upsun/cli/pkg/mockapi" +) + +// TestResourcesSizingDisabled checks the error shown when the project does not +// support flexible resources, which mentions Fixed only for Fixed orgs. +func TestResourcesSizingDisabled(t *testing.T) { + const ( + fixedNote = "not available for Fixed organizations" + fixedURL = "https://docs.cli-tests.example.com/fixed/" + ) + + commands := [][]string{ + {"resources:get", "-e", "main"}, + {"resources:set", "-e", "main", "--size", "app:0.5"}, + {"resources:size:list", "-e", "main"}, + {"resources:build:get"}, + {"resources:build:set", "--cpu", "1"}, + } + cases := []struct { + name string + orgType string + wantFixed bool + }{ + {"fixed org", "fixed", true}, + {"flexible org", "flexible", false}, + {"inaccessible org", "", false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + authServer := mockapi.NewAuthServer(t) + defer authServer.Close() + + apiHandler := mockapi.NewHandler(t) + + myUserID := "my-user-id" + orgID := "org-id" + apiHandler.SetMyUser(&mockapi.User{ID: myUserID}) + if c.orgType != "" { + apiHandler.SetOrgs([]*mockapi.Org{{ + ID: orgID, + Type: c.orgType, + Name: "acme", + Label: "Acme", + Owner: myUserID, + Capabilities: []string{}, + Links: mockapi.MakeHALLinks("self=/organizations/" + url.PathEscape(orgID)), + }}) + } + + projectID := mockapi.ProjectID() + apiHandler.SetProjects([]*mockapi.Project{{ + ID: projectID, + Organization: orgID, + Links: mockapi.MakeHALLinks( + "self=/projects/"+projectID, + "environments=/projects/"+projectID+"/environments", + ), + DefaultBranch: "main", + }}) + apiHandler.SetEnvironments([]*mockapi.Environment{ + makeEnv(projectID, "main", "production", "active", nil), + }) + apiHandler.Get("/projects/"+projectID+"/settings", func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"sizing_api_enabled": false}) + }) + + apiServer := httptest.NewServer(apiHandler) + defer apiServer.Close() + + f := newCommandFactory(t, apiServer.URL, authServer.URL) + + for _, args := range commands { + t.Run(args[0], func(t *testing.T) { + _, stderr, err := f.RunCombinedOutput(append(args, "-p", projectID)...) + require.Error(t, err) + assert.Contains(t, stderr, "The flexible resources API is not enabled for the project") + if c.wantFixed { + assert.Contains(t, stderr, fixedNote) + assert.Contains(t, stderr, fixedURL) + } else { + assert.NotContains(t, stderr, fixedNote) + assert.NotContains(t, stderr, fixedURL) + } + }) + } + }) + } +} diff --git a/internal/config/platformsh-cli.yaml b/internal/config/platformsh-cli.yaml index dd7592e03..e3860e3ab 100644 --- a/internal/config/platformsh-cli.yaml +++ b/internal/config/platformsh-cli.yaml @@ -38,6 +38,8 @@ service: pricing_url: "https://upsun.com/pricing" + fixed_docs_url: "https://docs.upsun.com/anchors/fixed/" + activity_type_list_url: "https://docs.upsun.com/anchors/fixed/integrations/activity-scripts/type/" runtime_operations_help_url: "https://docs.upsun.com/anchors/fixed/app/runtime-operations/" diff --git a/internal/config/upsun-cli.yaml b/internal/config/upsun-cli.yaml index 7fff20369..39d96adf1 100644 --- a/internal/config/upsun-cli.yaml +++ b/internal/config/upsun-cli.yaml @@ -44,6 +44,8 @@ service: pricing_url: "https://upsun.com/pricing" + fixed_docs_url: "https://docs.upsun.com/anchors/fixed/" + activity_type_list_url: "https://docs.upsun.com/anchors/integrations/activity-scripts/type/" runtime_operations_help_url: "https://docs.upsun.com/anchors/app/runtime-operations/" diff --git a/legacy/config-defaults.yaml b/legacy/config-defaults.yaml index 256665a74..552d4f271 100644 --- a/legacy/config-defaults.yaml +++ b/legacy/config-defaults.yaml @@ -155,6 +155,9 @@ service: # Optional URL to help about managing resources. resources_help_url: null + # Optional URL to docs for Fixed organizations. + fixed_docs_url: null + # Configuration relating to API calls. # This can be overridden in the user config file. api: diff --git a/legacy/config.yaml b/legacy/config.yaml index 254c775a4..8b9c5b9c2 100644 --- a/legacy/config.yaml +++ b/legacy/config.yaml @@ -38,6 +38,8 @@ service: pricing_url: 'https://upsun.com/pricing' + fixed_docs_url: 'https://docs.upsun.com/anchors/fixed/' + activity_type_list_url: 'https://docs.upsun.com/anchors/fixed/integrations/activity-scripts/type/' runtime_operations_help_url: 'https://docs.upsun.com/anchors/fixed/app/runtime-operations/' diff --git a/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php b/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php index 494488dde..db8bfd1c5 100644 --- a/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php +++ b/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php @@ -7,6 +7,7 @@ use Platformsh\Cli\Selector\Selector; use Platformsh\Cli\Service\Api; use Platformsh\Cli\Service\Config; +use Platformsh\Cli\Service\ResourcesUtil; use Platformsh\Cli\Command\CommandBase; use Platformsh\Cli\Service\Table; use Symfony\Component\Console\Attribute\AsCommand; @@ -23,7 +24,7 @@ class BuildResourcesGetCommand extends CommandBase 'memory' => 'Memory (MB)', ]; - public function __construct(private readonly Api $api, private readonly Config $config, private readonly Selector $selector, private readonly Table $table) + public function __construct(private readonly Api $api, private readonly Config $config, private readonly ResourcesUtil $resourcesUtil, private readonly Selector $selector, private readonly Table $table) { parent::__construct(); } @@ -42,7 +43,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $selection = $this->selector->getSelection($input); if (!$this->api->supportsSizingApi($selection->getProject())) { - $this->stdErr->writeln(sprintf('The flexible resources API is not enabled for the project %s.', $this->api->getProjectLabel($selection->getProject(), 'comment'))); + $this->resourcesUtil->writeSizingApiDisabledError($selection->getProject()); return 1; } diff --git a/legacy/src/Command/Resources/Build/BuildResourcesSetCommand.php b/legacy/src/Command/Resources/Build/BuildResourcesSetCommand.php index 7a21c4ded..e2bbcec32 100644 --- a/legacy/src/Command/Resources/Build/BuildResourcesSetCommand.php +++ b/legacy/src/Command/Resources/Build/BuildResourcesSetCommand.php @@ -37,7 +37,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $selection = $this->selector->getSelection($input); if (!$this->api->supportsSizingApi($selection->getProject())) { - $this->stdErr->writeln(sprintf('The flexible resources API is not enabled for the project %s.', $this->api->getProjectLabel($selection->getProject(), 'comment'))); + $this->resourcesUtil->writeSizingApiDisabledError($selection->getProject()); return 1; } diff --git a/legacy/src/Command/Resources/ResourcesGetCommand.php b/legacy/src/Command/Resources/ResourcesGetCommand.php index 6402393b8..63dd93f52 100644 --- a/legacy/src/Command/Resources/ResourcesGetCommand.php +++ b/legacy/src/Command/Resources/ResourcesGetCommand.php @@ -68,7 +68,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $selection = $this->selector->getSelection($input); if (!$this->api->supportsSizingApi($selection->getProject())) { - $this->stdErr->writeln(sprintf('The flexible resources API is not enabled for the project %s.', $this->api->getProjectLabel($selection->getProject(), 'comment'))); + $this->resourcesUtil->writeSizingApiDisabledError($selection->getProject()); return 1; } diff --git a/legacy/src/Command/Resources/ResourcesSetCommand.php b/legacy/src/Command/Resources/ResourcesSetCommand.php index fd78577fb..997afccf5 100644 --- a/legacy/src/Command/Resources/ResourcesSetCommand.php +++ b/legacy/src/Command/Resources/ResourcesSetCommand.php @@ -113,7 +113,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $selection = $this->selector->getSelection($input); if (!$this->api->supportsSizingApi($selection->getProject())) { - $this->stdErr->writeln(sprintf('The flexible resources API is not enabled for the project %s.', $this->api->getProjectLabel($selection->getProject(), 'comment'))); + $this->resourcesUtil->writeSizingApiDisabledError($selection->getProject()); return 1; } diff --git a/legacy/src/Command/Resources/ResourcesSizeListCommand.php b/legacy/src/Command/Resources/ResourcesSizeListCommand.php index 33b78279b..98c9a282f 100644 --- a/legacy/src/Command/Resources/ResourcesSizeListCommand.php +++ b/legacy/src/Command/Resources/ResourcesSizeListCommand.php @@ -49,7 +49,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $selection = $this->selector->getSelection($input); if (!$this->api->supportsSizingApi($selection->getProject())) { - $this->stdErr->writeln(sprintf('The flexible resources API is not enabled for the project %s.', $this->api->getProjectLabel($selection->getProject(), 'comment'))); + $this->resourcesUtil->writeSizingApiDisabledError($selection->getProject()); return 1; } diff --git a/legacy/src/Service/ResourcesUtil.php b/legacy/src/Service/ResourcesUtil.php index 99703b18b..af50303c5 100644 --- a/legacy/src/Service/ResourcesUtil.php +++ b/legacy/src/Service/ResourcesUtil.php @@ -39,6 +39,35 @@ public function featureEnabled(): bool return $this->config->getBool('api.sizing'); } + /** + * Prints an error explaining that the project does not support flexible resources. + */ + public function writeSizingApiDisabledError(Project $project): void + { + $this->stdErr->writeln(sprintf('The flexible resources API is not enabled for the project %s.', $this->api->getProjectLabel($project, 'comment'))); + if ($this->config->has('service.fixed_docs_url') && $this->isFixedProject($project)) { + $this->stdErr->writeln('Flexible resources are not available for Fixed organizations. See: ' . $this->config->getStr('service.fixed_docs_url') . ''); + } + } + + /** + * Checks if a project belongs to a Fixed organization, returning false if unknown. + */ + private function isFixedProject(Project $project): bool + { + $orgId = $project->getProperty('organization', false, false); + if (!$orgId || !$this->config->getBool('api.organizations')) { + return false; + } + try { + $organization = $this->api->getOrganizationById($orgId); + } catch (\Exception) { + // The user may not have access to the organization. + return false; + } + return $organization && $organization->getProperty('type', false) === 'fixed'; + } + /** * Lists services in a deployment. *