From 51a86b3e763e9eed3ec938c4f60c57528f2730d3 Mon Sep 17 00:00:00 2001 From: toptobes Date: Fri, 24 Jul 2026 00:03:51 -0500 Subject: [PATCH 1/4] a --- .env.example | 2 +- assets/AstraEnvironment.class.sha256 | 1 + build.gradle.kts | 52 ++++++++++++++ .../astra/cli/commands/ConnectionOptions.java | 2 +- .../com/dtsx/astra/cli/commands/SetupCmd.java | 4 +- .../cli/commands/config/ConfigCreateCmd.java | 2 +- .../completions/impls/AstraEnvCompletion.java | 4 +- .../astra/cli/core/config/AstraConfig.java | 2 +- .../astra/cli/gateways/APIProviderImpl.java | 19 +++-- .../astra/sdk/utils/AstraEnvironment.java | 70 +++++++++++++++++++ 10 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 assets/AstraEnvironment.class.sha256 create mode 100644 src/main/java/com/dtsx/astra/sdk/utils/AstraEnvironment.java diff --git a/.env.example b/.env.example index d5689243..df3e953f 100644 --- a/.env.example +++ b/.env.example @@ -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: diff --git a/assets/AstraEnvironment.class.sha256 b/assets/AstraEnvironment.class.sha256 new file mode 100644 index 00000000..b7f3cdcf --- /dev/null +++ b/assets/AstraEnvironment.class.sha256 @@ -0,0 +1 @@ +a18d6edf7a50ce022ca1b6624a9e0331f444aaf17914547e3c69c3c2f567b286 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index a0dd4604..1b1a4b75 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,6 @@ import java.net.URLClassLoader +import java.util.zip.ZipFile +import java.security.MessageDigest buildscript { repositories { @@ -401,3 +403,53 @@ tasks.register("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") } diff --git a/src/main/java/com/dtsx/astra/cli/commands/ConnectionOptions.java b/src/main/java/com/dtsx/astra/cli/commands/ConnectionOptions.java index b2f8d2f8..da3f28cc 100644 --- a/src/main/java/com/dtsx/astra/cli/commands/ConnectionOptions.java +++ b/src/main/java/com/dtsx/astra/cli/commands/ConnectionOptions.java @@ -46,7 +46,7 @@ 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 $env; diff --git a/src/main/java/com/dtsx/astra/cli/commands/SetupCmd.java b/src/main/java/com/dtsx/astra/cli/commands/SetupCmd.java index 2929e224..1a3efdb9 100644 --- a/src/main/java/com/dtsx/astra/cli/commands/SetupCmd.java +++ b/src/main/java/com/dtsx/astra/cli/commands/SetupCmd.java @@ -60,7 +60,7 @@ public class SetupCmd extends AbstractCmd { @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 ) @@ -316,7 +316,7 @@ private AstraEnvironment promptForEnv(AstraEnvironment defaultEnv) { .defaultOption(defaultEnv) .mapper(e -> e.name().toLowerCase()) .fallbackFlag("--env") - .fix(originalArgs(), "--env ") + .fix(originalArgs(), "--env <" + String.join("|", AstraEnvironment.allValuesLower()) + ">") .dontClearAfterSelection(); } diff --git a/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java b/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java index 1f5929b7..c9c4c607 100644 --- a/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java +++ b/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java @@ -74,7 +74,7 @@ public class ConfigCreateCmd extends AbstractConfigCmd { @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 diff --git a/src/main/java/com/dtsx/astra/cli/core/completions/impls/AstraEnvCompletion.java b/src/main/java/com/dtsx/astra/cli/core/completions/impls/AstraEnvCompletion.java index afdb7751..d6a67321 100644 --- a/src/main/java/com/dtsx/astra/cli/core/completions/impls/AstraEnvCompletion.java +++ b/src/main/java/com/dtsx/astra/cli/core/completions/impls/AstraEnvCompletion.java @@ -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())); } } diff --git a/src/main/java/com/dtsx/astra/cli/core/config/AstraConfig.java b/src/main/java/com/dtsx/astra/cli/core/config/AstraConfig.java index 851c8a92..a0f51643 100644 --- a/src/main/java/com/dtsx/astra/cli/core/config/AstraConfig.java +++ b/src/main/java/com/dtsx/astra/cli/core/config/AstraConfig.java @@ -141,7 +141,7 @@ private static Either mkProfileFromSection(CliContext c ); } catch (IllegalArgumentException e) { return Either.left( - new InvalidProfile(section, "Error parsing " + ctx.colors().PURPLE_300.useOrQuote(ENV_KEY) + ": Got '" + rawEnv + "', expected one of (prod|dev|test)") + new InvalidProfile(section, "Error parsing " + ctx.colors().PURPLE_300.useOrQuote(ENV_KEY) + ": Got '" + rawEnv + "', expected one of (" + String.join("|", AstraEnvironment.allValuesLower()) + ")") ); } }); diff --git a/src/main/java/com/dtsx/astra/cli/gateways/APIProviderImpl.java b/src/main/java/com/dtsx/astra/cli/gateways/APIProviderImpl.java index faf04477..54b95856 100644 --- a/src/main/java/com/dtsx/astra/cli/gateways/APIProviderImpl.java +++ b/src/main/java/com/dtsx/astra/cli/gateways/APIProviderImpl.java @@ -30,6 +30,7 @@ import java.util.Optional; import java.util.UUID; +import static com.dtsx.astra.cli.core.output.ExitCode.ILLEGAL_OPERATION; import static com.dtsx.astra.cli.core.output.ExitCode.UNIQUENESS_ISSUE; @RequiredArgsConstructor @@ -76,14 +77,22 @@ public String apiEndpoint(DbRef dbRef, AstraEnvironment env) { } private DataAPIClient dataApiClient() { - val destination = switch (env) { - case PROD -> DataAPIDestination.ASTRA; - case DEV -> DataAPIDestination.ASTRA_DEV; - case TEST -> DataAPIDestination.ASTRA_TEST; - }; + val destination = + (env == AstraEnvironment.PROD) + ? DataAPIDestination.ASTRA : + (env == AstraEnvironment.DEV) + ? DataAPIDestination.ASTRA_DEV : + (env == AstraEnvironment.TEST) + ? DataAPIDestination.ASTRA_TEST + : throwCantUseLocalWithDataApi(); + return new DataAPIClient(token.unsafeUnwrap(), new DataAPIClientOptions().destination(destination)); } + private DataAPIDestination throwCantUseLocalWithDataApi() { + throw new AstraCliException(ILLEGAL_OPERATION, "Local environments are not supported for Data API operations. Please use a prod, dev, or test Astra environment."); + } + private UUID resolveDbId(DbRef ref) { val cachedId = dbCache.lookupDbId(ref); diff --git a/src/main/java/com/dtsx/astra/sdk/utils/AstraEnvironment.java b/src/main/java/com/dtsx/astra/sdk/utils/AstraEnvironment.java new file mode 100644 index 00000000..1472f717 --- /dev/null +++ b/src/main/java/com/dtsx/astra/sdk/utils/AstraEnvironment.java @@ -0,0 +1,70 @@ +package com.dtsx.astra.sdk.utils; + +import lombok.*; +import lombok.experimental.Accessors; + +@Getter +@Accessors(fluent = false) +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode(of = "name") +public final class AstraEnvironment { + public static final AstraEnvironment PROD = new AstraEnvironment( + 0, + "PROD", + "https://api.astra.datastax.com/v2", + ".apps.astra.datastax.com", + ".api.streaming.datastax.com" + ); + + public static final AstraEnvironment DEV = new AstraEnvironment( + 1, + "DEV", + "https://api.dev.cloud.datastax.com/v2", + ".apps.astra-dev.datastax.com", + ".api.dev.streaming.datastax.com" + ); + + public static final AstraEnvironment TEST = new AstraEnvironment( + 2, + "TEST", + "https://api.test.cloud.datastax.com/v2", + ".apps.astra-test.datastax.com", + ".api.staging.streaming.datastax.com" + ); + + @Accessors(fluent = true) + private final int ordinal; + + @Accessors(fluent = true) + private final String name; + + private final String endPoint; + private final String appsSuffix; + private final String streamingV3Suffix; + + public static AstraEnvironment local(String endpoint) { + return new AstraEnvironment(-1, "LOCAL", endpoint, "", ""); + } + + public static AstraEnvironment[] values() { + return new AstraEnvironment[]{ PROD, DEV, TEST }; + } + + public static String[] allValuesLower() { + return new String[]{ PROD.name.toLowerCase(), DEV.name.toLowerCase(), TEST.name.toLowerCase(), "local" }; + } + + public static AstraEnvironment valueOf(String name) { + for (val env : values()) { + if (env.name.equals(name)) { + return env; + } + } + throw new IllegalArgumentException("No AstraEnvironment constant with name: " + name); + } + + @Override + public String toString() { + return name; + } +} From 643ff0cd900fb21fdd2d84363c16b6ff91c03f8b Mon Sep 17 00:00:00 2001 From: toptobes Date: Fri, 24 Jul 2026 01:32:33 -0500 Subject: [PATCH 2/4] b --- .../cli/commands/AbstractConnectedCmd.java | 7 ++- .../astra/cli/commands/ConnectionOptions.java | 27 +++++--- .../com/dtsx/astra/cli/commands/SetupCmd.java | 33 ++++++++-- .../cli/commands/config/ConfigCreateCmd.java | 22 +++++-- .../astra/cli/core/config/AstraConfig.java | 63 ++++++++++++++----- .../config/ConfigCreateOperation.java | 5 +- .../astra/sdk/utils/AstraEnvironment.java | 35 ++++++++++- .../dtsx/astra/cli/testlib/TestConfig.java | 2 +- 8 files changed, 152 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/dtsx/astra/cli/commands/AbstractConnectedCmd.java b/src/main/java/com/dtsx/astra/cli/commands/AbstractConnectedCmd.java index 225a7419..0abd2041 100644 --- a/src/main/java/com/dtsx/astra/cli/commands/AbstractConnectedCmd.java +++ b/src/main/java/com/dtsx/astra/cli/commands/AbstractConnectedCmd.java @@ -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() { @@ -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); diff --git a/src/main/java/com/dtsx/astra/cli/commands/ConnectionOptions.java b/src/main/java/com/dtsx/astra/cli/commands/ConnectionOptions.java index da3f28cc..5e946dbc 100644 --- a/src/main/java/com/dtsx/astra/cli/commands/ConnectionOptions.java +++ b/src/main/java/com/dtsx/astra/cli/commands/ConnectionOptions.java @@ -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; @@ -49,16 +49,25 @@ public static class CredsSpec { 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 $env; + public Optional $env; - public CredsSpec(AstraToken $token, Optional $env) { + @Option( + names = { "--local-endpoint" }, + description = "The endpoint URL for local Astra environments (required when --env local)", + paramLabel = "URL", + hidden = true + ) + public Optional $localEndpoint; + + public CredsSpec(AstraToken $token, Optional $env, Optional $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()); } } @@ -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.empty()); + : (this.$creds != null ? this.$creds.$env : Optional.empty()); + + val localEndpoint = (other.$creds != null && other.$creds.$localEndpoint != null && other.$creds.$localEndpoint.isPresent()) + ? other.$creds.$localEndpoint + : (this.$creds != null ? this.$creds.$localEndpoint : Optional.empty()); - return new CredsSpec(token, env); + return new CredsSpec(token, env, localEndpoint); } return null; } diff --git a/src/main/java/com/dtsx/astra/cli/commands/SetupCmd.java b/src/main/java/com/dtsx/astra/cli/commands/SetupCmd.java index 1a3efdb9..c99b45c6 100644 --- a/src/main/java/com/dtsx/astra/cli/commands/SetupCmd.java +++ b/src/main/java/com/dtsx/astra/cli/commands/SetupCmd.java @@ -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; @@ -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", @@ -64,7 +66,7 @@ public class SetupCmd extends AbstractCmd { completionCandidates = AstraEnvCompletion.class, paramLabel = $Env.LABEL ) - public Optional $env; + public Optional $env; @Option( names = { "--name" }, @@ -129,10 +131,10 @@ private OutputHuman handleSameProfileAlreadyExists(SameProfileAlreadyExists resu private T throwInvalidToken(Optional 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 + "!@"; @@ -176,13 +178,32 @@ private OutputHuman showDocsLink() { @Override protected Operation 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 --token --env local --local-endpoint ") + )); + } + + 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, @@ -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("")) ); } @@ -321,7 +342,7 @@ private AstraEnvironment promptForEnv(AstraEnvironment defaultEnv) { } private ProfileName promptForName(String defaultName, AstraEnvironment env) { - val envAddendum = (env != AstraEnvironment.PROD) + val envAddendum = (env != PROD) ? " " + ctx.highlight(env.name().toLowerCase()) : ""; diff --git a/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java b/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java index c9c4c607..86e749ab 100644 --- a/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java +++ b/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java @@ -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", @@ -79,7 +80,15 @@ public class ConfigCreateCmd extends AbstractConfigCmd { defaultValue = $Env.DEFAULT, paramLabel = $Env.LABEL ) - public AstraEnvironment $env; + public Optional $env; + + @Option( + names = { "--local-endpoint" }, + description = "The endpoint URL for local Astra environments (required when --env local)", + paramLabel = "URL", + hidden = true + ) + public Optional $localEndpoint; @Option( names = { "-d", "--default" }, @@ -168,10 +177,10 @@ private T throwAttemptedToSetDefault() { private T throwInvalidToken(Optional 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 + "!@"; @@ -193,12 +202,15 @@ private T throwInvalidToken(Optional hint) { @Override public Operation 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, + $localEndpoint, this::assertCanOverwriteProfile )); } diff --git a/src/main/java/com/dtsx/astra/cli/core/config/AstraConfig.java b/src/main/java/com/dtsx/astra/cli/core/config/AstraConfig.java index a0f51643..fe38e153 100644 --- a/src/main/java/com/dtsx/astra/cli/core/config/AstraConfig.java +++ b/src/main/java/com/dtsx/astra/cli/core/config/AstraConfig.java @@ -34,11 +34,14 @@ import static com.dtsx.astra.cli.core.output.ExitCode.FILE_ISSUE; import static com.dtsx.astra.cli.utils.StringUtils.trimIndent; +import static com.dtsx.astra.sdk.utils.AstraEnvironment.LOCAL_NAME; +import static com.dtsx.astra.sdk.utils.AstraEnvironment.PROD; @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class AstraConfig { public static final String TOKEN_KEY = "ASTRA_DB_APPLICATION_TOKEN"; public static final String ENV_KEY = "ASTRA_ENV"; + public static final String LOCAL_ENDPOINT_KEY = "ASTRA_LOCAL_ENDPOINT"; public static final String SOURCE_KEY = "PROFILE_SOURCE"; private final CliContext ctx; @@ -124,29 +127,47 @@ private static Either mkProfileFromSection(CliContext c ); } - val rawEnv = section.lookupKey(ENV_KEY).orElse("PROD"); + val env = lookupEnvironment(ctx, section); - try { - val env = AstraEnvironment.valueOf(rawEnv.toUpperCase()); - - val sourceForDefault = section.lookupKey(SOURCE_KEY) - .filter(s -> profileName.isDefault() && !s.isBlank() && !s.equals(ProfileName.DEFAULT.unwrap())) - .map(ProfileName::parse) - .filter(Either::isRight) - .map(Either::getRight); - - return AstraToken.parse(token.get()).bimap( - (msg) -> new InvalidProfile(section, "Error parsing " + ctx.colors().PURPLE_300.useOrQuote(TOKEN_KEY) + ": " + msg), - (tokenValue) -> new Profile(Optional.of(profileName), tokenValue, env, sourceForDefault) - ); - } catch (IllegalArgumentException e) { + if (env.isLeft()) { return Either.left( - new InvalidProfile(section, "Error parsing " + ctx.colors().PURPLE_300.useOrQuote(ENV_KEY) + ": Got '" + rawEnv + "', expected one of (" + String.join("|", AstraEnvironment.allValuesLower()) + ")") + new InvalidProfile(section, env.getLeft()) ); } + + val sourceForDefault = section.lookupKey(SOURCE_KEY) + .filter(s -> profileName.isDefault() && !s.isBlank() && !s.equals(ProfileName.DEFAULT.unwrap())) + .map(ProfileName::parse) + .filter(Either::isRight) + .map(Either::getRight); + + return AstraToken.parse(token.get()).bimap( + (msg) -> new InvalidProfile(section, "Error parsing " + ctx.colors().PURPLE_300.useOrQuote(TOKEN_KEY) + ": " + msg), + (tokenValue) -> new Profile(Optional.of(profileName), tokenValue, env.getRight(), sourceForDefault) + ); }); } + private static Either lookupEnvironment(CliContext ctx, IniSection section) { + val rawEnv = section.lookupKey(ENV_KEY).orElse(PROD.name()); + + if (!rawEnv.equalsIgnoreCase(LOCAL_NAME)) { + try { + return Either.pure(AstraEnvironment.valueOf(rawEnv)); + } catch (IllegalArgumentException e) { + return Either.left("Error parsing " + ctx.colors().PURPLE_300.useOrQuote(ENV_KEY) + ": Got '" + rawEnv + "', expected one of (" + String.join("|", AstraEnvironment.allValuesLower()) + ")"); + } + } + + val endpoint = section.lookupKey(LOCAL_ENDPOINT_KEY); + + if (endpoint.isEmpty()) { + return Either.left("Using a LOCAL environment requires " + ctx.colors().PURPLE_300.useOrQuote(LOCAL_ENDPOINT_KEY) + " to be set"); + } + + return Either.pure(AstraEnvironment.local(endpoint.get())); + } + public List profilesValidated() { return profiles.stream().map((e) -> e.fold( (invalid) -> { @@ -210,15 +231,23 @@ public class ProfileModificationCtx { private final List actions = new ArrayList<>(); public void createProfile(ProfileName name, AstraToken token, AstraEnvironment env) { + createProfile(name, token, env, Optional.empty()); + } + + public void createProfile(ProfileName name, AstraToken token, AstraEnvironment env, Optional localEndpoint) { actions.add(() -> { profiles.add(Either.pure(new Profile(Optional.of(name), token, env, Optional.empty()))); backingIniFile.addSection(name.unwrap(), new TreeMap<>() {{ put(TOKEN_KEY, token.unsafeUnwrap()); - if (env != AstraEnvironment.PROD) { + if (env != PROD) { put(ENV_KEY, env.name()); } + + if (env.name().equalsIgnoreCase(LOCAL_NAME) && localEndpoint.isPresent()) { + put(LOCAL_ENDPOINT_KEY, localEndpoint.get()); + } }}); }); } diff --git a/src/main/java/com/dtsx/astra/cli/operations/config/ConfigCreateOperation.java b/src/main/java/com/dtsx/astra/cli/operations/config/ConfigCreateOperation.java index d7e0f3dc..efc471a8 100644 --- a/src/main/java/com/dtsx/astra/cli/operations/config/ConfigCreateOperation.java +++ b/src/main/java/com/dtsx/astra/cli/operations/config/ConfigCreateOperation.java @@ -31,6 +31,7 @@ public record CreateConfigRequest( AstraEnvironment env, Optional overwrite, boolean setDefault, + Optional localEndpoint, Consumer assertCanOverwriteProfile ) {} @@ -66,12 +67,12 @@ public ConfigCreateResult execute() { config.modify((ctx) -> { ctx.deleteProfile(profileName); - ctx.createProfile(profileName, request.token, request.env); + ctx.createProfile(profileName, request.token, request.env, request.localEndpoint); if (request.setDefault) { ctx.deleteProfile(ProfileName.DEFAULT); - ctx.createProfile(ProfileName.DEFAULT, request.token, request.env); + ctx.createProfile(ProfileName.DEFAULT, request.token, request.env, request.localEndpoint); } }); diff --git a/src/main/java/com/dtsx/astra/sdk/utils/AstraEnvironment.java b/src/main/java/com/dtsx/astra/sdk/utils/AstraEnvironment.java index 1472f717..92a09a98 100644 --- a/src/main/java/com/dtsx/astra/sdk/utils/AstraEnvironment.java +++ b/src/main/java/com/dtsx/astra/sdk/utils/AstraEnvironment.java @@ -1,8 +1,14 @@ package com.dtsx.astra.sdk.utils; +import com.dtsx.astra.cli.core.exceptions.AstraCliException; +import com.dtsx.astra.cli.core.exceptions.internal.cli.OptionValidationException; import lombok.*; import lombok.experimental.Accessors; +import java.util.Optional; + +import static com.dtsx.astra.cli.core.output.ExitCode.VALIDATION_ISSUE; + @Getter @Accessors(fluent = false) @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @@ -32,6 +38,8 @@ public final class AstraEnvironment { ".api.staging.streaming.datastax.com" ); + public static final String LOCAL_NAME = "LOCAL"; + @Accessors(fluent = true) private final int ordinal; @@ -42,8 +50,12 @@ public final class AstraEnvironment { private final String appsSuffix; private final String streamingV3Suffix; + public boolean isLocal() { + return this.ordinal == -1; + } + public static AstraEnvironment local(String endpoint) { - return new AstraEnvironment(-1, "LOCAL", endpoint, "", ""); + return new AstraEnvironment(-1, LOCAL_NAME, endpoint, "n/a", "n/a"); // TODO figure out what to do with these endpoints } public static AstraEnvironment[] values() { @@ -51,18 +63,35 @@ public static AstraEnvironment[] values() { } public static String[] allValuesLower() { - return new String[]{ PROD.name.toLowerCase(), DEV.name.toLowerCase(), TEST.name.toLowerCase(), "local" }; + return new String[]{ PROD.name.toLowerCase(), DEV.name.toLowerCase(), TEST.name.toLowerCase(), LOCAL_NAME.toLowerCase() }; } public static AstraEnvironment valueOf(String name) { for (val env : values()) { - if (env.name.equals(name)) { + if (env.name.equals(name.toUpperCase())) { return env; } } throw new IllegalArgumentException("No AstraEnvironment constant with name: " + name); } + public static AstraEnvironment resolve(Optional envString, Optional localEndpoint) { + val envName = envString.orElse(PROD.name()).toUpperCase(); + + if (envName.equalsIgnoreCase(LOCAL_NAME)) { + if (localEndpoint.isEmpty()) { + throw new AstraCliException(VALIDATION_ISSUE, "@|bold,red When using --env local, you must provide --local-endpoint with the URL of your local DevOps API instance.|@"); + } + return AstraEnvironment.local(localEndpoint.get()); + } + + try { + return AstraEnvironment.valueOf(envName); + } catch (IllegalArgumentException e) { + throw new OptionValidationException("env", "Invalid environment: '" + envName + "'. Expected one of: " + String.join(", ", AstraEnvironment.allValuesLower())); + } + } + @Override public String toString() { return name; diff --git a/src/test/java/com/dtsx/astra/cli/testlib/TestConfig.java b/src/test/java/com/dtsx/astra/cli/testlib/TestConfig.java index 66f6af36..f03c1c97 100644 --- a/src/test/java/com/dtsx/astra/cli/testlib/TestConfig.java +++ b/src/test/java/com/dtsx/astra/cli/testlib/TestConfig.java @@ -24,7 +24,7 @@ public static AstraToken token() { } public static AstraEnvironment env() { - return AstraEnvironment.valueOf(env("ASTRA_ENV", "prod").toUpperCase()); + return AstraEnvironment.valueOf(env("ASTRA_ENV", "prod")); } public static String apiEndpoint() { From 578307efc1298db5bb5b593ed8b66ea976afcd12 Mon Sep 17 00:00:00 2001 From: toptobes Date: Fri, 24 Jul 2026 03:50:39 -0500 Subject: [PATCH 3/4] c --- src/main/java/com/dtsx/astra/cli/gateways/APIProviderImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/dtsx/astra/cli/gateways/APIProviderImpl.java b/src/main/java/com/dtsx/astra/cli/gateways/APIProviderImpl.java index 54b95856..316a7cdb 100644 --- a/src/main/java/com/dtsx/astra/cli/gateways/APIProviderImpl.java +++ b/src/main/java/com/dtsx/astra/cli/gateways/APIProviderImpl.java @@ -90,7 +90,7 @@ private DataAPIClient dataApiClient() { } private DataAPIDestination throwCantUseLocalWithDataApi() { - throw new AstraCliException(ILLEGAL_OPERATION, "Local environments are not supported for Data API operations. Please use a prod, dev, or test Astra environment."); + throw new AstraCliException(ILLEGAL_OPERATION, "@|bold,red Local environments are not supported for Data API operations. Please use a prod, dev, or test Astra environment.|@"); } private UUID resolveDbId(DbRef ref) { From 97498113c74f6b54b0f9b93951b7ba88704bb859 Mon Sep 17 00:00:00 2001 From: toptobes Date: Sat, 25 Jul 2026 23:12:58 -0500 Subject: [PATCH 4/4] little more work --- .../cli/commands/config/ConfigCreateCmd.java | 17 +++++++++ .../astra/cli/core/output/AstraLogger.java | 2 +- .../config/ConfigCreateOperation.java | 37 ++++++++++--------- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java b/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java index 86e749ab..a93e4c9a 100644 --- a/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java +++ b/src/main/java/com/dtsx/astra/cli/commands/config/ConfigCreateCmd.java @@ -103,6 +103,15 @@ public class ConfigCreateCmd extends AbstractConfigCmd { ) private Optional $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 resultSupplier) { val result = resultSupplier.get(); @@ -112,6 +121,7 @@ public final OutputAll execute(Supplier resultSupplier) { case ProfileIllegallyExists(var profileName) -> throwProfileAlreadyExists(profileName); case ViolatedFailIfExists() -> throwAttemptedToSetDefault(); case InvalidToken(var hint) -> throwInvalidToken(hint); + case NameRequiredIfNotValidated _ -> throwNameRequiredIfNotValidated(); }; } @@ -200,6 +210,12 @@ private T throwInvalidToken(Optional hint) { """); } + private T throwNameRequiredIfNotValidated() { + throw new AstraCliException(VALIDATION_ISSUE, """ + @|bold,red An explicit profile name must be provided if @|italic --validate=false|@.|@ + """); + } + @Override public Operation mkOperation() { val env = AstraEnvironment.resolve($env, $localEndpoint); @@ -210,6 +226,7 @@ public Operation mkOperation() { env, $overwrite, $setDefault, + $validate, $localEndpoint, this::assertCanOverwriteProfile )); diff --git a/src/main/java/com/dtsx/astra/cli/core/output/AstraLogger.java b/src/main/java/com/dtsx/astra/cli/core/output/AstraLogger.java index 86de6a8d..8f50f0ae 100644 --- a/src/main/java/com/dtsx/astra/cli/core/output/AstraLogger.java +++ b/src/main/java/com/dtsx/astra/cli/core/output/AstraLogger.java @@ -110,7 +110,7 @@ public void info(String... msg) { } public void hint(String... msg) { - log("@|green [HINT]|@ " + String.join("", msg), Level.REGULAR, true); + log("@![HINT]!@ " + String.join("", msg), Level.REGULAR, true); } public void warn(String... msg) { diff --git a/src/main/java/com/dtsx/astra/cli/operations/config/ConfigCreateOperation.java b/src/main/java/com/dtsx/astra/cli/operations/config/ConfigCreateOperation.java index efc471a8..2c900dee 100644 --- a/src/main/java/com/dtsx/astra/cli/operations/config/ConfigCreateOperation.java +++ b/src/main/java/com/dtsx/astra/cli/operations/config/ConfigCreateOperation.java @@ -3,12 +3,12 @@ import com.dtsx.astra.cli.core.CliContext; import com.dtsx.astra.cli.core.config.AstraConfig; import com.dtsx.astra.cli.core.config.ProfileName; +import com.dtsx.astra.cli.core.datatypes.Either; import com.dtsx.astra.cli.core.models.AstraToken; import com.dtsx.astra.cli.gateways.org.OrgGateway; import com.dtsx.astra.cli.operations.Operation; import com.dtsx.astra.cli.operations.config.ConfigCreateOperation.ConfigCreateResult; import com.dtsx.astra.sdk.exception.AuthenticationException; -import com.dtsx.astra.sdk.org.domain.Organization; import com.dtsx.astra.sdk.utils.AstraEnvironment; import lombok.RequiredArgsConstructor; import lombok.val; @@ -31,6 +31,7 @@ public record CreateConfigRequest( AstraEnvironment env, Optional overwrite, boolean setDefault, + boolean validate, Optional localEndpoint, Consumer assertCanOverwriteProfile ) {} @@ -40,21 +41,17 @@ public record ProfileCreated(ProfileName profileName, boolean overwritten, boole public record ProfileIllegallyExists(ProfileName profileName) implements ConfigCreateResult {} public record ViolatedFailIfExists() implements ConfigCreateResult {} public record InvalidToken(Optional hint) implements ConfigCreateResult {} + public record NameRequiredIfNotValidated() implements ConfigCreateResult {} @Override public ConfigCreateResult execute() { - val org = validateTokenAndFetchOrg(orgGateway); + val maybeProfileName = validateTokenAndResolveName(orgGateway, request); - if (org.isEmpty()) { - return new InvalidToken(statelessOrgGateway.resolveOrganizationEnvironment(request.token).map(Pair::getLeft)); - } - - val profileName = resolveProfileName(org.get(), request); - - if (profileName.isDefault()) { - return new ViolatedFailIfExists(); + if (maybeProfileName.isLeft()) { + return maybeProfileName.getLeft(); } + val profileName = maybeProfileName.getRight(); val profileExists = config.profileExists(profileName); if (profileExists) { @@ -69,7 +66,6 @@ public ConfigCreateResult execute() { ctx.deleteProfile(profileName); ctx.createProfile(profileName, request.token, request.env, request.localEndpoint); - if (request.setDefault) { ctx.deleteProfile(ProfileName.DEFAULT); ctx.createProfile(ProfileName.DEFAULT, request.token, request.env, request.localEndpoint); @@ -83,20 +79,25 @@ public ConfigCreateResult execute() { ); } - private Optional validateTokenAndFetchOrg(OrgGateway orgGateway) { + private Either validateTokenAndResolveName(OrgGateway orgGateway, CreateConfigRequest request) { + if (!request.validate) { + return Either.fromOptional(request.profileName, NameRequiredIfNotValidated::new); + } + return ctx.log().loading("Validating your Astra token", (_) -> { try { - return Optional.of(orgGateway.current()); + val name = ProfileName.mkUnsafe(orgGateway.current().getName()); + return Either.pure(name); } catch (AuthenticationException e) { - return Optional.empty(); + val validEnv = statelessOrgGateway.resolveOrganizationEnvironment(request.token).map(Pair::getLeft); + return Either.left(new InvalidToken(validEnv)); + } catch (Exception e) { + ctx.log().hint("You can use @!--no-validate!@ to skip token validation and create a profile with an explicit name."); + throw e; } }); } - private ProfileName resolveProfileName(Organization org, CreateConfigRequest request) { - return request.profileName.orElse(ProfileName.mkUnsafe(org.getName())); - } - private Optional assertCanOverwriteProfile(ProfileName profileName, CreateConfigRequest request) { if (request.overwrite.isEmpty()) { request.assertCanOverwriteProfile.accept(profileName);