diff --git a/go-tests/go.mod b/go-tests/go.mod
index 39405cc7ad..30fbf9c05f 100644
--- a/go-tests/go.mod
+++ b/go-tests/go.mod
@@ -3,8 +3,9 @@ module github.com/platformsh/legacy-cli/tests
go 1.22.9
require (
- github.com/platformsh/cli v0.0.0-20241229194532-b86546247906
+ github.com/platformsh/cli v0.0.0-20250115153051-60dcb793eb89
github.com/stretchr/testify v1.9.0
+ golang.org/x/crypto v0.31.0
)
require (
@@ -13,7 +14,6 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/oklog/ulid/v2 v2.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
- golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sys v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go-tests/go.sum b/go-tests/go.sum
index d509303e44..332ea2f4e4 100644
--- a/go-tests/go.sum
+++ b/go-tests/go.sum
@@ -11,8 +11,8 @@ github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
-github.com/platformsh/cli v0.0.0-20241229194532-b86546247906 h1:BvQf2yvT7USm80fAJ0C5FXfTfjjK2onrnoVMPz5CT9k=
-github.com/platformsh/cli v0.0.0-20241229194532-b86546247906/go.mod h1:jMxyJGLMlkjDq7l9cVWFhsX/8xxFqEOH95rWbDFyO08=
+github.com/platformsh/cli v0.0.0-20250115153051-60dcb793eb89 h1:+EN931RRA5tsviOEqw6HW62u6UusZJq7LJKY3EIYrE0=
+github.com/platformsh/cli v0.0.0-20250115153051-60dcb793eb89/go.mod h1:b1v98rkg8bScSoo5gK8Fc1qRta1FUU8wElRf+bGvV5Y=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
diff --git a/go-tests/ssh_test.go b/go-tests/ssh_test.go
index d2b5a56fa5..2ecac9f63e 100644
--- a/go-tests/ssh_test.go
+++ b/go-tests/ssh_test.go
@@ -2,6 +2,7 @@ package tests
import (
"net/http/httptest"
+ "os"
"os/exec"
"strconv"
"testing"
@@ -21,10 +22,6 @@ func TestSSH(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- sshServer.RemoteEnv = []string{
- // TODO use this
- "PLATFORM_RELATIONSHIPS=e30K",
- }
t.Cleanup(func() {
if err := sshServer.Stop(); err != nil {
t.Error(err)
@@ -71,7 +68,8 @@ func TestSSH(t *testing.T) {
}
f.Run("cc")
- assert.Equal(t, sshServer.RemoteDir+"\n", f.Run("ssh", "-p", projectID, "-e", ".", "pwd"))
+ wd, _ := os.Getwd()
+ assert.Equal(t, wd+"\n", f.Run("ssh", "-p", projectID, "-e", ".", "pwd"))
_, stdErr, err := f.RunCombinedOutput("ssh", "-p", projectID, "-e", "main", "--instance", "2", "pwd")
assert.Error(t, err)
diff --git a/go-tests/valkey_test.go b/go-tests/valkey_test.go
new file mode 100644
index 0000000000..7a4c4f19c0
--- /dev/null
+++ b/go-tests/valkey_test.go
@@ -0,0 +1,115 @@
+package tests
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "strings"
+ "testing"
+
+ "golang.org/x/crypto/ssh"
+
+ "github.com/platformsh/cli/pkg/mockapi"
+ "github.com/platformsh/cli/pkg/mockssh"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestValkey(t *testing.T) {
+ authServer := mockapi.NewAuthServer(t)
+ defer authServer.Close()
+
+ myUserID := "my-user-id"
+
+ sshServer, err := mockssh.NewServer(t, authServer.URL+"/ssh/authority")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ relationships := map[string]any{
+ "cache": []map[string]any{{
+ "username": nil,
+ "host": "cache.internal",
+ "path": nil,
+ "query": url.Values{},
+ "password": nil,
+ "port": 6379,
+ "service": "cache",
+ "scheme": "valkey",
+ "type": "valkey:8.0",
+ "public": false,
+ }},
+ }
+ relationshipsJSON, err := json.Marshal(relationships)
+ require.NoError(t, err)
+
+ execHandler := mockssh.ExecHandler(t.TempDir(), []string{
+ "PLATFORM_RELATIONSHIPS=" + base64.StdEncoding.EncodeToString(relationshipsJSON),
+ })
+
+ sshServer.CommandHandler = func(conn ssh.ConnMetadata, command string, io mockssh.CommandIO) int {
+ if strings.HasPrefix(command, "valkey-cli") {
+ _, _ = fmt.Fprint(io.StdOut, "Received command: "+command)
+ return 0
+ }
+
+ return execHandler(conn, command, io)
+ }
+ t.Cleanup(func() {
+ if err := sshServer.Stop(); err != nil {
+ t.Error(err)
+ }
+ })
+
+ projectID := mockapi.ProjectID()
+
+ apiHandler := mockapi.NewHandler(t)
+ apiHandler.SetMyUser(&mockapi.User{ID: myUserID})
+ apiHandler.SetProjects([]*mockapi.Project{
+ {
+ ID: projectID,
+ Links: mockapi.MakeHALLinks(
+ "self=/projects/"+projectID,
+ "environments=/projects/"+projectID+"/environments",
+ ),
+ DefaultBranch: "main",
+ },
+ })
+ mainEnv := makeEnv(projectID, "main", "production", "active", nil)
+ mainEnv.SetCurrentDeployment(&mockapi.Deployment{
+ WebApps: map[string]mockapi.App{
+ "app": {Name: "app", Type: "golang:1.23", Size: "M", Disk: 2048, Mounts: map[string]mockapi.Mount{}},
+ },
+ Services: map[string]mockapi.App{},
+ Workers: map[string]mockapi.Worker{},
+ Routes: mockRoutes(),
+ Links: mockapi.MakeHALLinks("self=/projects/" + projectID + "/environments/main/deployment/current"),
+ })
+ mainEnv.Links["pf:ssh:app:0"] = mockapi.HALLink{HREF: "ssh://app--0@ssh.cli-tests.example.com"}
+ apiHandler.SetEnvironments([]*mockapi.Environment{
+ mainEnv,
+ })
+
+ apiServer := httptest.NewServer(apiHandler)
+ defer apiServer.Close()
+
+ f := newCommandFactory(t, apiServer.URL, authServer.URL)
+ f.extraEnv = []string{
+ EnvPrefix + "SSH_OPTIONS=HostName 127.0.0.1\nPort " + strconv.Itoa(sshServer.Port()),
+ EnvPrefix + "SSH_HOST_KEYS=" + sshServer.HostKeyConfig(),
+ }
+
+ f.Run("cc")
+
+ assert.Equal(t, "Received command: valkey-cli -h cache.internal -p 6379 ping",
+ f.Run("valkey", "-p", projectID, "-e", ".", "ping"))
+
+ assert.Equal(t, "Received command: valkey-cli -h cache.internal -p 6379 --scan",
+ f.Run("valkey", "-p", projectID, "-e", ".", "--", "--scan"))
+
+ assert.Equal(t, "Received command: valkey-cli -h cache.internal -p 6379 --scan --pattern '*-11*'",
+ f.Run("valkey", "-p", projectID, "-e", ".", "--", "--scan --pattern '*-11*'"))
+}
diff --git a/src/Application.php b/src/Application.php
index 5da57a1172..06e7510309 100644
--- a/src/Application.php
+++ b/src/Application.php
@@ -233,6 +233,7 @@ protected function getCommands()
$commands[] = new Command\Service\MongoDB\MongoShellCommand();
$commands[] = new Command\Service\RedisCliCommand();
$commands[] = new Command\Service\ServiceListCommand();
+ $commands[] = new Command\Service\ValkeyCliCommand();
$commands[] = new Command\Session\SessionSwitchCommand();
$commands[] = new Command\Backup\BackupCreateCommand();
$commands[] = new Command\Backup\BackupDeleteCommand();
diff --git a/src/Command/Service/RedisCliCommand.php b/src/Command/Service/RedisCliCommand.php
index fd8124c55f..5280bf4d74 100644
--- a/src/Command/Service/RedisCliCommand.php
+++ b/src/Command/Service/RedisCliCommand.php
@@ -2,70 +2,9 @@
namespace Platformsh\Cli\Command\Service;
-use Platformsh\Cli\Command\CommandBase;
-use Platformsh\Cli\Model\Host\RemoteHost;
-use Platformsh\Cli\Service\Relationships;
-use Platformsh\Cli\Service\Ssh;
-use Platformsh\Cli\Util\OsUtil;
-use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-
-class RedisCliCommand extends CommandBase
+class RedisCliCommand extends ValkeyCliCommand
{
- protected function configure()
- {
- $this->setName('service:redis-cli');
- $this->setAliases(['redis']);
- $this->setDescription('Access the Redis CLI');
- $this->addArgument('args', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Arguments to add to the Redis command');
- Relationships::configureInput($this->getDefinition());
- Ssh::configureInput($this->getDefinition());
- $this->addProjectOption()
- ->addEnvironmentOption()
- ->addAppOption();
- $this->addExample('Open the redis-cli shell');
- $this->addExample('Ping the Redis server', 'ping');
- $this->addExample('Show Redis status information', 'info');
- $this->addExample('Scan keys', "-- --scan");
- $this->addExample('Scan keys matching a pattern', '-- "--scan --pattern \'*-11*\'"');
- }
-
- protected function execute(InputInterface $input, OutputInterface $output)
- {
- if ($this->runningViaMulti && !$input->getArgument('args')) {
- throw new \RuntimeException('The redis-cli command cannot run as a shell via multi');
- }
-
- /** @var \Platformsh\Cli\Service\Relationships $relationshipsService */
- $relationshipsService = $this->getService('relationships');
- $host = $this->selectHost($input, $relationshipsService->hasLocalEnvVar());
-
- $service = $relationshipsService->chooseService($host, $input, $output, ['redis']);
- if (!$service) {
- return 1;
- }
-
- $redisCommand = sprintf(
- 'redis-cli -h %s -p %d',
- OsUtil::escapePosixShellArg($service['host']),
- $service['port']
- );
- if ($args = $input->getArgument('args')) {
- if (count($args) === 1) {
- $redisCommand .= ' ' . $args[0];
- } else {
- $redisCommand .= ' ' . implode(' ', array_map([OsUtil::class, 'escapePosixShellArg'], $args));
- }
- } elseif ($this->isTerminal(STDIN) && $host instanceof RemoteHost) {
- // Force TTY output when the input is a terminal.
- $host->setExtraSshOptions(['RequestTTY yes']);
- }
-
- $this->stdErr->writeln(
- sprintf('Connecting to Redis service via relationship %s on %s', $service['_relationship_name'], $host->getLabel())
- );
-
- return $host->runCommandDirect($redisCommand);
- }
+ protected $dbName = 'redis';
+ protected $dbTitle = 'Redis';
+ protected $dbCommand = 'redis-cli';
}
diff --git a/src/Command/Service/ValkeyCliCommand.php b/src/Command/Service/ValkeyCliCommand.php
new file mode 100644
index 0000000000..1e30208eb9
--- /dev/null
+++ b/src/Command/Service/ValkeyCliCommand.php
@@ -0,0 +1,76 @@
+setName('service:' . $this->dbCommand);
+ $this->setAliases([$this->dbName]);
+ $this->setDescription('Access the ' . $this->dbTitle . ' CLI');
+ $this->addArgument('args', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, sprintf('Arguments to add to the %s command', $this->dbCommand));
+ Relationships::configureInput($this->getDefinition());
+ Ssh::configureInput($this->getDefinition());
+ $this->addProjectOption()
+ ->addEnvironmentOption()
+ ->addAppOption();
+ $this->addExample(sprintf('Open the %s shell', $this->dbCommand));
+ $this->addExample(sprintf('Ping the %s server', $this->dbTitle), 'ping');
+ $this->addExample(sprintf('Show %s status information', $this->dbTitle), 'info');
+ $this->addExample('Scan keys', "-- --scan");
+ $this->addExample('Scan keys matching a pattern', '-- "--scan --pattern \'*-11*\'"');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ if ($this->runningViaMulti && !$input->getArgument('args')) {
+ throw new \RuntimeException(sprintf('The %s command cannot run as a shell via multi', $this->dbCommand));
+ }
+
+ /** @var \Platformsh\Cli\Service\Relationships $relationshipsService */
+ $relationshipsService = $this->getService('relationships');
+ $host = $this->selectHost($input, $relationshipsService->hasLocalEnvVar());
+
+ $service = $relationshipsService->chooseService($host, $input, $output, [$this->dbName]);
+ if (!$service) {
+ return 1;
+ }
+
+ $command = sprintf(
+ '%s -h %s -p %d',
+ $this->dbCommand,
+ OsUtil::escapePosixShellArg($service['host']),
+ $service['port']
+ );
+ if ($args = $input->getArgument('args')) {
+ if (count($args) === 1) {
+ $command .= ' ' . $args[0];
+ } else {
+ $command .= ' ' . implode(' ', array_map([OsUtil::class, 'escapePosixShellArg'], $args));
+ }
+ } elseif ($this->isTerminal(STDIN) && $host instanceof RemoteHost) {
+ // Force TTY output when the input is a terminal.
+ $host->setExtraSshOptions(['RequestTTY yes']);
+ }
+
+ $this->stdErr->writeln(
+ sprintf('Connecting to %s service via relationship %s on %s', $this->dbTitle, $service['_relationship_name'], $host->getLabel())
+ );
+
+ return $host->runCommandDirect($command);
+ }
+}