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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
ASTRA_TOKEN=your_astra_token_here

# Optional: Environment name, defaults to 'prod' if not set.
# Possible values: prod, dev, test
# Possible values: prod, dev, test, local
ASTRA_ENV=prod

# Required: Full Astra DB URL, example:
Expand Down
1 change: 1 addition & 0 deletions assets/AstraEnvironment.class.sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a18d6edf7a50ce022ca1b6624a9e0331f444aaf17914547e3c69c3c2f567b286
52 changes: 52 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import java.net.URLClassLoader
import java.util.zip.ZipFile
import java.security.MessageDigest

buildscript {
repositories {
Expand Down Expand Up @@ -401,3 +403,53 @@ tasks.register<Jar>("fatJar") {
configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
})
}

// -----------------------------------------------------------------
// AstraEnvironment classpath patch
//
// src/main/java/com/dtsx/astra/sdk/utils/AstraEnvironment.java shadows
// the upstream enum to add LOCAL support. The SHA below guards against
// the upstream class changing without us noticing.
//
// If the build fails with a SHA mismatch:
// 1. Check what changed in astra-sdk-devops
// 2. Update src/main/java/com/dtsx/astra/sdk/utils/AstraEnvironment.java
// 3. Run ./gradlew updateAstraEnvironmentPatchSha
// 4. Commit both files
// -----------------------------------------------------------------

fun astraEnvClassSha(): String {
val jar = configurations.runtimeClasspath.get().first { it.name.contains("astra-sdk-devops") }
val bytes = ZipFile(jar).use { zip ->
zip.getInputStream(zip.getEntry("com/dtsx/astra/sdk/utils/AstraEnvironment.class")).readBytes()
}
return MessageDigest.getInstance("SHA-256")
.digest(bytes).joinToString("") { "%02x".format(it) }
}

tasks.register("verifyAstraEnvironmentPatch") {
inputs.files(configurations.runtimeClasspath)
inputs.file("assets/AstraEnvironment.class.sha256")
doLast {
val expected = file("assets/AstraEnvironment.class.sha256").readText().trim()
val actual = astraEnvClassSha()
if (actual != expected)
throw GradleException(
"Upstream AstraEnvironment has changed (SHA mismatch).\n" +
"Update the patch then run: ./gradlew updateAstraEnvironmentPatchSha\n\n" +
"Expected: $expected\nActual: $actual"
)
}
}

tasks.register("updateAstraEnvironmentPatchSha") {
group = "build"
description = "Regenerates the AstraEnvironment patch SHA after an upstream change."
doLast {
val sha = astraEnvClassSha()
file("assets/AstraEnvironment.class.sha256").writeText(sha)
logger.lifecycle("SHA updated: $sha")
}
}

tasks.compileJava { dependsOn("verifyAstraEnvironmentPatch") }
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ record DefaultFile(ProfileName profile) implements ProfileSource {}
protected void prelude() {
super.prelude();
$connOpts = mergeConnectionOptions();

if (!ctx.properties().disableBetaWarnings() && profile().env().isLocal()) {
ctx.log().warn("Local environments are still in beta and may change without notice.");
}
}

private ConnectionOptions mergeConnectionOptions() {
Expand Down Expand Up @@ -85,7 +89,8 @@ private ProfileSource profileSource() {
}

if ($connOpts.$creds != null) {
return new FromArgs($connOpts.$creds.$token, $connOpts.$creds.$env.orElse(AstraEnvironment.PROD));
val env = AstraEnvironment.resolve($connOpts.$creds.$env, $connOpts.$creds.$localEndpoint);
return new FromArgs($connOpts.$creds.$token, env);
}

val defaultFilePath = AstraConfig.resolveDefaultAstraConfigFile(ctx);
Expand Down
29 changes: 21 additions & 8 deletions src/main/java/com/dtsx/astra/cli/commands/ConnectionOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import com.dtsx.astra.cli.core.config.ProfileName;
import com.dtsx.astra.cli.core.models.AstraToken;
import com.dtsx.astra.cli.core.properties.CliProperties.ConstEnvVars;
import com.dtsx.astra.sdk.utils.AstraEnvironment;

import lombok.NoArgsConstructor;
import lombok.val;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -46,19 +46,28 @@ public static class CredsSpec {
@Option(
names = { $Env.LONG },
completionCandidates = AstraEnvCompletion.class,
description = "Astra environment the token belongs to: prod (default), dev, or test. Leave unset unless you were issued a non-prod token.",
description = "Astra environment the token belongs to: prod (default), dev, test, or local. Leave unset unless you were issued a non-prod token.",
paramLabel = $Env.LABEL
)
public Optional<AstraEnvironment> $env;
public Optional<String> $env;

public CredsSpec(AstraToken $token, Optional<AstraEnvironment> $env) {
@Option(
names = { "--local-endpoint" },
description = "The endpoint URL for local Astra environments (required when --env local)",
paramLabel = "URL",
hidden = true
)
public Optional<String> $localEndpoint;

public CredsSpec(AstraToken $token, Optional<String> $env, Optional<String> $localEndpoint) {
this.$token = $token;
this.$env = $env;
this.$localEndpoint = $localEndpoint;
}

@SuppressWarnings("unused") // it may be marked as unused but used by picocli
@SuppressWarnings("unused") // it may be marked as unused but is used by picocli
public CredsSpec() {
this(null, Optional.empty());
this(null, Optional.empty(), Optional.empty());
}
}

Expand Down Expand Up @@ -121,9 +130,13 @@ public ConnectionOptions merge(ConnectionOptions other) {

val env = (other.$creds != null && other.$creds.$env != null && other.$creds.$env.isPresent())
? other.$creds.$env
: (this.$creds != null ? this.$creds.$env : Optional.<AstraEnvironment>empty());
: (this.$creds != null ? this.$creds.$env : Optional.<String>empty());

val localEndpoint = (other.$creds != null && other.$creds.$localEndpoint != null && other.$creds.$localEndpoint.isPresent())
? other.$creds.$localEndpoint
: (this.$creds != null ? this.$creds.$localEndpoint : Optional.<String>empty());

return new CredsSpec(token, env);
return new CredsSpec(token, env, localEndpoint);
}
return null;
}
Expand Down
37 changes: 29 additions & 8 deletions src/main/java/com/dtsx/astra/cli/commands/SetupCmd.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.dtsx.astra.cli.core.datatypes.NEList;
import com.dtsx.astra.cli.core.exceptions.AstraCliException;
import com.dtsx.astra.cli.core.exceptions.internal.cli.ExecutionCancelledException;
import com.dtsx.astra.cli.core.exceptions.internal.cli.OptionValidationException;
import com.dtsx.astra.cli.core.exceptions.internal.misc.InvalidTokenException;
import com.dtsx.astra.cli.core.help.Example;
import com.dtsx.astra.cli.core.models.AstraToken;
Expand Down Expand Up @@ -37,6 +38,7 @@
import static com.dtsx.astra.cli.core.output.ExitCode.UNSUPPORTED_EXECUTION;
import static com.dtsx.astra.cli.utils.StringUtils.NL;
import static com.dtsx.astra.cli.utils.StringUtils.trimIndent;
import static com.dtsx.astra.sdk.utils.AstraEnvironment.PROD;

@Command(
name = "setup",
Expand All @@ -60,11 +62,11 @@ public class SetupCmd extends AbstractCmd<SetupResult> {

@Option(
names = { $Env.LONG, $Env.SHORT },
description = "Astra environment the token belongs to: prod (default), dev, or test. Leave unset unless you were issued a non-prod token.",
description = "Astra environment the token belongs to: prod (default), dev, test, or local. Leave unset unless you were issued a non-prod token.",
completionCandidates = AstraEnvCompletion.class,
paramLabel = $Env.LABEL
)
public Optional<AstraEnvironment> $env;
public Optional<String> $env;

@Option(
names = { "--name" },
Expand Down Expand Up @@ -129,10 +131,10 @@ private OutputHuman handleSameProfileAlreadyExists(SameProfileAlreadyExists resu

private <T> T throwInvalidToken(Optional<AstraEnvironment> hint) {
if (hint.isPresent()) {
val currentEnvName = $env.orElse(AstraEnvironment.PROD).name().toLowerCase();
val currentEnvName = $env.orElse(PROD.name()).toLowerCase();
val validEnvName = hint.get().name().toLowerCase();

val fixAction = hint.get() == AstraEnvironment.PROD
val fixAction = hint.get() == PROD
? "drop @'!--env!@ (prod is the default) or pass @'!--env prod!@"
: "pass @'!--env " + validEnvName + "!@";

Expand Down Expand Up @@ -176,13 +178,32 @@ private OutputHuman showDocsLink() {

@Override
protected Operation<SetupResult> mkOperation() {
// TODO not sure if this should be here
if ($env.isPresent() && $env.get().equalsIgnoreCase("LOCAL")) {
throw new AstraCliException(UNSUPPORTED_EXECUTION, """
@|bold,red Error: LOCAL environments cannot be configured via interactive setup.|@

Please use @'!${cli.name} config create!@ with @'!--env local!@ and @'!--local-endpoint!@ to create a LOCAL profile.
""", List.of(
new Hint("Create a LOCAL profile", "${cli.name} config create <name> --token <token> --env local --local-endpoint <url>")
));
}

val resolvedEnv = $env.map(envStr -> {
try {
return AstraEnvironment.valueOf(envStr);
} catch (IllegalArgumentException e) {
throw new OptionValidationException("env", "Invalid environment: '" + envStr + "'. Expected one of: " + String.join(", ", AstraEnvironment.allValuesLower()));
}
});

return new SetupOperation(
ctx,
ctx.gateways()::mkOrgGateway,
ctx.gateways().mkOrgGatewayStateless(),
new SetupRequest(
$token,
$env,
resolvedEnv,
$name,
this::assertShouldSetup,
this::promptForNextActionIfExistingUser,
Expand Down Expand Up @@ -213,7 +234,7 @@ private String mkArgsAddendum() {
$name.map(n -> "Profile '" + n + "'").orElse("The profile"),
Stream.concat(
$token.map(t -> " with token " + t).stream(),
$env.map(e -> " in env '" + e.name().toLowerCase() + "'").stream()
$env.map(e -> " in env '" + e.toLowerCase() + "'").stream()
).collect(Collectors.joining(""))
);
}
Expand Down Expand Up @@ -316,12 +337,12 @@ private AstraEnvironment promptForEnv(AstraEnvironment defaultEnv) {
.defaultOption(defaultEnv)
.mapper(e -> e.name().toLowerCase())
.fallbackFlag("--env")
.fix(originalArgs(), "--env <prod|test|dev>")
.fix(originalArgs(), "--env <" + String.join("|", AstraEnvironment.allValuesLower()) + ">")
.dontClearAfterSelection();
}

private ProfileName promptForName(String defaultName, AstraEnvironment env) {
val envAddendum = (env != AstraEnvironment.PROD)
val envAddendum = (env != PROD)
? " " + ctx.highlight(env.name().toLowerCase())
: "";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import static com.dtsx.astra.cli.utils.CollectionUtils.sequencedMapOf;
import static com.dtsx.astra.cli.utils.StringUtils.NL;
import static com.dtsx.astra.cli.utils.StringUtils.trimIndent;
import static com.dtsx.astra.sdk.utils.AstraEnvironment.PROD;

@Command(
name = "create",
Expand Down Expand Up @@ -74,12 +75,20 @@ public class ConfigCreateCmd extends AbstractConfigCmd<ConfigCreateResult> {

@Option(
names = { $Env.LONG, $Env.SHORT },
description = "Astra environment the token belongs to: prod (default), dev, or test. Leave unset unless you were issued a non-prod token.",
description = "Astra environment the token belongs to: prod (default), dev, test, or local. Leave unset unless you were issued a non-prod token.",
completionCandidates = AstraEnvCompletion.class,
defaultValue = $Env.DEFAULT,
paramLabel = $Env.LABEL
)
public AstraEnvironment $env;
public Optional<String> $env;

@Option(
names = { "--local-endpoint" },
description = "The endpoint URL for local Astra environments (required when --env local)",
paramLabel = "URL",
hidden = true
)
public Optional<String> $localEndpoint;

@Option(
names = { "-d", "--default" },
Expand All @@ -94,6 +103,15 @@ public class ConfigCreateCmd extends AbstractConfigCmd<ConfigCreateResult> {
)
private Optional<Boolean> $overwrite;

@Option(
names = { "--validate" },
description = "Validate the token by making a request to the Astra",
defaultValue = "true",
fallbackValue = "true",
negatable = true
)
public boolean $validate;

@Override
public final OutputAll execute(Supplier<ConfigCreateResult> resultSupplier) {
val result = resultSupplier.get();
Expand All @@ -103,6 +121,7 @@ public final OutputAll execute(Supplier<ConfigCreateResult> resultSupplier) {
case ProfileIllegallyExists(var profileName) -> throwProfileAlreadyExists(profileName);
case ViolatedFailIfExists() -> throwAttemptedToSetDefault();
case InvalidToken(var hint) -> throwInvalidToken(hint);
case NameRequiredIfNotValidated _ -> throwNameRequiredIfNotValidated();
};
}

Expand Down Expand Up @@ -168,10 +187,10 @@ private <T> T throwAttemptedToSetDefault() {

private <T> T throwInvalidToken(Optional<AstraEnvironment> hint) {
if (hint.isPresent()) {
val currentEnvName = $env.name().toLowerCase();
val currentEnvName = $env.orElse(PROD.name()).toLowerCase();
val validEnvName = hint.get().name().toLowerCase();

val fixAction = hint.get() == AstraEnvironment.PROD
val fixAction = hint.get() == PROD
? "drop @'!--env!@ (prod is the default) or pass @'!--env prod!@"
: "pass @'!--env " + validEnvName + "!@";

Expand All @@ -191,14 +210,24 @@ private <T> T throwInvalidToken(Optional<AstraEnvironment> hint) {
""");
}

private <T> T throwNameRequiredIfNotValidated() {
throw new AstraCliException(VALIDATION_ISSUE, """
@|bold,red An explicit profile name must be provided if @|italic --validate=false|@.|@
""");
}

@Override
public Operation<ConfigCreateResult> mkOperation() {
return new ConfigCreateOperation(ctx, config(true), ctx.gateways().mkOrgGateway($token, $env), ctx.gateways().mkOrgGatewayStateless(), new CreateConfigRequest(
val env = AstraEnvironment.resolve($env, $localEndpoint);

return new ConfigCreateOperation(ctx, config(true), ctx.gateways().mkOrgGateway($token, env), ctx.gateways().mkOrgGatewayStateless(), new CreateConfigRequest(
$profileName,
$token,
$env,
env,
$overwrite,
$setDefault,
$validate,
$localEndpoint,
this::assertCanOverwriteProfile
));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import com.dtsx.astra.cli.core.completions.StaticCompletion;
import com.dtsx.astra.sdk.utils.AstraEnvironment;

import java.util.Arrays;
import java.util.List;

public class AstraEnvCompletion extends StaticCompletion {
public AstraEnvCompletion() {
super(Arrays.stream(AstraEnvironment.values()).map(Enum::name).map(String::toLowerCase).toList());
super(List.of(AstraEnvironment.allValuesLower()));
}
}
Loading