Skip to content

Commit 2291c2a

Browse files
authored
Merge pull request #160 from dferguson992/main
feat: secrets manager integration, architecture validation, and docs …
2 parents 3e81050 + 4cc882b commit 2291c2a

67 files changed

Lines changed: 9008 additions & 120 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/cli.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ program
9090

9191
// --- Authentication ---
9292
.addOption(new Option('--hf-token <token>', 'HuggingFace token (or "$HF_TOKEN" for env var reference)'))
93+
.addOption(new Option('--hf-token-arn <arn>', 'HuggingFace token ARN from Secrets Manager'))
94+
.addOption(new Option('--ngc-token <token>', 'NVIDIA NGC token (or "$NGC_API_KEY" for env var reference)'))
95+
.addOption(new Option('--ngc-token-arn <arn>', 'NVIDIA NGC token ARN from Secrets Manager'))
9396

9497
// --- Optional Features ---
9598
.addOption(new Option('--include-sample', 'Include sample model code'))
@@ -106,7 +109,18 @@ program
106109
.addOption(new Option('--validate-with-docker', 'Enable Docker introspection validation (opt-in)'))
107110
.addOption(new Option('--offline', 'Disable HuggingFace API lookups'))
108111

109-
.action((projectNameArgs, options) => run(projectNameArgs?.[0] || null, options));
112+
.action((projectNameArgs, options) => {
113+
// Mutual exclusion validation: plaintext token and ARN flags cannot both be provided
114+
if (options.hfToken && options.hfTokenArn) {
115+
console.error('❌ Cannot specify both --hf-token and --hf-token-arn. Use one or the other.');
116+
process.exit(1);
117+
}
118+
if (options.ngcToken && options.ngcTokenArn) {
119+
console.error('❌ Cannot specify both --ngc-token and --ngc-token-arn. Use one or the other.');
120+
process.exit(1);
121+
}
122+
return run(projectNameArgs?.[0] || null, options);
123+
});
110124

111125
// Custom help formatting — group options into logical sections (root command only)
112126
program.configureHelp({
@@ -174,7 +188,7 @@ program.configureHelp({
174188
groups.hyperpod.push(opt);
175189
} else if (['--model-env', '--server-env'].includes(long)) {
176190
groups.env.push(opt);
177-
} else if (['--hf-token'].includes(long)) {
191+
} else if (['--hf-token', '--hf-token-arn', '--ngc-token', '--ngc-token-arn'].includes(long)) {
178192
groups.auth.push(opt);
179193
} else if (['--include-sample', '--include-testing', '--test-types'].includes(long)) {
180194
groups.features.push(opt);
@@ -255,6 +269,7 @@ program
255269
.option('--ci', 'Provision CI integration infrastructure')
256270
.option('--skip-ci', 'Skip CI infrastructure provisioning')
257271
.option('--skip-s3', 'Skip S3 bucket creation')
272+
.option('--skip-post-setup', 'Skip post-setup chain (mcp init, sync-architectures, sync-schemas)')
258273
.action(async (action, args, options) => {
259274
const { default: BootstrapCommandHandler } = await import('../src/lib/bootstrap-command-handler.js');
260275
const handler = new BootstrapCommandHandler();
@@ -314,12 +329,33 @@ program
314329
.option('--project', 'Use project-level registry')
315330
.option('--parameters <json>', 'Parameters JSON string')
316331
.option('--generator-version <version>', 'Generator version')
332+
// Options used by `registry list-architectures`
333+
.option('--server <name>', 'Filter by server name (for list-architectures)')
334+
.option('--verbose', 'Show full list of supported model types (for list-architectures)')
317335
.action(async (action, args, options) => {
318336
const { default: RegistryCommandHandler } = await import('../src/lib/registry-command-handler.js');
319337
const handler = new RegistryCommandHandler();
320338
await handler.handle([action, ...args], options);
321339
});
322340

341+
program
342+
.command('secrets')
343+
.description('Manage secrets in AWS Secrets Manager (create, list, describe)')
344+
.argument('[action]', 'Secrets action (create, list, describe)')
345+
.argument('[args...]', 'Additional arguments')
346+
.option('--type <type>', 'Secret type (e.g., hf-token, ngc-token)')
347+
.option('--name <label>', 'Secret label (used in naming convention)')
348+
.option('--secret-value <value>', 'Secret value (masked in terminal)')
349+
.option('--description <text>', 'Secret description')
350+
.option('--kms-key-id <key>', 'KMS key for encryption')
351+
.option('--json <json-or-path>', 'JSON input (inline or file://path)')
352+
.action(async (action, args, options) => {
353+
const { default: SecretsCommandHandler } = await import('../src/lib/secrets-command-handler.js');
354+
const handler = new SecretsCommandHandler();
355+
const allArgs = action ? [action, ...args] : [];
356+
await handler.handle(allArgs, options);
357+
});
358+
323359
program
324360
.command('configure')
325361
.description('Interactive configuration setup (experimental)')

config/bootstrap-stack.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@
105105
"arn:aws:s3:::ml-container-creator-*",
106106
"arn:aws:s3:::ml-container-creator-*/*"
107107
]
108+
},
109+
{
110+
"Sid": "SecretsManagerRead",
111+
"Effect": "Allow",
112+
"Action": [
113+
"secretsmanager:GetSecretValue",
114+
"secretsmanager:DescribeSecret"
115+
],
116+
"Resource": "arn:aws:secretsmanager:*:*:secret:mlcc/*",
117+
"Condition": {
118+
"StringEquals": {
119+
"aws:ResourceTag/mlcc:managed-by": "ml-container-creator"
120+
}
121+
}
108122
}
109123
]
110124
}

docs/ADDING_FEATURES.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Guide for contributors who want to add new frameworks, model servers, or other f
88
- [Adding a New Model Server](#adding-a-new-model-server)
99
- [Adding a New Test Type](#adding-a-new-test-type)
1010
- [Adding a New Deployment Target](#adding-a-new-deployment-target)
11+
- [Adding a New Secret Type](#adding-a-new-secret-type)
1112
- [Testing Your Changes](#testing-your-changes)
1213

1314
---
@@ -506,6 +507,207 @@ if (this.answers.deployTarget === 'lambda') {
506507

507508
---
508509

510+
## Adding a New Secret Type
511+
512+
The secrets system uses a registry-driven architecture. Adding a new secret type requires only adding an entry to the `Secret_Classification` registry — the CLI, prompt flow, and do-script templates derive behavior from this registry automatically.
513+
514+
Let's walk through adding support for a private PyPI token used during build-time `pip install`.
515+
516+
### Understanding the Registry
517+
518+
The `Secret_Classification` registry lives at `src/lib/secret-classification.js`. Each entry defines all metadata needed for end-to-end integration:
519+
520+
```javascript
521+
// src/lib/secret-classification.js
522+
export const SECRET_CLASSIFICATIONS = Object.freeze([
523+
{
524+
identifier: 'hf-token', // Unique key — used in naming convention and CLI
525+
displayName: 'HuggingFace Token', // Human-readable label for prompts
526+
stages: ['build-time', 'runtime'], // When the secret is needed
527+
purpose: 'Gated model download from HuggingFace Hub',
528+
cliFlag: 'hf-token-arn', // CLI flag for ARN input (--hf-token-arn)
529+
cliFlagPlaintext: 'hf-token', // Existing CLI flag for plaintext (--hf-token)
530+
envVar: 'HF_TOKEN', // Environment variable for plaintext value
531+
envVarArn: 'HF_TOKEN_ARN', // Environment variable for ARN reference
532+
promptLabel: 'HuggingFace token' // Label shown in interactive prompts
533+
},
534+
// ... other entries
535+
]);
536+
```
537+
538+
| Field | Type | Description |
539+
|-------|------|-------------|
540+
| `identifier` | `string` | Unique key (e.g., `pypi-token`) — used in naming convention and type selection |
541+
| `displayName` | `string` | Human-readable label shown in prompts and output |
542+
| `stages` | `string[]` | When the secret is needed: `['build-time']`, `['runtime']`, or `['build-time', 'runtime']` |
543+
| `purpose` | `string` | Description of what the secret is used for |
544+
| `cliFlag` | `string` | CLI flag name for ARN input (e.g., `pypi-token-arn`) |
545+
| `cliFlagPlaintext` | `string` | CLI flag name for plaintext input (e.g., `pypi-token`) |
546+
| `envVar` | `string` | Environment variable name for plaintext (e.g., `PIP_INDEX_TOKEN`) |
547+
| `envVarArn` | `string` | Environment variable name for ARN (e.g., `PIP_INDEX_TOKEN_ARN`) |
548+
| `promptLabel` | `string` | Label shown in interactive prompts |
549+
550+
### Step 1: Add Registry Entry
551+
552+
Edit `src/lib/secret-classification.js` and add a new entry to the `SECRET_CLASSIFICATIONS` array:
553+
554+
```javascript
555+
export const SECRET_CLASSIFICATIONS = Object.freeze([
556+
// ... existing entries ...
557+
{
558+
identifier: 'pypi-token',
559+
displayName: 'Private PyPI Token',
560+
stages: ['build-time'],
561+
purpose: 'Authenticating with private PyPI registry during pip install',
562+
cliFlag: 'pypi-token-arn',
563+
cliFlagPlaintext: 'pypi-token',
564+
envVar: 'PIP_INDEX_TOKEN',
565+
envVarArn: 'PIP_INDEX_TOKEN_ARN',
566+
promptLabel: 'Private PyPI token'
567+
}
568+
]);
569+
```
570+
571+
This single addition automatically enables:
572+
- The type appears in `secrets create` interactive prompts
573+
- The prompt flow queries for managed secrets of this type
574+
- The `secrets list` command includes secrets of this type
575+
576+
### Step 2: Register CLI Flags
577+
578+
Edit `bin/cli.js` to add the new ARN and plaintext flags on the root command:
579+
580+
```javascript
581+
.addOption(new Option('--pypi-token-arn <arn>', 'Private PyPI token ARN from Secrets Manager'))
582+
.addOption(new Option('--pypi-token <value>', 'Private PyPI token (plaintext)'))
583+
```
584+
585+
Add mutual exclusion validation alongside the existing checks:
586+
587+
```javascript
588+
if (options.pypiToken && options.pypiTokenArn) {
589+
console.error('❌ Cannot specify both --pypi-token and --pypi-token-arn');
590+
process.exit(1);
591+
}
592+
```
593+
594+
### Step 3: Update Do-Script Templates
595+
596+
#### `templates/do/config`
597+
598+
Add the ARN vs plaintext export logic:
599+
600+
```bash
601+
<% if (pypiTokenArn) { %>
602+
# Private PyPI token — resolved from Secrets Manager at build-time
603+
export PIP_INDEX_TOKEN_ARN="<%= pypiTokenArn %>"
604+
<% } else if (pypiToken) { %>
605+
export PIP_INDEX_TOKEN="<%= pypiToken %>"
606+
<% } %>
607+
```
608+
609+
#### `templates/do/build`
610+
611+
Add a resolution block before `docker build` (alongside existing secret resolution blocks):
612+
613+
```bash
614+
if [ -n "${PIP_INDEX_TOKEN_ARN:-}" ]; then
615+
echo "🔐 Resolving Private PyPI token from Secrets Manager..."
616+
PIP_INDEX_TOKEN=$(aws secretsmanager get-secret-value \
617+
--secret-id "${PIP_INDEX_TOKEN_ARN}" \
618+
--query SecretString --output text) || {
619+
echo "❌ Failed to resolve Private PyPI token from Secrets Manager"
620+
exit 3
621+
}
622+
export PIP_INDEX_TOKEN
623+
fi
624+
```
625+
626+
Since `pypi-token` is build-time only, no changes are needed in `templates/do/serve`.
627+
628+
### Step 4: IAM Considerations
629+
630+
The existing IAM policy in `config/bootstrap-stack.json` already covers new secret types because it scopes to the `mlcc/*` naming prefix:
631+
632+
```json
633+
{
634+
"Sid": "SecretsManagerRead",
635+
"Effect": "Allow",
636+
"Action": [
637+
"secretsmanager:GetSecretValue",
638+
"secretsmanager:DescribeSecret"
639+
],
640+
"Resource": "arn:aws:secretsmanager:*:*:secret:mlcc/*",
641+
"Condition": {
642+
"StringEquals": {
643+
"aws:ResourceTag/mlcc:managed-by": "ml-container-creator"
644+
}
645+
}
646+
}
647+
```
648+
649+
No IAM changes are needed as long as your new secret follows the `mlcc/<type>/<label>` naming convention. The policy grants access to any secret under the `mlcc/` prefix that carries the `mlcc:managed-by` tag.
650+
651+
### Tagging Schema
652+
653+
Every secret created by the CLI automatically receives these tags:
654+
655+
| Tag Key | Value | Purpose |
656+
|---------|-------|---------|
657+
| `mlcc:managed-by` | `ml-container-creator` | Identifies mlcc-managed secrets for IAM scoping |
658+
| `mlcc:created-by` | `secrets` | Identifies the creation source |
659+
| `mlcc:secret-type` | `<identifier>` (e.g., `pypi-token`) | Links to the Secret_Classification entry |
660+
661+
These tags are applied automatically by the `SecretsCommandHandler` and cannot be overridden by the user. If a user provides conflicting `mlcc:` tags via `--json`, the system values take precedence and a warning is displayed.
662+
663+
### Naming Convention
664+
665+
All managed secrets follow the pattern:
666+
667+
```
668+
mlcc/<secret-type>/<user-provided-label>
669+
```
670+
671+
Examples:
672+
- `mlcc/hf-token/production`
673+
- `mlcc/ngc-token/team-shared`
674+
- `mlcc/pypi-token/ci-pipeline`
675+
676+
This naming convention maps directly to the IAM policy resource pattern `arn:aws:secretsmanager:*:*:secret:mlcc/*`, ensuring that any new secret type is automatically covered without IAM policy changes.
677+
678+
### Step 5: Add Tests
679+
680+
Add a test verifying the new entry has all required fields in `test/property/secret-classification-completeness.property.test.js` (the existing property test validates all entries automatically).
681+
682+
Add unit tests for the new do-script resolution logic in `test/unit/secrets-template-output.test.js`.
683+
684+
### Validation Checklist
685+
686+
After adding a new secret type, verify:
687+
688+
- [ ] The new type appears in `secrets create` interactive type selection prompt
689+
- [ ] `secrets create --type <new-type> --name test --secret-value xxx` creates a secret with the correct name (`mlcc/<type>/test`)
690+
- [ ] The prompt flow lists managed secrets of the new type during project generation
691+
- [ ] Do-scripts resolve the secret at the correct stage (build-time, runtime, or both)
692+
- [ ] The IAM policy covers the new naming path (automatic if using `mlcc/` prefix)
693+
- [ ] Mutual exclusion validation rejects both `--<type>` and `--<type>-arn` flags together
694+
- [ ] The `do/config` template exports the correct `_ARN` variable when an ARN is configured
695+
- [ ] Existing plaintext flows remain unchanged when no ARN is provided
696+
- [ ] All property tests pass (`npm test`)
697+
698+
### Files to Update Summary
699+
700+
| File | Change |
701+
|------|--------|
702+
| `src/lib/secret-classification.js` | Add new entry to `SECRET_CLASSIFICATIONS` array |
703+
| `bin/cli.js` | Add `--<type>-arn` and `--<type>` flags, add mutual exclusion check |
704+
| `templates/do/config` | Add ARN/plaintext export conditional |
705+
| `templates/do/build` | Add resolution block (if stages include `build-time`) |
706+
| `templates/do/serve` | Add resolution block (if stages include `runtime`) |
707+
| `test/unit/secrets-template-output.test.js` | Add tests for new template output |
708+
709+
---
710+
509711
## Testing Your Changes
510712
511713
### Unit Tests

0 commit comments

Comments
 (0)