Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.streamx.cli.commands.project.get.GetCommand;
import com.streamx.cli.commands.project.list.ListCommand;
import com.streamx.cli.commands.project.pendingchanges.PendingChangesCommand;
import com.streamx.cli.commands.project.repo.RepoCommand;
import com.streamx.cli.commands.project.status.StatusCommand;
import com.streamx.cli.commands.project.update.UpdateCommand;
import com.streamx.cli.framework.AbstractCommandGroup;
Expand All @@ -21,6 +22,7 @@
GetCommand.class,
ListCommand.class,
PendingChangesCommand.class,
RepoCommand.class,
StatusCommand.class,
UpdateCommand.class
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.streamx.cli.commands.project.repo;

import com.streamx.cli.platform.OrgIdCompletionCandidates;
import com.streamx.cli.platform.ProjectIdCompletionCandidates;
import picocli.CommandLine;

/** The org/project selectors shared by every {@code project repo} subcommand. */
public class ProjectScopedOptions {

@CommandLine.Option(
names = "--org",
paramLabel = "<orgId>",
description = "Organization ID (defaults to the current organization)",
completionCandidates = OrgIdCompletionCandidates.class
)
public String orgId;

@CommandLine.Option(
names = "--project",
paramLabel = "<projectId>",
description = "Project ID (defaults to the current project)",
completionCandidates = ProjectIdCompletionCandidates.class
)
public String projectId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.streamx.cli.commands.project.repo;

import com.streamx.cli.commands.project.repo.get.GetCommand;
import com.streamx.cli.commands.project.repo.remove.RemoveCommand;
import com.streamx.cli.commands.project.repo.set.SetCommand;
import com.streamx.cli.commands.project.repo.sshkey.SshKeyCommand;
import com.streamx.cli.framework.AbstractCommandGroup;
import picocli.CommandLine;

@CommandLine.Command(
name = "repo",
header = "Manage the Git repository connected to a project",
subcommands = {
GetCommand.class,
SetCommand.class,
RemoveCommand.class,
SshKeyCommand.class
}
)
public class RepoCommand extends AbstractCommandGroup {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.streamx.cli.commands.project.repo.get;

import static com.streamx.cli.i18n.MessageProvider.msg;

import com.streamx.cli.commands.project.repo.ProjectScopedOptions;
import com.streamx.cli.framework.AbstractCommand;
import com.streamx.cli.framework.CliException;
import com.streamx.cli.framework.CommandResult;
import com.streamx.cli.platform.PlatformClients;
import com.streamx.cli.platform.PlatformContext;
import com.streamx.cli.platform.ProjectRepositoryApi;
import com.streamx.cli.platform.generated.model.ProjectRepository;
import java.util.List;
import picocli.CommandLine;

@CommandLine.Command(
name = "get",
header = "Show the repository connected to a project"
)
public class GetCommand extends AbstractCommand<ProjectRepository> {

@CommandLine.Mixin
ProjectScopedOptions scope;

@Override
public String getTextOutput(CommandResult<ProjectRepository> result) {
ProjectRepository repository = result.getData();
var status = repository.getProjectRepositoryStatus();
Boolean ready = status == null ? null : status.getReady();
List<String> errors = status == null || status.getErrorMessages() == null
? List.of() : status.getErrorMessages();
return """
uri = %s
branch = %s
commit = %s
ready = %s
ssh key = %s%s"""
.formatted(
orDash(repository.getUri()),
orDash(repository.getBranch()),
orDash(repository.getCommitId()),
ready == null ? "-" : ready,
Boolean.TRUE.equals(repository.getSshKeyProvided())
? msg.sshKeySpecified() : msg.sshKeyNotSpecified(),
errors.isEmpty() ? "" : "\nerrors = " + String.join("; ", errors));
}

private static String orDash(String value) {
return value == null ? "-" : value;
}

@Override
public CommandResult<ProjectRepository> runCommand() {
PlatformContext.OrgProject context =
PlatformContext.orgAndProject(scope.orgId, scope.projectId);
try (PlatformClients client = PlatformClients.fromConfig()) {
return new CommandResult<>(new ProjectRepositoryApi(client)
.get(context.org(), context.project()));
} catch (PlatformClients.NotFoundException e) {
throw new CliException(msg.projectRepoNotConnected(context.project()), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.streamx.cli.commands.project.repo.remove;

import static com.streamx.cli.i18n.MessageProvider.msg;

import com.streamx.cli.commands.project.repo.ProjectScopedOptions;
import com.streamx.cli.framework.AbstractSilentCommand;
import com.streamx.cli.framework.CliException;
import com.streamx.cli.framework.CommandResult;
import com.streamx.cli.platform.PlatformClients;
import com.streamx.cli.platform.PlatformContext;
import com.streamx.cli.platform.ProjectRepositoryApi;
import picocli.CommandLine;

@CommandLine.Command(
name = "remove",
header = "Disconnect the repository from a project",
description = "Only removes the connection; the Git repository itself is not touched."
)
public class RemoveCommand extends AbstractSilentCommand {

@CommandLine.Mixin
ProjectScopedOptions scope;

@Override
public CommandResult<Void> runCommand() {
PlatformContext.OrgProject context =
PlatformContext.orgAndProject(scope.orgId, scope.projectId);
try (PlatformClients client = PlatformClients.fromConfig()) {
new ProjectRepositoryApi(client).disconnect(context.org(), context.project());
} catch (PlatformClients.NotFoundException e) {
throw new CliException(msg.projectRepoNotConnected(context.project()), e);
}
System.out.println(msg.projectRepoRemoved(context.project()));
return new CommandResult<>(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.streamx.cli.commands.project.repo.set;

import static com.streamx.cli.i18n.MessageProvider.msg;

import com.streamx.cli.commands.project.repo.ProjectScopedOptions;
import com.streamx.cli.framework.AbstractSilentCommand;
import com.streamx.cli.framework.CommandResult;
import com.streamx.cli.platform.PlatformClients;
import com.streamx.cli.platform.PlatformContext;
import com.streamx.cli.platform.ProjectRepositoryApi;
import picocli.CommandLine;

@CommandLine.Command(
name = "set",
header = "Connect a repository to a project, or change its settings",
description = "Connects the repository when the project has none yet, otherwise updates "
+ "the connection. Use 'ssh-key set' first (or 'project create --ssh-private-key') "
+ "for private repositories."
)
public class SetCommand extends AbstractSilentCommand {

@CommandLine.Mixin
ProjectScopedOptions scope;

@CommandLine.Option(
names = "--uri",
required = true,
paramLabel = "<uri>",
description = "Git repository URI"
)
public String uri;

@CommandLine.Option(
names = "--branch",
required = true,
paramLabel = "<branch>",
description = "Git branch the platform deploys from"
)
public String branch;

@Override
public CommandResult<Void> runCommand() {
PlatformContext.OrgProject context =
PlatformContext.orgAndProject(scope.orgId, scope.projectId);
try (PlatformClients client = PlatformClients.fromConfig()) {
ProjectRepositoryApi api = new ProjectRepositoryApi(client);
if (repositoryExists(api, context)) {
api.update(context.org(), context.project(), uri, branch);
System.out.println(msg.projectRepoUpdated(context.project()));
} else {
api.connect(context.org(), context.project(), uri, branch);
System.out.println(msg.projectRepoConnected(context.project()));
}
}
return new CommandResult<>(null);
}

private static boolean repositoryExists(
ProjectRepositoryApi api, PlatformContext.OrgProject context) {
try {
api.get(context.org(), context.project());
return true;
} catch (PlatformClients.NotFoundException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.streamx.cli.commands.project.repo.sshkey;

import static com.streamx.cli.i18n.MessageProvider.msg;

import com.streamx.cli.framework.AbstractSilentCommand;
import com.streamx.cli.framework.CliException;
import com.streamx.cli.framework.CommandResult;
import com.streamx.cli.platform.OrgIdCompletionCandidates;
import com.streamx.cli.platform.PlatformClients;
import com.streamx.cli.platform.PlatformContext;
import com.streamx.cli.platform.ProjectRepositoryApi;
import com.streamx.cli.platform.generated.model.PrivatePublicKeyPair;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermissions;
import picocli.CommandLine;

@CommandLine.Command(
name = "generate",
header = "Generate a new SSH key pair server-side and save it to files",
description = "The private key is written to <file> (mode 600) and the public key to "
+ "<file>.pub; the keys are not printed and not stored on the platform. Pass the "
+ "private key to 'ssh-key set' or 'project create --ssh-private-key'; add the "
+ "public key to the Git hosting's deploy keys."
)
public class GenerateSshKeyCommand extends AbstractSilentCommand {

@CommandLine.Parameters(
index = "0",
paramLabel = "<file>",
description = "Where to save the private key; the public key goes to <file>.pub"
)
public Path keyFile;

@CommandLine.Option(
names = "--org",
paramLabel = "<orgId>",
description = "Organization ID (defaults to the current organization)",
completionCandidates = OrgIdCompletionCandidates.class
)
public String orgId;

@Override
public CommandResult<Void> runCommand() {
orgId = PlatformContext.requireOrg(orgId);
Path publicKeyFile = keyFile.resolveSibling(keyFile.getFileName() + ".pub");
requireAbsent(keyFile);
requireAbsent(publicKeyFile);

PrivatePublicKeyPair pair;
try (PlatformClients client = PlatformClients.fromConfig()) {
pair = new ProjectRepositoryApi(client).generateKeyPair(orgId);
}
writePrivateKey(keyFile, pair.getPrivateKey());
writePublicKey(publicKeyFile, pair.getPublicKey());
System.out.println(
msg.projectSshKeyPairWritten(keyFile.toString(), publicKeyFile.toString()));
return new CommandResult<>(null);
}

private static void requireAbsent(Path path) {
if (Files.exists(path)) {
throw new CliException(msg.projectSshKeyFileExists(path.toString()));
}
}

private static void writePrivateKey(Path path, String key) {
try {
try {
Files.createFile(path, PosixFilePermissions.asFileAttribute(
PosixFilePermissions.fromString("rw-------")));
} catch (UnsupportedOperationException nonPosix) {
Files.createFile(path);
}
Files.writeString(path, withTrailingNewline(key));
} catch (IOException e) {
throw new CliException(
msg.projectSshKeyFileWriteFailed(path.toString(), e.getMessage()), e);
}
}

private static void writePublicKey(Path path, String key) {
try {
Files.writeString(path, withTrailingNewline(key), StandardOpenOption.CREATE_NEW);
} catch (IOException e) {
throw new CliException(
msg.projectSshKeyFileWriteFailed(path.toString(), e.getMessage()), e);
}
}

private static String withTrailingNewline(String key) {
return key.endsWith("\n") ? key : key + "\n";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.streamx.cli.commands.project.repo.sshkey;

import static com.streamx.cli.i18n.MessageProvider.msg;

import com.streamx.cli.commands.project.repo.ProjectScopedOptions;
import com.streamx.cli.framework.AbstractSilentCommand;
import com.streamx.cli.framework.CliException;
import com.streamx.cli.framework.CommandResult;
import com.streamx.cli.platform.PlatformClients;
import com.streamx.cli.platform.PlatformContext;
import com.streamx.cli.platform.ProjectRepositoryApi;
import picocli.CommandLine;

@CommandLine.Command(
name = "remove",
header = "Remove the SSH deploy key from the repository connection"
)
public class RemoveSshKeyCommand extends AbstractSilentCommand {

@CommandLine.Mixin
ProjectScopedOptions scope;

@Override
public CommandResult<Void> runCommand() {
PlatformContext.OrgProject context =
PlatformContext.orgAndProject(scope.orgId, scope.projectId);
try (PlatformClients client = PlatformClients.fromConfig()) {
new ProjectRepositoryApi(client).removeSshKey(context.org(), context.project());
} catch (PlatformClients.NotFoundException e) {
throw new CliException(msg.projectSshKeyMissing(context.project()), e);
}
System.out.println(msg.projectSshKeyRemoved(context.project()));
return new CommandResult<>(null);
}
}
Loading
Loading