From 5bd030ee2eb8b0fb7777de2bcb39412e19864da2 Mon Sep 17 00:00:00 2001 From: Matthias Van Woensel <3532563+matthiaz@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:35:31 +0200 Subject: [PATCH 1/5] fix: Clarify why certain functions are not available for upsun fixed --- .../Command/Resources/Build/BuildResourcesGetCommand.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php b/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php index 494488dde..7c32500b0 100644 --- a/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php +++ b/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php @@ -42,7 +42,12 @@ 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->stdErr->writeln(sprintf( + 'The flexible resources API is not enabled for the project %s.' . "\n" + . 'The function you attempted to use is not available on upsun fixed (platformsh).' . "\n" + . 'Please refer to the Upsun fixed (platformsh) documentation: https://fixed.docs.upsun.com/', + $this->api->getProjectLabel($selection->getProject(), 'comment') + )); return 1; } From 57a4b180ba22c48237143260f9ce6779e66546ba Mon Sep 17 00:00:00 2001 From: Matthias Van Woensel <3532563+matthiaz@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:46:53 +0200 Subject: [PATCH 2/5] Update error message for unsupported API usage --- .../src/Command/Resources/Build/BuildResourcesGetCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php b/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php index 7c32500b0..4ef84fde8 100644 --- a/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php +++ b/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php @@ -44,8 +44,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!$this->api->supportsSizingApi($selection->getProject())) { $this->stdErr->writeln(sprintf( 'The flexible resources API is not enabled for the project %s.' . "\n" - . 'The function you attempted to use is not available on upsun fixed (platformsh).' . "\n" - . 'Please refer to the Upsun fixed (platformsh) documentation: https://fixed.docs.upsun.com/', + . 'The function you attempted to use is not available on fixed plans.' . "\n" + . 'Please refer to the Fixed documentation: https://fixed.docs.upsun.com/', $this->api->getProjectLabel($selection->getProject(), 'comment') )); return 1; From a1adb08e57e3ed017cbb7afee629b919696b45f8 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Thu, 24 Sep 2026 11:07:25 +0100 Subject: [PATCH 3/5] fix(legacy): mention Upsun Fixed only for projects in Fixed organizations The "flexible resources API is not enabled" error is shared by all the resources:* commands, so move it to ResourcesUtil and use it everywhere instead of only in resources:build:get. A disabled sizing API does not by itself mean the project is on Upsun Fixed, so the Fixed note is now only shown when the project's organization has the "fixed" type. If the organization cannot be loaded (e.g. the user has no access to it), the note is skipped. The docs link uses the docs.upsun.com/anchors/fixed/ redirect, like other Fixed links in the CLI. Co-Authored-By: Claude Opus 5.5 --- .../resources_sizing_disabled_test.go | 98 +++++++++++++++++++ .../Build/BuildResourcesGetCommand.php | 10 +- .../Build/BuildResourcesSetCommand.php | 2 +- .../Command/Resources/ResourcesGetCommand.php | 2 +- .../Command/Resources/ResourcesSetCommand.php | 2 +- .../Resources/ResourcesSizeListCommand.php | 2 +- legacy/src/Service/ResourcesUtil.php | 29 ++++++ 7 files changed, 134 insertions(+), 11 deletions(-) create mode 100644 integration-tests/resources_sizing_disabled_test.go diff --git a/integration-tests/resources_sizing_disabled_test.go b/integration-tests/resources_sizing_disabled_test.go new file mode 100644 index 000000000..3f902be57 --- /dev/null +++ b/integration-tests/resources_sizing_disabled_test.go @@ -0,0 +1,98 @@ +package tests + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "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 Upsun Fixed only for Fixed orgs. +func TestResourcesSizingDisabled(t *testing.T) { + const fixedNote = "not available on Upsun 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, "https://docs.upsun.com/anchors/fixed/") + } else { + assert.NotContains(t, strings.ToLower(stderr), "fixed") + } + }) + } + }) + } +} diff --git a/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php b/legacy/src/Command/Resources/Build/BuildResourcesGetCommand.php index 4ef84fde8..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,12 +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.' . "\n" - . 'The function you attempted to use is not available on fixed plans.' . "\n" - . 'Please refer to the Fixed documentation: https://fixed.docs.upsun.com/', - $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..4d9bd489d 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->isFixedProject($project)) { + $this->stdErr->writeln('Flexible resources are not available on Upsun Fixed. See: https://docs.upsun.com/anchors/fixed/'); + } + } + + /** + * Checks if a project belongs to a Fixed organization, returning false if unknown. + */ + private function isFixedProject(Project $project): bool + { + $orgId = $project->getProperty('organization', false); + if (!$orgId) { + 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. * From 4717f0132274b6488bcaf3568564d99766a7a590 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Thu, 24 Sep 2026 11:13:56 +0100 Subject: [PATCH 4/5] test(metrics): mock the observability entrypoint in TestMetricsLatest Since #118, the metrics commands find the resources overview URL via the "resources_overview" link from GET {environment}/observability/, instead of the environment's "#observability-pipeline" link. TestMetricsLatest (#165) was written against the old lookup, so it failed on main. Co-Authored-By: Claude Opus 5.5 --- integration-tests/metrics_test.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/integration-tests/metrics_test.go b/integration-tests/metrics_test.go index b855e463e..1d90b7dea 100644 --- a/integration-tests/metrics_test.go +++ b/integration-tests/metrics_test.go @@ -31,7 +31,6 @@ func TestMetricsLatest(t *testing.T) { envPath := "/projects/" + projectID + "/environments/main" main := makeEnv(projectID, "main", "production", "active", nil) - main.Links["#observability-pipeline"] = mockapi.HALLink{HREF: apiServer.URL + envPath + "/observability"} main.SetCurrentDeployment(&mockapi.Deployment{ WebApps: map[string]mockapi.App{"app": {Name: "app", Type: "golang:1.23", Size: "AUTO"}}, Services: map[string]mockapi.App{"db": {Name: "db", Type: "mariadb:11.4", Size: "AUTO"}}, @@ -79,7 +78,13 @@ func TestMetricsLatest(t *testing.T) { defer mu.Unlock() data = d } - apiHandler.Get(envPath+"/observability/resources/overview", func(w http.ResponseWriter, _ *http.Request) { + overviewPath := envPath + "/observability/resources/overview" + apiHandler.Get(envPath+"/observability/", func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{ + "_links": mockapi.MakeHALLinks("resources_overview=" + apiServer.URL + overviewPath), + }) + }) + apiHandler.Get(overviewPath, func(w http.ResponseWriter, _ *http.Request) { mu.Lock() defer mu.Unlock() now = time.Now().UTC() From 9adcfe5cedbc7671cdc845e9aad58b4c915e71f8 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Thu, 24 Sep 2026 11:16:15 +0100 Subject: [PATCH 5/5] fix(legacy): configure the Fixed docs URL and avoid extra API calls Address review feedback on the sizing API error: - Read the docs link from a new service.fixed_docs_url config key, and drop the "Upsun" brand from the message, so vendor builds without the key show no Fixed note. - Skip the organization lookup when api.organizations is disabled. - Read the project's organization without lazy loading, so a missing property cannot trigger an uncaught API request. - Assert on the note and URL in the test, rather than any "fixed" text. Co-Authored-By: Claude Opus 5.5 --- integration-tests/config.yaml | 1 + integration-tests/resources_sizing_disabled_test.go | 13 ++++++++----- internal/config/platformsh-cli.yaml | 2 ++ internal/config/upsun-cli.yaml | 2 ++ legacy/config-defaults.yaml | 3 +++ legacy/config.yaml | 2 ++ legacy/src/Service/ResourcesUtil.php | 8 ++++---- 7 files changed, 22 insertions(+), 9 deletions(-) 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 index 3f902be57..61d955e7d 100644 --- a/integration-tests/resources_sizing_disabled_test.go +++ b/integration-tests/resources_sizing_disabled_test.go @@ -5,7 +5,6 @@ import ( "net/http" "net/http/httptest" "net/url" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -15,9 +14,12 @@ import ( ) // TestResourcesSizingDisabled checks the error shown when the project does not -// support flexible resources, which mentions Upsun Fixed only for Fixed orgs. +// support flexible resources, which mentions Fixed only for Fixed orgs. func TestResourcesSizingDisabled(t *testing.T) { - const fixedNote = "not available on Upsun Fixed" + const ( + fixedNote = "not available for Fixed organizations" + fixedURL = "https://docs.cli-tests.example.com/fixed/" + ) commands := [][]string{ {"resources:get", "-e", "main"}, @@ -87,9 +89,10 @@ func TestResourcesSizingDisabled(t *testing.T) { 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, "https://docs.upsun.com/anchors/fixed/") + assert.Contains(t, stderr, fixedURL) } else { - assert.NotContains(t, strings.ToLower(stderr), "fixed") + 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/Service/ResourcesUtil.php b/legacy/src/Service/ResourcesUtil.php index 4d9bd489d..af50303c5 100644 --- a/legacy/src/Service/ResourcesUtil.php +++ b/legacy/src/Service/ResourcesUtil.php @@ -45,8 +45,8 @@ public function featureEnabled(): bool 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->isFixedProject($project)) { - $this->stdErr->writeln('Flexible resources are not available on Upsun Fixed. See: https://docs.upsun.com/anchors/fixed/'); + 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') . ''); } } @@ -55,8 +55,8 @@ public function writeSizingApiDisabledError(Project $project): void */ private function isFixedProject(Project $project): bool { - $orgId = $project->getProperty('organization', false); - if (!$orgId) { + $orgId = $project->getProperty('organization', false, false); + if (!$orgId || !$this->config->getBool('api.organizations')) { return false; } try {