Skip to content
Merged
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
49 changes: 45 additions & 4 deletions generators/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ export default class extends Generator {
});

// Infrastructure options
this.option('deploy-target', {
this.option('build-target', {
type: String,
description: 'Deployment target (sagemaker, codebuild)'
description: 'Build target (codebuild)'
});

this.option('codebuild-compute-type', {
Expand All @@ -126,6 +126,31 @@ export default class extends Generator {
description: 'AWS IAM role ARN for SageMaker execution'
});

this.option('deployment-target', {
type: String,
description: 'Deployment target (managed-inference, hyperpod-eks)'
});

this.option('hyperpod-cluster', {
type: String,
description: 'HyperPod EKS cluster name'
});

this.option('hyperpod-namespace', {
type: String,
description: 'Kubernetes namespace for HyperPod deployment (default: default)'
});

this.option('hyperpod-replicas', {
type: Number,
description: 'Number of replicas for HyperPod deployment (default: 1)'
});

this.option('fsx-volume-handle', {
type: String,
description: 'FSx for Lustre volume handle for HyperPod storage'
});

this.option('hf-token', {
type: String,
description: 'HuggingFace authentication token (or "$HF_TOKEN" to use environment variable)'
Expand Down Expand Up @@ -361,11 +386,21 @@ export default class extends Generator {
orderedEnvVars
};

// Build ignore patterns for conditional directory exclusion
const ignorePatterns = [];

// Exclude HyperPod K8s manifests when not deploying to HyperPod
if (this.answers.deploymentTarget !== 'hyperpod-eks') {
ignorePatterns.push('**/hyperpod/**');
}

// Copy all templates, processing EJS variables
this.fs.copyTpl(
this.templatePath('**/*'),
this.destinationPath(),
templateVars
templateVars,
{},
{ globOptions: { ignore: ignorePatterns, dot: true } }
);

// Remove files that don't belong in this deployment configuration
Expand Down Expand Up @@ -723,7 +758,13 @@ export default class extends Generator {
includeSampleModel: false,
includeTesting: true,
testTypes: [],
buildTimestamp: new Date().toISOString()
buildTimestamp: new Date().toISOString(),
buildTarget: 'codebuild',
deploymentTarget: 'managed-inference',
hyperPodCluster: null,
hyperPodNamespace: 'default',
hyperPodReplicas: 1,
fsxVolumeHandle: null
};

// Apply defaults for any missing fields
Expand Down
13 changes: 9 additions & 4 deletions generators/app/lib/cli-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,17 @@ CLI OPTIONS:
--include-sample Include sample model code
--include-testing Include test suite
--test-types=<types> Comma-separated test types (local-model-cli,local-model-server,hosted-model-endpoint)
--deploy-target=<target> Deployment target (sagemaker|codebuild)
--build-target=<target> Build target (codebuild)
--codebuild-compute-type=<type> CodeBuild compute type (BUILD_GENERAL1_SMALL|BUILD_GENERAL1_MEDIUM|BUILD_GENERAL1_LARGE)
--codebuild-project-name=<name> CodeBuild project name
--instance-type=<type> SageMaker instance type (e.g., ml.m5.large, ml.g5.xlarge)
--region=<region> AWS region
--role-arn=<arn> AWS IAM role ARN for SageMaker execution
--deployment-target=<target> Deployment target (managed-inference|hyperpod-eks)
--hyperpod-cluster=<name> HyperPod EKS cluster name
--hyperpod-namespace=<ns> Kubernetes namespace for HyperPod (default: default)
--hyperpod-replicas=<n> Number of replicas for HyperPod (default: 1)
--fsx-volume-handle=<id> FSx for Lustre volume handle for HyperPod storage
--hf-token=<token> HuggingFace token (or "$HF_TOKEN" for env var)

VALIDATION OPTIONS:
Expand Down Expand Up @@ -211,7 +216,7 @@ REGISTRY SYSTEM:

ENVIRONMENT VARIABLES:
ML_INSTANCE_TYPE Instance type
ML_DEPLOY_TARGET Deployment target
ML_BUILD_TARGET Build target
ML_CODEBUILD_COMPUTE_TYPE CodeBuild compute type
AWS_REGION AWS region
AWS_ROLE AWS IAM role ARN
Expand Down Expand Up @@ -460,7 +465,7 @@ yo ml-container-creator my-codebuild-project \\
--framework=sklearn \\
--model-server=flask \\
--model-format=pkl \\
--deploy-target=codebuild \\
--build-target=codebuild \\
--codebuild-compute-type=BUILD_GENERAL1_MEDIUM \\
--codebuild-project-name=my-build-project \\
--skip-prompts
Expand Down Expand Up @@ -494,7 +499,7 @@ yo ml-container-creator \\
'includeSampleModel': false,
'includeTesting': true,
'testTypes': ['local-model-cli', 'local-model-server', 'hosted-model-endpoint'],
'deployTarget': 'codebuild',
'buildTarget': 'codebuild',
'codebuildComputeType': 'BUILD_GENERAL1_MEDIUM',
'codebuildProjectName': 'my-build-project',
'instanceType': 'ml.m5.large',
Expand Down
129 changes: 109 additions & 20 deletions generators/app/lib/config-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'node:url';
import { McpClient } from './mcp-client.js';

// Resolve the generator project root (three levels up from generators/app/lib/)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const GENERATOR_ROOT = path.resolve(__dirname, '..', '..', '..');

/**
* Configuration error for invalid configuration values
*/
Expand Down Expand Up @@ -122,6 +128,21 @@ export default class ConfigManager {
}
});

// Derive framework and modelServer from deploymentConfig if present.
// In prompted mode the PromptRunner does this split, but in --skip-prompts
// mode we need to do it here so the values are available for downstream logic.
if (finalConfig.deploymentConfig) {
const parts = finalConfig.deploymentConfig.split('-');
const derivedFramework = parts[0];
const derivedModelServer = parts.slice(1).join('-');
if (!finalConfig.framework || finalConfig.framework === null) {
finalConfig.framework = derivedFramework;
}
if (!finalConfig.modelServer || finalConfig.modelServer === null) {
finalConfig.modelServer = derivedModelServer;
}
}

// When skipping prompts, provide reasonable defaults for missing required parameters
if (this.skipPrompts) {
Object.entries(this.parameterMatrix).forEach(([param, config]) => {
Expand Down Expand Up @@ -202,8 +223,8 @@ export default class ConfigManager {
finalConfig.destinationDir = `./${finalConfig.projectName}`;
}

// Generate CodeBuild project name if deployTarget is codebuild
if (finalConfig.deployTarget === 'codebuild' && !finalConfig.codebuildProjectName) {
// Generate CodeBuild project name if buildTarget is codebuild
if ((finalConfig.buildTarget === 'codebuild' || finalConfig.deployTarget === 'codebuild') && !finalConfig.codebuildProjectName) {
finalConfig.codebuildProjectName = this._generateCodeBuildProjectName(
finalConfig.projectName,
finalConfig.framework
Expand Down Expand Up @@ -343,6 +364,7 @@ export default class ConfigManager {
awsRegion: {
cliOption: 'region',
envVar: 'AWS_REGION',
ambientEnvVar: true, // AWS_REGION is commonly set in shells; treat as default, not explicit override
configFile: true,
packageJson: true,
mcp: true,
Expand Down Expand Up @@ -406,9 +428,9 @@ export default class ConfigManager {
default: '.',
valueSpace: 'bounded'
},
deployTarget: {
cliOption: 'deploy-target',
envVar: 'ML_DEPLOY_TARGET',
buildTarget: {
cliOption: 'build-target',
envVar: 'ML_BUILD_TARGET',
configFile: true,
packageJson: false,
mcp: false,
Expand Down Expand Up @@ -449,6 +471,61 @@ export default class ConfigManager {
required: false,
default: null,
valueSpace: 'bounded'
},
deploymentTarget: {
cliOption: 'deployment-target',
envVar: 'ML_DEPLOYMENT_TARGET',
configFile: true,
packageJson: false,
mcp: false,
promptable: true,
required: true,
default: 'managed-inference',
valueSpace: 'bounded'
},
hyperPodCluster: {
cliOption: 'hyperpod-cluster',
envVar: null,
configFile: true,
packageJson: false,
mcp: true,
promptable: true,
required: false,
default: null,
valueSpace: 'unbounded'
},
hyperPodNamespace: {
cliOption: 'hyperpod-namespace',
envVar: null,
configFile: true,
packageJson: false,
mcp: false,
promptable: true,
required: false,
default: 'default',
valueSpace: 'bounded'
},
hyperPodReplicas: {
cliOption: 'hyperpod-replicas',
envVar: null,
configFile: true,
packageJson: false,
mcp: false,
promptable: true,
required: false,
default: 1,
valueSpace: 'bounded'
},
fsxVolumeHandle: {
cliOption: 'fsx-volume-handle',
envVar: null,
configFile: true,
packageJson: false,
mcp: false,
promptable: true,
required: false,
default: null,
valueSpace: 'bounded'
}
};
}
Expand Down Expand Up @@ -550,7 +627,7 @@ export default class ConfigManager {
*/
async _loadCustomConfigFile() {
try {
const configPath = this.generator.destinationPath('config/mcp.json');
const configPath = path.join(GENERATOR_ROOT, 'config', 'mcp.json');
if (fs.existsSync(configPath)) {
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
this._mergeConfig(config);
Expand Down Expand Up @@ -626,19 +703,22 @@ export default class ConfigManager {
const envMapping = {};
Object.entries(this.parameterMatrix).forEach(([param, config]) => {
if (config.envVar) {
envMapping[config.envVar] = param;
envMapping[config.envVar] = { param, ambient: config.ambientEnvVar === true };
}
});

Object.entries(envMapping).forEach(([envVar, configKey]) => {
Object.entries(envMapping).forEach(([envVar, { param: configKey, ambient }]) => {
const value = process.env[envVar];
if (value !== undefined && value !== '' && this._isSourceSupported(configKey, 'envVar')) {
this.config[configKey] = this._parseValue(configKey, value);
// Track as explicit configuration
if (!this.explicitConfig) {
this.explicitConfig = {};
// Track as explicit configuration — unless the env var is ambient
// (e.g. AWS_REGION is commonly set in shells as a default, not an override)
if (!ambient) {
if (!this.explicitConfig) {
this.explicitConfig = {};
}
this.explicitConfig[configKey] = this._parseValue(configKey, value);
}
this.explicitConfig[configKey] = this._parseValue(configKey, value);
}
});
}
Expand Down Expand Up @@ -702,7 +782,7 @@ export default class ConfigManager {
async queryMcpServer(serverName, context = {}) {
let mcpServerConfigs;
try {
const configPath = this.generator.destinationPath('config/mcp.json');
const configPath = path.join(GENERATOR_ROOT, 'config', 'mcp.json');
if (!fs.existsSync(configPath)) return null;
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
mcpServerConfigs = config.mcpServers;
Expand Down Expand Up @@ -770,7 +850,7 @@ export default class ConfigManager {
*/
getMcpServerNames() {
try {
const configPath = this.generator.destinationPath('config/mcp.json');
const configPath = path.join(GENERATOR_ROOT, 'config', 'mcp.json');
if (!fs.existsSync(configPath)) return [];
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
return Object.keys(config.mcpServers || {});
Expand Down Expand Up @@ -868,9 +948,10 @@ export default class ConfigManager {
}
}

// Validate deployment target
if (this.config.deployTarget && !supportedOptions.deployTargets.includes(this.config.deployTarget)) {
errors.push(`Unsupported deployment target: ${this.config.deployTarget}. Supported targets: ${supportedOptions.deployTargets.join(', ')}`);
// Validate build target (renamed from deployTarget)
const buildTarget = this.config.buildTarget || this.config.deployTarget;
if (buildTarget && !supportedOptions.buildTargets.includes(buildTarget)) {
errors.push(`Unsupported build target: ${buildTarget}. Supported targets: ${supportedOptions.buildTargets.join(', ')}`);
}

// Validate CodeBuild compute type
Expand Down Expand Up @@ -944,6 +1025,13 @@ export default class ConfigManager {
return; // Skip validation for transformers
}

// Special case: instanceType is not required for hyperpod-eks
// when not provided (backward compatibility) — but it IS prompted now
// so it should normally be present
if (param === 'instanceType' && finalConfig.deploymentTarget === 'hyperpod-eks' && !finalConfig.instanceType) {
return; // Skip validation only if truly missing for backward compat
}

if (isEmpty) {
if (config.promptable) {
// Promptable required parameter is missing - this should not happen after prompting
Expand Down Expand Up @@ -1154,10 +1242,11 @@ export default class ConfigManager {
}
break;

case 'buildTarget':
case 'deployTarget':
if (value && !supportedOptions.deployTargets.includes(value)) {
if (value && !supportedOptions.buildTargets.includes(value)) {
throw new ValidationError(
`Unsupported deployment target: ${value}. Supported targets: ${supportedOptions.deployTargets.join(', ')}`,
`Unsupported build target: ${value}. Supported targets: ${supportedOptions.buildTargets.join(', ')}`,
parameter,
value
);
Expand Down Expand Up @@ -1253,7 +1342,7 @@ export default class ConfigManager {
'tensorflow': ['keras', 'h5', 'SavedModel'],
'transformers': [] // No format needed
},
deployTargets: ['codebuild'],
buildTargets: ['codebuild'],
codebuildComputeTypes: ['BUILD_GENERAL1_SMALL', 'BUILD_GENERAL1_MEDIUM', 'BUILD_GENERAL1_LARGE'],
awsRegions: [
'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
Expand Down
8 changes: 3 additions & 5 deletions generators/app/lib/mcp-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,10 @@ class McpClient {

// Build environment: merge process.env with server-specific env
// When --smart flag is active, inject BEDROCK_SMART=true for this run
// Always pass process.env so child processes inherit AWS credentials, profiles, etc.
const smartEnv = this.smart ? { BEDROCK_SMART: 'true' } : {};
const serverEnv = env && Object.keys(env).length > 0 ? env : {};
const mergedEnv = { ...smartEnv, ...serverEnv };
const spawnEnv = Object.keys(mergedEnv).length > 0
? { ...process.env, ...mergedEnv }
: undefined;
const spawnEnv = { ...process.env, ...smartEnv, ...serverEnv };

// Create stdio transport — spawns the server process
this._transport = new StdioClientTransport({
Expand Down Expand Up @@ -231,7 +229,7 @@ class McpClient {
}
}

return { values, choices };
return { values, choices, message: parsed.message || null };
}

/**
Expand Down
Loading
Loading