MarketplaceParameterSupport.java
package com.cloudforgeci.api.launch;
import com.cloudforgeci.api.core.DeploymentContext;
import com.cloudforgeci.api.core.iam.ManagerOperatorIamSupport;
import com.cloudforge.core.interfaces.ApplicationSpec;
import com.cloudforge.core.manager.ManagerEnvKeys;
import software.amazon.awscdk.CfnParameter;
import software.amazon.awscdk.Stack;
import software.constructs.Construct;
import java.util.HashMap;
import java.util.Map;
/**
* For an AWS Marketplace CloudFormation listing of CloudForge Manager (the only application this
* applies to -- see {@link ManagerOperatorIamSupport#APPLICATION_ID}), replaces the two values a
* buyer must supply at launch -- {@code cognitoInitialAdminEmail}, {@code managerLicenseKey} --
* with real {@link CfnParameter}s the AWS Console's CloudFormation launch form renders, instead of
* requiring them via CDK {@code --context}/cdk.json (meaningless to a buyer who never runs the CDK
* CLI at all).
*
* <p>Whether Marketplace mode applies is never sourced from {@code DeploymentConfig}/{@code
* --context} -- a field there would be just another value in a user-editable JSON file, settable
* on a normal deploy by anyone, not something only an actual Marketplace listing build controls.
* The caller (see {@link com.cloudforgeci.api.deploy.CloudForgeSynthesizer#synthesizeForMarketplace})
* passes it as a plain method parameter instead, hardcoded {@code true} at that one dedicated call
* site and never exposed as a setting anywhere else.</p>
*
* <p>LicenseSeat is the single licensing model across every deployment channel, including
* Marketplace (BYOL -- Bring Your Own License -- is an AWS Marketplace-supported pricing model
* under which the seller's own licensing system, not Marketplace's native entitlement mechanism,
* governs the license), so {@code managerLicenseKey} belongs in the launch form the same as
* {@code cognitoInitialAdminEmail}.</p>
*
* <p>Deliberately value-only, not structural: both existing consumers ({@code
* CognitoAuthenticationFactory}/{@code ContainerFactory} for the admin email, {@code
* ApplicationFactory} for the license key) already gate resource creation on "is this
* non-null/non-empty" -- a CDK Token string (what {@code CfnParameter#getValueAsString()} returns
* at synth time, before the buyer's real value exists) is always non-null and non-empty, so those
* synth-time structural decisions are completely unaffected; only the literal value each resource
* ends up holding changes, resolved by CloudFormation at deploy time from what the buyer actually
* typed in.</p>
*
* <p>Can't override the stack's own node context: CDK's {@code Node.setContext} refuses once the
* node already has children, and the two {@link CfnParameter}s this creates are themselves
* children of the stack the moment they're built. Returns a small nested scope instead -- created
* fresh, so it has no children yet when its context is set -- for the caller to build the
* application's factories under in place of the stack directly. Each factory resolves its own
* {@link DeploymentContext} independently via {@code DeploymentContext.from(this)} in {@code
* BaseFactory}'s constructor (not a shared instance), so building them under this scope is enough
* for the existing {@code @DeploymentContext} injection to pick up the overridden values for
* free, with no change needed in any of those factories. This does shift every resource's logical
* id by one construct-path segment relative to a non-Marketplace deploy of the same app -- fine
* for a brand new feature with no prior deployment to preserve continuity with, but would be a
* breaking change (CloudFormation replaces rather than updates) for anything already deployed, so
* never make this unconditional.</p>
*
* <p>Never validates the key's content -- this library has no way to check a LicenseSeat key is
* real, only that some value was supplied, identical to how {@code --context
* managerLicenseKey=...} already works today. Actual license verification happens at runtime
* inside cloudforge-manager's own compiled binary against LicenseSeat's servers, not here.</p>
*/
final class MarketplaceParameterSupport {
private static final String SSO_DESCRIPTION =
"CloudForge Manager with Cognito sign-in. Enter the administrator's email address and a "
+ "unique Cognito domain prefix; Cognito emails that address a temporary password for "
+ "the first login.";
private static final String TOKEN_DESCRIPTION =
"CloudForge Manager with setup-token sign-in (no Cognito). After launch, Manager writes a "
+ "one-time setup token to its CloudWatch log at startup. Open the Manager URL from the "
+ "stack Outputs and enter that token (X-CFC-TOKEN) in the setup wizard to create the "
+ "first administrator.";
private MarketplaceParameterSupport() {
}
/**
* Returns {@code stack} unchanged unless {@code variant} is non-null and {@code
* applicationSpec} is CloudForge Manager -- every other application, and every non-Marketplace
* CloudForge Manager deployment, keeps sourcing these values from CDK context exactly as
* before, with the caller building the application directly under the stack as usual.
* {@link MarketplaceVariant#SSO} adds required {@code AdminEmail}/{@code CognitoDomainPrefix}
* parameters and provisions Cognito; {@link MarketplaceVariant#TOKEN} adds neither and
* disables auth, so the first administrator is created with Manager's one-time setup token.
*/
static Construct applyIfApplicable(
Stack stack, DeploymentContext cfc, ApplicationSpec applicationSpec, MarketplaceVariant variant) {
if (variant == null) {
return stack;
}
if (!ManagerOperatorIamSupport.APPLICATION_ID.equals(applicationSpec.applicationId())) {
return stack;
}
// No minLength(1), and defaultValue("") so CloudFormation doesn't treat this as a required
// parameter: a buyer may launch the stack before they have a key in hand and activate
// licensing later from the Settings -> License page. ApplicationFactory's
// LicenseKeySecret guards the deploy-time-resolved value too (an Fn::If against a
// CfnCondition, not just the Java-side isBlank() check, which can't see an empty
// CfnParameter token at synth time) -- see that block's own comment for why both guards
// are needed.
CfnParameter licenseKey = CfnParameter.Builder.create(stack, "LicenseKey")
.type("String")
.description("LicenseSeat license key (CFC-XXXX-XXXX-XXXX-XXXX) from your AWS Marketplace "
+ "purchase. Optional at launch -- activate it later from Settings -> License if you "
+ "don't have one yet.")
.defaultValue("")
.noEcho(true)
.build();
Map<String, Object> overridden = new HashMap<>(cfc.raw());
overridden.put("managerLicenseKey", licenseKey.getValueAsString());
if (variant == MarketplaceVariant.SSO) {
stack.getTemplateOptions().setDescription(SSO_DESCRIPTION);
CfnParameter adminEmail = CfnParameter.Builder.create(stack, "AdminEmail")
.type("String")
.description("Email address for the initial CloudForge Manager administrator. "
+ "Cognito emails a temporary password to this address.")
.minLength(3)
.allowedPattern("^.+@.+$")
.constraintDescription("Must be a valid email address")
.build();
CfnParameter cognitoDomainPrefix = CfnParameter.Builder.create(stack, "CognitoDomainPrefix")
.type("String")
.description("Globally unique prefix for the Cognito sign-in domain "
+ "(lowercase letters, numbers and hyphens).")
.minLength(1)
.maxLength(63)
.allowedPattern("^[a-z0-9][a-z0-9-]*$")
.constraintDescription("Must be lowercase letters, numbers and hyphens, "
+ "starting with a letter or number")
.build();
overridden.put("authMode", "application-oidc");
overridden.put("cognitoAutoProvision", true);
overridden.put("cognitoInitialAdminEmail", adminEmail.getValueAsString());
overridden.put("cognitoAdminGroupName", ManagerEnvKeys.DEFAULT_OIDC_ADMIN_GROUP);
overridden.put("cognitoUserGroupName", ManagerEnvKeys.DEFAULT_OIDC_MANAGER_GROUP);
overridden.put("cognitoDomainPrefix", cognitoDomainPrefix.getValueAsString());
} else {
stack.getTemplateOptions().setDescription(TOKEN_DESCRIPTION);
overridden.put("authMode", "none");
overridden.put("cognitoAutoProvision", false);
}
// Defaults to true for this Marketplace listing only -- DeploymentConfig#managerDirectDeployEnabled
// is REQUIRES_APPROVAL/EXPERIMENTAL and false elsewhere, since it grants Manager's task role
// real CFN CreateStack/UpdateStack + Service Catalog ProvisionProduct permissions. A
// Marketplace buyer is specifically subscribing to use Manager's deploy capability, so the
// approval this flag normally requires is implicit in the purchase itself.
overridden.put("managerDirectDeployEnabled", true);
Construct appScope = new Construct(stack, "App");
appScope.getNode().setContext("cfc", overridden);
return appScope;
}
}