Skip to content
Draft
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
Binary file added assets/astra-db-java.jar
Binary file not shown.
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ dependencies {
implementation("org.slf4j:slf4j-nop:2.0.17")

// for underlying api calls
implementation("com.datastax.astra:astra-db-java:2.3.1")
// implementation("com.datastax.astra:astra-db-java:2.3.1")
implementation("com.datastax.astra:astra-sdk-devops:2.3.1")
implementation(files("./assets/astra-db-java.jar"))

// unzip downloaded external programs (cqlsh, dsbulk, etc.)
implementation("org.apache.commons:commons-compress:1.28.0")
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/dtsx/astra/cli/commands/db/DbCmd.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dtsx.astra.cli.commands.db;

import com.dtsx.astra.cli.commands.db.clone.DbCloneCmd;

import com.dtsx.astra.cli.commands.db.cdc.CdcCreateCmd;
import com.dtsx.astra.cli.commands.db.cdc.CdcDeleteCmd;
import com.dtsx.astra.cli.commands.db.cdc.CdcListCmd;
Expand Down Expand Up @@ -38,6 +40,7 @@
DbCreateCmd.class,
DbDeleteCmd.class,
DbStatusCmd.class,
DbCloneCmd.class,
CqlshCmd.class,
DataAPICmd.class,
DbDsbulkCmd.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.dtsx.astra.cli.commands.db.clone;

import com.dtsx.astra.cli.commands.db.AbstractPromptForDbCmd;
import com.dtsx.astra.cli.core.completions.caches.DbCompletionsCache;
import com.dtsx.astra.cli.gateways.db.clone.DbCloneGateway;
import org.jetbrains.annotations.MustBeInvokedByOverriders;

public abstract class AbstractDbCloneCmd<OpRes> extends AbstractPromptForDbCmd<OpRes> {
protected DbCloneGateway dbCloneGateway;

@Override
@MustBeInvokedByOverriders
protected void prelude() {
super.prelude();
dbCloneGateway = ctx.gateways().mkDbCloneGateway(profile().token(), profile().env(), new DbCompletionsCache(ctx, profileAndSource()));

if (!ctx.properties().disableBetaWarnings()) {
ctx.log().warn("${cli.name} db clone commands are still in beta and may change without notice.");
}
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/dtsx/astra/cli/commands/db/clone/DbCloneCmd.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.dtsx.astra.cli.commands.db.clone;

import com.dtsx.astra.cli.core.mixins.HelpMixin;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;

@Command(
name = "clone",
description = "Manage database cloning and snapshots",
subcommands = {
DbCloneStartCmd.class,
DbCloneStatusCmd.class,
DbCloneListSnapshotsCmd.class,
}
)
public class DbCloneCmd {
@Mixin
public HelpMixin help;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.dtsx.astra.cli.commands.db.clone;

import com.dtsx.astra.cli.core.help.Example;
import com.dtsx.astra.cli.core.output.formats.OutputAll;
import com.dtsx.astra.cli.core.output.formats.OutputJson;
import com.dtsx.astra.cli.core.output.table.ShellTable;
import com.dtsx.astra.cli.operations.Operation;
import com.dtsx.astra.cli.operations.db.clone.DbCloneListSnapshotsOperation;
import com.dtsx.astra.cli.operations.db.clone.DbCloneListSnapshotsOperation.DbCloneListSnapshotsRequest;
import com.dtsx.astra.sdk.db.domain.DatabaseSnapshot;
import lombok.val;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;

import static com.dtsx.astra.cli.utils.CollectionUtils.sequencedMapOf;

@Command(
name = "list-snapshots",
description = "List available snapshots for a database that can be used for cloning"
)
@Example(
comment = "List available snapshots for a database interactively",
command = "${cli.name} db clone list-snapshots"
)
@Example(
comment = "List available snapshots for a specific database",
command = "${cli.name} db clone list-snapshots my_db"
)
public class DbCloneListSnapshotsCmd extends AbstractDbCloneCmd<Stream<DatabaseSnapshot>> {
@Option(
names = { "-r", "--region" },
description = "Filter snapshots by a specific region",
paramLabel = "REGION"
)
public Optional<String> $region = Optional.empty();

@Option(
names = { "--from" },
description = "Start date for snapshot filtering (e.g. 2023-01-01)",
paramLabel = "START_DATE"
)
public Optional<String> $from = Optional.empty(); // TODO see if I can use instants or something

@Option(
names = { "--to" },
description = "End date for snapshot filtering (e.g. 2023-12-31)",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution: in the querystring, the only format that won't give an Invalid to/from value error is the full ISO date string all the way to seconds and the trailing Z: 2028-01-01T01:02Z , 2028-01-01T01:02.987Z are the only forms I managed to successfully use.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I need to create a custom Instant parser for this

paramLabel = "END_DATE"
)
public Optional<String> $to = Optional.empty();

@Override
protected OutputJson executeJson(Supplier<Stream<DatabaseSnapshot>> result) {
return OutputJson.serializeValue(result);
}

@Override
protected OutputAll execute(Supplier<Stream<DatabaseSnapshot>> result) {
val items = result.get()
.map(s -> sequencedMapOf(
"ID", s.getId(),
"Time", s.getTime()
))
.toList();

return new ShellTable(items).withColumns("ID", "Time");
}

@Override
protected Operation<Stream<DatabaseSnapshot>> mkOperation() {
return new DbCloneListSnapshotsOperation(dbCloneGateway, new DbCloneListSnapshotsRequest($dbRef, $region, $from, $to));
}

@Override
protected String dbRefPrompt() {
return "Select the source database to list snapshots for";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.dtsx.astra.cli.commands.db.clone;

import com.dtsx.astra.cli.core.CliConstants.$Db;
import com.dtsx.astra.cli.core.CliConstants.$Regions;
import com.dtsx.astra.cli.core.help.Example;
import com.dtsx.astra.cli.core.mixins.LongRunningOptionsMixin;
import com.dtsx.astra.cli.core.mixins.LongRunningOptionsMixin.WithSetTimeout;
import com.dtsx.astra.cli.core.models.DbRef;
import com.dtsx.astra.cli.core.output.Hint;
import com.dtsx.astra.cli.core.output.formats.OutputAll;
import com.dtsx.astra.cli.core.output.formats.OutputJson;
import com.dtsx.astra.cli.core.output.prompters.specific.DbRefPrompter;
import com.dtsx.astra.cli.core.output.prompters.specific.SnapshotPrompter;
import com.dtsx.astra.cli.operations.Operation;
import com.dtsx.astra.cli.operations.db.clone.DbCloneStartOperation;
import com.dtsx.astra.cli.operations.db.clone.DbCloneStartOperation.CloneCompleted;
import com.dtsx.astra.cli.operations.db.clone.DbCloneStartOperation.CloneStarted;
import com.dtsx.astra.cli.operations.db.clone.DbCloneStartOperation.DbCloneStartRequest;
import com.dtsx.astra.cli.operations.db.clone.DbCloneStartOperation.DbCloneStartResult;
import com.dtsx.astra.sdk.db.domain.DatabaseCloneStatus;
import lombok.val;
import org.jetbrains.annotations.MustBeInvokedByOverriders;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;

import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;

import static com.dtsx.astra.cli.core.mixins.LongRunningOptionsMixin.LR_OPTS_TIMEOUT_NAME;
import static com.dtsx.astra.cli.utils.CollectionUtils.sequencedMapOf;

@Command(
name = "start",
aliases = { "create" },
description = "Start a clone operation from an existing database snapshot"
)
@Example(
comment = "Start and wait for a clone op interactively",
command = "${cli.name} db clone start"
)
@Example( // TODO probably use --from/--to or --source/--target or something
comment = "Start and wait for a clone op with explicit arguments",
command = "${cli.name} db clone start my_new_db -sd my_old_db -s snap123"
)
@Example(
comment = "Start a clone without waiting for it to become active",
command = "${cli.name} db clone start my_new_db -sd my_old_db -s snap123 --async"
)
@Example(
comment = "Start a clone from a source database in a specific region",
command = "${cli.name} db clone start my_new_db -sd my_old_db -s snap123 -sr us-east1"
)
public class DbCloneStartCmd extends AbstractDbCloneCmd<DbCloneStartResult> implements WithSetTimeout {
@Option(
names = { "-sd", "--source-db" },
description = "The source database to clone from",
paramLabel = $Db.LABEL
)
public DbRef $sourceDbRef;

@Option(
names = { "-s", "--snapshot-id" },
description = "The snapshot ID to clone from",
paramLabel = "SNAPSHOT_ID"
)
public String $snapshotId;

@Option(
names = { "-sr", "--source-region" },
description = "The region of the source database",
paramLabel = $Regions.LABEL
)
public Optional<String> $sourceRegion = Optional.empty();

@Mixin
protected LongRunningOptionsMixin lrMixin;

@Option(
names = LR_OPTS_TIMEOUT_NAME,
description = "Timeout for the clone operation to complete",
defaultValue = "30m"
)
public void setTimeout(Duration timeout) {
lrMixin.setTimeout(timeout);
}

@Override
@MustBeInvokedByOverriders
protected void prelude() {
super.prelude();

if ($sourceDbRef == null) {
$sourceDbRef = DbRefPrompter.prompt(ctx, dbGateway, "Select the source database to clone from:", (b) -> b.fallbackIndex(0).fix(originalArgs(), "--source-db"));
}

if ($snapshotId == null) {
$snapshotId = SnapshotPrompter.prompt(ctx, dbCloneGateway, $sourceDbRef, "Select the snapshot to clone from:", (b) -> b.fallbackIndex(0).fix(originalArgs(), "--snapshot-id"));
}
}

@Override
protected OutputJson executeJson(Supplier<DbCloneStartResult> result) {
return switch (result.get()) {
case CloneStarted(var status) -> OutputJson.serializeValue(mkData(status, null));
case CloneCompleted(var status, var waitTime) -> OutputJson.serializeValue(mkData(status, waitTime));
};
}

@Override
protected OutputAll execute(Supplier<DbCloneStartResult> result) {
return switch (result.get()) {
case CloneStarted(var status) -> {
val msg = "Clone operation started with ID %s.".formatted(
ctx.highlight(status.getOperationId())
);
yield OutputAll.response(msg, mkData(status, null), List.of(
new Hint("Check clone status", originalArgs(), "--operation-id " + status.getOperationId())
));
}
case CloneCompleted(var status, var waitTime) -> {
val msg = "Clone operation completed after waiting %s seconds.".formatted(
ctx.highlight(waitTime.toSeconds())
);
yield OutputAll.response(msg, mkData(status, waitTime), List.of(
new Hint("Get database info", "${cli.name} db get %s".formatted($dbRef))
));
}
};
}

@Override
protected Operation<DbCloneStartResult> mkOperation() {
return new DbCloneStartOperation(dbCloneGateway, new DbCloneStartRequest(
$dbRef, $sourceDbRef, $snapshotId, $sourceRegion, lrMixin.options(ctx)
));
}

@Override
protected String dbRefPrompt() {
return "Select the target database";
}

private LinkedHashMap<String, Object> mkData(DatabaseCloneStatus status, Duration waitedDuration) {
return sequencedMapOf(
"operationId", status.getOperationId(),
"status", status.getStatus(),
"phase", status.getPhase(),
"sourceDbId", status.getSourceDbId(),
"targetDbId", status.getTargetDbId(),
"snapshotId", status.getSnapshotId(),
"waitedSeconds", Optional.ofNullable(waitedDuration).map(Duration::getSeconds) // TODO check if data is all correct and complete
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.dtsx.astra.cli.commands.db.clone;

import com.dtsx.astra.cli.core.help.Example;
import com.dtsx.astra.cli.core.models.CloneOperationId;
import com.dtsx.astra.cli.core.output.formats.OutputAll;
import com.dtsx.astra.cli.core.output.formats.OutputJson;
import com.dtsx.astra.cli.core.output.table.ShellTable;
import com.dtsx.astra.cli.operations.Operation;
import com.dtsx.astra.cli.operations.db.clone.DbCloneStatusOperation;
import com.dtsx.astra.cli.operations.db.clone.DbCloneStatusOperation.DbCloneStatusRequest;
import com.dtsx.astra.sdk.db.domain.DatabaseCloneStatus;
import lombok.val;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

import java.util.function.Supplier;

import static com.dtsx.astra.cli.utils.CollectionUtils.sequencedMapOf;

@Command(
name = "status",
description = "Check the status of an ongoing or completed clone operation"
)
@Example(
comment = "Check the status of a specific clone operation",
command = "${cli.name} db clone status my_db -id 202d0d1b-14cf-43dc-89fe-efb23758ccef"
)
public class DbCloneStatusCmd extends AbstractDbCloneCmd<DatabaseCloneStatus> {
@Option(
names = { "-id", "--operation-id" },
description = "The ID of the clone operation",
paramLabel = "OPERATION_ID",
required = true
)
public CloneOperationId $operationId;

@Override
protected OutputJson executeJson(Supplier<DatabaseCloneStatus> result) {
return OutputJson.serializeValue(result);
}

@Override
protected OutputAll execute(Supplier<DatabaseCloneStatus> result) {
val status = result.get();
return ShellTable.forAttributes(sequencedMapOf(
"Operation ID", status.getOperationId(),
"Status", status.getStatus(),
"Source DB", status.getSourceDbId(),
"Target DB", status.getTargetDbId(),
"Snapshot ID", status.getSnapshotId(),
"Phase", status.getPhase(),
"Message", status.getMessage()
));
}

@Override
protected Operation<DatabaseCloneStatus> mkOperation() {
return new DbCloneStatusOperation(dbCloneGateway, new DbCloneStatusRequest($dbRef, $operationId));
}

@Override
protected String dbRefPrompt() {
return "Select the target database for the clone operation";
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dtsx.astra.cli.commands.token;

import com.dtsx.astra.cli.commands.AbstractConnectedCmd;
import com.dtsx.astra.cli.core.completions.caches.RoleCompletionsCache;
import com.dtsx.astra.cli.gateways.token.TokenGateway;
import org.jetbrains.annotations.MustBeInvokedByOverriders;

Expand All @@ -11,6 +12,6 @@ public abstract class AbstractTokenCmd<OpRes> extends AbstractConnectedCmd<OpRes
@MustBeInvokedByOverriders
protected void prelude() {
super.prelude();
tokenGateway = ctx.gateways().mkTokenGateway(profile().token(), profile().env());
tokenGateway = ctx.gateways().mkTokenGateway(profile().token(), profile().env(), new RoleCompletionsCache(ctx));
}
}
1 change: 1 addition & 0 deletions src/main/java/com/dtsx/astra/cli/core/TypeConverters.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public static List<? extends ContextualTypeConverter> mkInstances(Ref<CliContext
new ContextlessTypeConverter(TenantName.class, TenantName::parse),
new ContextlessTypeConverter(UserRef.class, UserRef::parse),
new ContextlessTypeConverter(ProfileName.class, ProfileName::parse),
new ContextlessTypeConverter(CloneOperationId.class, CloneOperationId::parse),
new ContextlessTypeConverter(Version.class, Version::parse),
new ContextlessTypeConverter(Duration.class, Misc::parseDuration),
new ContextualTypeConverter(Path.class, Misc::parsePath, ctxRef)
Expand Down
Loading