Skip to content

Commit d5b0831

Browse files
authored
Merge pull request #1421 from Dstack-TEE/fix/annotation-cli-duplicates
fix(vmm): refuse repeated annotation keys; carry annotations in the UI
2 parents 1b6a4fe + 5c33813 commit d5b0831

4 files changed

Lines changed: 60 additions & 20 deletions

File tree

‎dstack/vmm/src/vmm-cli.py‎

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -372,17 +372,19 @@ def encrypt_env(envs, hex_public_key: str) -> str:
372372
return result.hex()
373373

374374

375-
def parse_annotations(items: Optional[List[str]]) -> Dict[str, str]:
376-
"""Parse repeated KEY=VALUE annotation arguments."""
377-
annotations = {}
378-
for item in items or []:
379-
key, sep, value = item.partition("=")
375+
class AnnotationAction(argparse.Action):
376+
"""Collect repeated KEY=VALUE arguments, refusing a key given twice."""
377+
378+
def __call__(self, parser, namespace, value, option_string=None):
379+
"""Add one annotation to the namespace."""
380+
key, sep, val = value.partition("=")
380381
if not sep or not key:
381-
raise argparse.ArgumentTypeError(
382-
f"invalid annotation {item!r}, expected KEY=VALUE"
383-
)
384-
annotations[key] = value
385-
return annotations
382+
parser.error(f"{option_string}: expected KEY=VALUE, got {value!r}")
383+
annotations = getattr(namespace, self.dest) or {}
384+
if key in annotations:
385+
parser.error(f"{option_string}: {key!r} given twice")
386+
annotations[key] = val
387+
setattr(namespace, self.dest, annotations)
386388

387389

388390
def parse_port_mapping(port_str: str) -> Dict:
@@ -996,7 +998,7 @@ def create_vm(self, args) -> None:
996998
"pin_numa": args.pin_numa,
997999
"stopped": args.stopped,
9981000
"no_tee": args.no_tee,
999-
"annotations": parse_annotations(args.annotation),
1001+
"annotations": args.annotation or {},
10001002
}
10011003
if args.disk_prealloc:
10021004
params["disk_prealloc"] = args.disk_prealloc
@@ -2049,7 +2051,7 @@ def _patched_format_help():
20492051
)
20502052
deploy_parser.add_argument(
20512053
"--annotation",
2052-
action="append",
2054+
action=AnnotationAction,
20532055
metavar="KEY=VALUE",
20542056
help="Annotation for host-side tooling (can be repeated)",
20552057
)
@@ -2326,7 +2328,7 @@ def _patched_format_help():
23262328
annotations_group = update_parser.add_mutually_exclusive_group()
23272329
annotations_group.add_argument(
23282330
"--annotation",
2329-
action="append",
2331+
action=AnnotationAction,
23302332
metavar="KEY=VALUE",
23312333
help="Replace the annotations with these (can be repeated)",
23322334
)
@@ -2430,13 +2432,7 @@ def _patched_format_help():
24302432
net_vhost=args.net_vhost,
24312433
net_vhost_inherit=getattr(args, "net_vhost_inherit", False),
24322434
net_queues=args.net_queues,
2433-
annotations=(
2434-
{}
2435-
if args.no_annotations
2436-
else parse_annotations(args.annotation)
2437-
if args.annotation
2438-
else None
2439-
),
2435+
annotations={} if args.no_annotations else args.annotation,
24402436
)
24412437
elif args.command == "kms":
24422438
if not args.kms_action:

‎dstack/vmm/ui/src/components/CreateVmDialog.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,11 @@ const CreateVmDialogComponent = {
197197
<textarea id="userConfig" v-model="form.user_config" placeholder="Optional user config placed at /dstack/.user-config in the CVM"></textarea>
198198
</div>
199199
200+
<div class="form-group full-width">
201+
<label for="annotations">Annotations</label>
202+
<textarea id="annotations" v-model="form.annotations" placeholder="Optional KEY=VALUE per line, for host-side tooling; never passed to the CVM"></textarea>
203+
</div>
204+
200205
<div class="form-group full-width" v-if="availableGpus.length > 0">
201206
<gpu-config-editor
202207
:available-gpus="availableGpus"

‎dstack/vmm/ui/src/composables/useVmManager.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,29 @@ type AppCompose = {
3636
event_log_version?: number;
3737
};
3838

39+
/** Parses one `KEY=VALUE` annotation per line, refusing a key given twice. */
40+
function parseAnnotations(text: string): Record<string, string> {
41+
const annotations = new Map<string, string>();
42+
for (const line of text.split('\n').map((l) => l.trim()).filter(Boolean)) {
43+
const eq = line.indexOf('=');
44+
if (eq <= 0) {
45+
throw new Error(`Invalid annotation "${line}", expected KEY=VALUE`);
46+
}
47+
const key = line.slice(0, eq);
48+
if (annotations.has(key)) {
49+
throw new Error(`Annotation "${key}" given twice`);
50+
}
51+
annotations.set(key, line.slice(eq + 1));
52+
}
53+
return Object.fromEntries(annotations);
54+
}
55+
56+
function annotationsText(annotations: Record<string, string> | null | undefined): string {
57+
return Object.entries(annotations || {})
58+
.map(([key, value]) => `${key}=${value}`)
59+
.join('\n');
60+
}
61+
3962
function initScriptList(initScript: string | string[] | undefined): string[] {
4063
const scripts = Array.isArray(initScript)
4164
? [...initScript]
@@ -167,6 +190,7 @@ type VmFormState = {
167190
disk_prealloc: string;
168191
networks: NetworkFormEntry[];
169192
user_config: string;
193+
annotations: string;
170194
kms_urls: string[];
171195
gateway_urls: string[];
172196
stopped: boolean;
@@ -259,6 +283,7 @@ function createVmFormState(preLaunchScript: string): VmFormState {
259283
disk_prealloc: '',
260284
networks: [],
261285
user_config: '',
286+
annotations: '',
262287
kms_urls: [],
263288
gateway_urls: [],
264289
stopped: false,
@@ -630,6 +655,7 @@ type CreateVmPayloadSource = {
630655
kms_urls?: string[];
631656
gateway_urls?: string[];
632657
stopped?: boolean;
658+
annotations?: Record<string, string>;
633659
};
634660

635661
function buildCreateVmPayload(source: CreateVmPayloadSource): VmmTypes.IVmConfiguration {
@@ -657,6 +683,7 @@ type CreateVmPayloadSource = {
657683
kms_urls: source.kms_urls?.filter((url) => url && url.trim().length) ?? [],
658684
gateway_urls: source.gateway_urls?.filter((url) => url && url.trim().length) ?? [],
659685
stopped: !!source.stopped,
686+
annotations: source.annotations ?? {},
660687
};
661688
}
662689

@@ -1126,6 +1153,8 @@ type CreateVmPayloadSource = {
11261153
// and inheriting "full" unannounced costs the next deploy a full write of
11271154
// its disk before the VM boots.
11281155
vmForm.value.disk_prealloc = '';
1156+
// And a clone's annotations are parameters for that VM's host tooling.
1157+
vmForm.value.annotations = '';
11291158
vmForm.value.swapValue = 0;
11301159
vmForm.value.swapUnit = 'GB';
11311160
vmForm.value.swap_size = 0;
@@ -1265,6 +1294,7 @@ type CreateVmPayloadSource = {
12651294
kms_urls: vmForm.value.kms_urls,
12661295
gateway_urls: vmForm.value.gateway_urls,
12671296
stopped: vmForm.value.stopped,
1297+
annotations: parseAnnotations(vmForm.value.annotations),
12681298
});
12691299

12701300
await vmmRpc.createVm(payload);
@@ -1420,6 +1450,7 @@ type CreateVmPayloadSource = {
14201450
no_tee: !!config.no_tee,
14211451
simulated_tee: config.simulated_tee || '',
14221452
user_config: config.user_config || '',
1453+
annotations: annotationsText(config.annotations),
14231454
stopped: !!config.stopped,
14241455
event_log_version: theVm.appCompose?.event_log_version || 1,
14251456
};

‎dstack/vmm/ui/src/templates/app.html‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,14 @@ <h4>User Config</h4>
483483
<pre class="user-config-content">{{ vm.configuration.user_config }}</pre>
484484
</div>
485485

486+
<div v-if="Object.keys(vm.configuration?.annotations || {}).length" class="user-config-section">
487+
<div class="section-header">
488+
<h4>Annotations</h4>
489+
</div>
490+
<pre class="user-config-content"><template v-for="(value, key) in vm.configuration.annotations">{{ key }}={{ value }}
491+
</template></pre>
492+
</div>
493+
486494
<div class="vm-log-tabs">
487495
<button class="vm-log-button" @click="showLogs(vm.id, 'serial')">Serial Logs</button>
488496
<button class="vm-log-button" @click="showLogs(vm.id, 'stdout')">Stdout</button>

0 commit comments

Comments
 (0)