diff --git a/cli/azd/extensions/azure.ai.customtraining/extension.yaml b/cli/azd/extensions/azure.ai.customtraining/extension.yaml index d5d957abbf3..8874c1a9b8a 100644 --- a/cli/azd/extensions/azure.ai.customtraining/extension.yaml +++ b/cli/azd/extensions/azure.ai.customtraining/extension.yaml @@ -12,9 +12,9 @@ examples: - name: init description: Initialize project configuration for custom training. usage: azd ai training init - - name: job create + - name: job submit description: Submit a command job from YAML definition. - usage: azd ai training job create --file job.yaml + usage: azd ai training job submit --file job.yaml - name: job list description: List all training jobs. usage: azd ai training job list diff --git a/cli/azd/extensions/azure.ai.customtraining/internal/cmd/job.go b/cli/azd/extensions/azure.ai.customtraining/internal/cmd/job.go index 5f96f40e815..e3c1e220fd1 100644 --- a/cli/azd/extensions/azure.ai.customtraining/internal/cmd/job.go +++ b/cli/azd/extensions/azure.ai.customtraining/internal/cmd/job.go @@ -30,7 +30,7 @@ func newJobCommand() *cobra.Command { "Azure AI Foundry project endpoint URL (e.g., https://account.services.ai.azure.com/api/projects/project-name)") cmd.AddCommand(newJobListCommand()) - cmd.AddCommand(newJobCreateCommand()) + cmd.AddCommand(newJobSubmitCommand()) cmd.AddCommand(newJobShowCommand()) return cmd diff --git a/cli/azd/extensions/azure.ai.customtraining/internal/cmd/job_create.go b/cli/azd/extensions/azure.ai.customtraining/internal/cmd/job_submit.go similarity index 84% rename from cli/azd/extensions/azure.ai.customtraining/internal/cmd/job_create.go rename to cli/azd/extensions/azure.ai.customtraining/internal/cmd/job_submit.go index fd687c9388d..dd39be83821 100644 --- a/cli/azd/extensions/azure.ai.customtraining/internal/cmd/job_create.go +++ b/cli/azd/extensions/azure.ai.customtraining/internal/cmd/job_submit.go @@ -6,6 +6,7 @@ package cmd import ( "fmt" + "azure.ai.customtraining/internal/service" "azure.ai.customtraining/internal/utils" "azure.ai.customtraining/pkg/client" "azure.ai.customtraining/pkg/models" @@ -15,14 +16,14 @@ import ( "github.com/spf13/cobra" ) -func newJobCreateCommand() *cobra.Command { +func newJobSubmitCommand() *cobra.Command { var filePath string var outputFormat string cmd := &cobra.Command{ - Use: "create", - Short: "Create a new training job from a YAML definition file", - Long: "Create a new training job by providing a YAML job definition file.\n\nExample:\n azd ai training job create --file job.yaml", + Use: "submit", + Short: "Submit a new training job from a YAML definition file", + Long: "Submit a new training job by providing a YAML job definition file.\n\nExample:\n azd ai training job submit --file job.yaml", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { ctx := azdext.WithAccessToken(cmd.Context()) @@ -75,17 +76,27 @@ func newJobCreateCommand() *cobra.Command { jobDef.Name = utils.GenerateJobName() } + // Resolve references (compute name → ARM ID, local paths → datastore URIs) + resolver := service.NewJobResolver( + service.NewDefaultComputeResolver(), + service.NewDefaultCodeResolver(), + service.NewDefaultInputResolver(), + ) + if err := resolver.ResolveJobDefinition(ctx, jobDef); err != nil { + return fmt.Errorf("failed to resolve job definition: %w", err) + } + // Build REST payload from YAML definition jobResource := buildJobResource(jobDef) - fmt.Printf("Creating command job: %s\n\n", jobDef.Name) + fmt.Printf("Submitting command job: %s\n\n", jobDef.Name) result, err := apiClient.CreateOrUpdateJob(ctx, jobDef.Name, jobResource) if err != nil { return fmt.Errorf("failed to create job: %w", err) } - fmt.Printf("✓ Job '%s' created successfully\n\n", jobDef.Name) + fmt.Printf("✓ Job '%s' submitted successfully\n\n", jobDef.Name) if err := utils.PrintObject(result, utils.OutputFormat(outputFormat)); err != nil { return err diff --git a/cli/azd/extensions/azure.ai.customtraining/internal/service/code_resolver.go b/cli/azd/extensions/azure.ai.customtraining/internal/service/code_resolver.go new file mode 100644 index 00000000000..d96c4615326 --- /dev/null +++ b/cli/azd/extensions/azure.ai.customtraining/internal/service/code_resolver.go @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package service + +import ( + "context" + "fmt" +) + +// DefaultCodeResolver is a stub implementation of CodeResolver. +// Replace with actual datastore upload logic to resolve local code path to asset ID. +type DefaultCodeResolver struct{} + +func NewDefaultCodeResolver() *DefaultCodeResolver { + return &DefaultCodeResolver{} +} + +func (r *DefaultCodeResolver) ResolveCode(ctx context.Context, codePath string) (string, error) { + return "", fmt.Errorf("code resolution not implemented: provide a remote URI for code path '%s'", codePath) +} diff --git a/cli/azd/extensions/azure.ai.customtraining/internal/service/compute_resolver.go b/cli/azd/extensions/azure.ai.customtraining/internal/service/compute_resolver.go new file mode 100644 index 00000000000..377264364d6 --- /dev/null +++ b/cli/azd/extensions/azure.ai.customtraining/internal/service/compute_resolver.go @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package service + +import ( + "context" + "fmt" +) + +// DefaultComputeResolver is a stub implementation of ComputeResolver. +// Replace with actual ARM API call to resolve compute name to ARM resource ID. +type DefaultComputeResolver struct{} + +func NewDefaultComputeResolver() *DefaultComputeResolver { + return &DefaultComputeResolver{} +} + +func (r *DefaultComputeResolver) ResolveCompute(ctx context.Context, computeName string) (string, error) { + return "", fmt.Errorf("compute resolution not implemented: provide a full ARM resource ID for compute '%s'", computeName) +} diff --git a/cli/azd/extensions/azure.ai.customtraining/internal/service/input_resolver.go b/cli/azd/extensions/azure.ai.customtraining/internal/service/input_resolver.go new file mode 100644 index 00000000000..0e0becae558 --- /dev/null +++ b/cli/azd/extensions/azure.ai.customtraining/internal/service/input_resolver.go @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package service + +import ( + "context" + "fmt" +) + +// DefaultInputResolver is a stub implementation of InputResolver. +// Replace with actual datastore upload logic to resolve local input paths to datastore URIs. +type DefaultInputResolver struct{} + +func NewDefaultInputResolver() *DefaultInputResolver { + return &DefaultInputResolver{} +} + +func (r *DefaultInputResolver) ResolveInput(ctx context.Context, inputPath string, inputType string) (string, error) { + return "", fmt.Errorf("input resolution not implemented: provide a remote URI for input path '%s'", inputPath) +} diff --git a/cli/azd/extensions/azure.ai.customtraining/internal/service/resolver.go b/cli/azd/extensions/azure.ai.customtraining/internal/service/resolver.go new file mode 100644 index 00000000000..65c83a03097 --- /dev/null +++ b/cli/azd/extensions/azure.ai.customtraining/internal/service/resolver.go @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package service + +import ( + "context" + "fmt" + "strings" + + "azure.ai.customtraining/internal/utils" +) + +// ComputeResolver resolves a user-friendly compute name to a full ARM resource ID. +type ComputeResolver interface { + ResolveCompute(ctx context.Context, computeName string) (armID string, err error) +} + +// CodeResolver resolves a local code path to a datastore asset ID. +type CodeResolver interface { + ResolveCode(ctx context.Context, codePath string) (codeID string, err error) +} + +// InputResolver resolves a local input path to a datastore URI. +type InputResolver interface { + ResolveInput(ctx context.Context, inputPath string, inputType string) (uri string, err error) +} + +// JobResolver orchestrates resolution of all references in a JobDefinition. +type JobResolver struct { + compute ComputeResolver + code CodeResolver + input InputResolver +} + +// NewJobResolver creates a new JobResolver with the given resolver implementations. +func NewJobResolver(compute ComputeResolver, code CodeResolver, input InputResolver) *JobResolver { + return &JobResolver{ + compute: compute, + code: code, + input: input, + } +} + +// ResolveJobDefinition resolves all references (compute, code, inputs) in the job definition in place. +func (r *JobResolver) ResolveJobDefinition(ctx context.Context, jobDef *utils.JobDefinition) error { + // Resolve compute: simple name → ARM ID + if jobDef.Compute != "" && !isARMResourceID(jobDef.Compute) { + armID, err := r.compute.ResolveCompute(ctx, jobDef.Compute) + if err != nil { + return fmt.Errorf("failed to resolve compute '%s': %w", jobDef.Compute, err) + } + jobDef.Compute = armID + } + + // Resolve code: local path → datastore asset ID + if jobDef.Code != "" && !isRemoteURI(jobDef.Code) { + codeID, err := r.code.ResolveCode(ctx, jobDef.Code) + if err != nil { + return fmt.Errorf("failed to resolve code path '%s': %w", jobDef.Code, err) + } + jobDef.Code = codeID + } + + // Resolve inputs: local paths → datastore URIs + for name, input := range jobDef.Inputs { + if input.Path != "" && !isRemoteURI(input.Path) && input.Value == "" { + uri, err := r.input.ResolveInput(ctx, input.Path, input.Type) + if err != nil { + return fmt.Errorf("failed to resolve input '%s' path '%s': %w", name, input.Path, err) + } + input.Path = uri + jobDef.Inputs[name] = input + } + } + + return nil +} + +// isARMResourceID checks if a string is a full ARM resource ID. +func isARMResourceID(s string) bool { + return strings.HasPrefix(strings.ToLower(s), "/subscriptions/") +} + +// isRemoteURI checks if a string is a remote URI (not a local path). +func isRemoteURI(s string) bool { + lower := strings.ToLower(s) + return strings.HasPrefix(lower, "azureml://") || + strings.HasPrefix(lower, "https://") || + strings.HasPrefix(lower, "http://") +}