Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions integration-tests/variable_write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,102 @@ func TestVariableCreate(t *testing.T) {
assert.NoError(t, err)
assertTrimmed(t, "false", f.Run("var:get", "-p", projectID, "env:TEST", "-l", "p", "-P", "visible_runtime"))
}

func TestVariableCreateWithAppScope(t *testing.T) {
authServer := mockapi.NewAuthServer(t)
t.Cleanup(authServer.Close)

apiHandler := mockapi.NewHandler(t)
apiServer := httptest.NewServer(apiHandler)
t.Cleanup(apiServer.Close)

projectID := mockapi.ProjectID()

apiHandler.SetProjects([]*mockapi.Project{{
ID: projectID,
Links: mockapi.MakeHALLinks(
"self=/projects/"+projectID,
"environments=/projects/"+projectID+"/environments",
),
DefaultBranch: "main",
}})

main := makeEnv(projectID, "main", "production", "active", nil)
main.Links["#variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"}
main.Links["#manage-variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"}
main.SetCurrentDeployment(&mockapi.Deployment{
WebApps: map[string]mockapi.App{
"app1": {Name: "app1", Type: "golang:1.23"},
"app2": {Name: "app2", Type: "php:8.3"},
},
Routes: map[string]any{},
Links: mockapi.MakeHALLinks("self=/projects/" + projectID + "/environments/main/deployment/current"),
})
apiHandler.SetEnvironments([]*mockapi.Environment{main})

f := newCommandFactory(t, apiServer.URL, authServer.URL)

_, stdErr, err := f.RunCombinedOutput("var:create", "-p", projectID, "-l", "p",
"env:SCOPED", "--value", "val", "--app-scope", "app1")
assert.NoError(t, err)
assert.Contains(t, stdErr, "Creating variable env:SCOPED")

out := f.Run("var:get", "-p", projectID, "-l", "p", "env:SCOPED", "-P", "application_scope")
assert.Contains(t, out, "app1")

_, _, err = f.RunCombinedOutput("var:create", "-p", projectID, "-l", "p",
"env:MULTI", "--value", "val", "--app-scope", "app1", "--app-scope", "app2")
assert.NoError(t, err)

out = f.Run("var:get", "-p", projectID, "-l", "p", "env:MULTI", "-P", "application_scope")
assert.Contains(t, out, "app1")
assert.Contains(t, out, "app2")

_, stdErr, err = f.RunCombinedOutput("var:create", "-p", projectID, "-l", "p",
"env:BAD", "--value", "val", "--app-scope", "nonexistent")
assert.Error(t, err)
assert.Contains(t, stdErr, "was not found")

_, _, err = f.RunCombinedOutput("var:update", "-p", projectID, "-l", "p",
"env:SCOPED", "--app-scope", "app2")
assert.NoError(t, err)

out = f.Run("var:get", "-p", projectID, "-l", "p", "env:SCOPED", "-P", "application_scope")
assert.Contains(t, out, "app2")
}

func TestVariableCreateWithAppScopeNoDeployment(t *testing.T) {
// Uses an environment without a deployment, so app-scope validation is skipped.
authServer := mockapi.NewAuthServer(t)
t.Cleanup(authServer.Close)

apiHandler := mockapi.NewHandler(t)
apiServer := httptest.NewServer(apiHandler)
t.Cleanup(apiServer.Close)

projectID := mockapi.ProjectID()

apiHandler.SetProjects([]*mockapi.Project{{
ID: projectID,
Links: mockapi.MakeHALLinks(
"self=/projects/"+projectID,
"environments=/projects/"+projectID+"/environments",
),
DefaultBranch: "main",
}})

main := makeEnv(projectID, "main", "production", "active", nil)
main.Links["#variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"}
main.Links["#manage-variables"] = mockapi.HALLink{HREF: "/projects/" + projectID + "/environments/main/variables"}
apiHandler.SetEnvironments([]*mockapi.Environment{main})

f := newCommandFactory(t, apiServer.URL, authServer.URL)

_, stdErr, err := f.RunCombinedOutput("var:create", "-p", projectID, "-l", "p",
"env:ANY_APP", "--value", "val", "--app-scope", "anyapp")
assert.NoError(t, err)
assert.Contains(t, stdErr, "Creating variable env:ANY_APP")

out := f.Run("var:get", "-p", projectID, "-l", "p", "env:ANY_APP", "-P", "application_scope")
assert.Contains(t, out, "anyapp")
}
52 changes: 52 additions & 0 deletions legacy/src/Service/VariableCommandUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
use Platformsh\Cli\Console\AdaptiveTableCell;
use Platformsh\Cli\Selector\Selection;
use Platformsh\Client\Model\ApiResourceBase;
use Platformsh\Client\Model\Environment;
use Platformsh\Client\Model\Project;
use Platformsh\Client\Model\ProjectLevelVariable;
use Platformsh\Client\Model\Variable as EnvironmentLevelVariable;
use Platformsh\ConsoleForm\Field\ArrayField;
use Platformsh\ConsoleForm\Field\BooleanField;
use Platformsh\ConsoleForm\Field\Field;
use Platformsh\ConsoleForm\Field\OptionsField;
Expand Down Expand Up @@ -170,6 +173,32 @@ public function getFields(callable $getSelection): array
'includeAsOption' => false,
'defaultCallback' => fn(): ?string => $getSelection()->hasEnvironment() ? $getSelection()->getEnvironment()->id : null,
]);
$fields['application_scope'] = new ArrayField('Application scope', [
'optionName' => 'app-scope',
'description' => 'A list of application names to which this variable will apply.',
'questionLine' => 'To which applications should this variable apply?',
'default' => [],
'required' => false,
'avoidQuestion' => true,
'validator' => function ($values) use ($getSelection): bool {
$selection = $getSelection();
$appNames = $this->listApps($selection->getProject(), $selection->hasEnvironment() ? $selection->getEnvironment() : null);
if ($appNames === false) {
// No app names available: skip validation.
return true;
}
foreach ($values as $value) {
if (!in_array($value, $appNames, true)) {
throw new InvalidArgumentException(sprintf(
'The app "%s" was not found. Valid app names are: %s',
$value,
implode(', ', $appNames),
));
}
}
return true;
},
]);
$fields['name'] = new Field('Name', [
'description' => 'The variable name',
'validators' => [
Expand Down Expand Up @@ -256,4 +285,27 @@ private function getPrefixOptions(string $name): array
'env:' => 'env: The variable will be exposed directly, e.g. as <comment>$' . strtoupper($name) . '</comment>.',
];
}

/**
* Lists application names for validating application_scope values.
*
* @return string[]|false An array of app names, or false if they could not be determined.
*/
private function listApps(Project $project, ?Environment $environment = null): array|false
{
if (!$environment) {
if (\is_string($project->default_branch) && $project->default_branch !== '') {
$environment = $this->api->getEnvironment($project->default_branch, $project) ?: null;
}
if (!$environment) {
return false;
}
}
try {
$deployment = $this->api->getCurrentDeployment($environment);
} catch (\Exception) {
return false;
}
return array_keys($deployment->webapps);
}
}
Loading