Skip to content

Commit e9a1eda

Browse files
feat: rename job create to submit and add resolver layer for compute, code, and input resolution (#7205)
- Rename job create command to job submit for consistency with finetune extension - Add resolver interfaces: ComputeResolver, CodeResolver, InputResolver - Add JobResolver orchestrator that resolves all references in JobDefinition - Wire resolver into submit flow before buildJobResource() - Stub implementations guide users to provide full ARM IDs / remote URIs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent cc6f677 commit e9a1eda

7 files changed

Lines changed: 174 additions & 9 deletions

File tree

cli/azd/extensions/azure.ai.customtraining/extension.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ examples:
1212
- name: init
1313
description: Initialize project configuration for custom training.
1414
usage: azd ai training init
15-
- name: job create
15+
- name: job submit
1616
description: Submit a command job from YAML definition.
17-
usage: azd ai training job create --file job.yaml
17+
usage: azd ai training job submit --file job.yaml
1818
- name: job list
1919
description: List all training jobs.
2020
usage: azd ai training job list

cli/azd/extensions/azure.ai.customtraining/internal/cmd/job.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func newJobCommand() *cobra.Command {
3030
"Azure AI Foundry project endpoint URL (e.g., https://account.services.ai.azure.com/api/projects/project-name)")
3131

3232
cmd.AddCommand(newJobListCommand())
33-
cmd.AddCommand(newJobCreateCommand())
33+
cmd.AddCommand(newJobSubmitCommand())
3434
cmd.AddCommand(newJobShowCommand())
3535

3636
return cmd

cli/azd/extensions/azure.ai.customtraining/internal/cmd/job_create.go renamed to cli/azd/extensions/azure.ai.customtraining/internal/cmd/job_submit.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package cmd
66
import (
77
"fmt"
88

9+
"azure.ai.customtraining/internal/service"
910
"azure.ai.customtraining/internal/utils"
1011
"azure.ai.customtraining/pkg/client"
1112
"azure.ai.customtraining/pkg/models"
@@ -15,14 +16,14 @@ import (
1516
"github.com/spf13/cobra"
1617
)
1718

18-
func newJobCreateCommand() *cobra.Command {
19+
func newJobSubmitCommand() *cobra.Command {
1920
var filePath string
2021
var outputFormat string
2122

2223
cmd := &cobra.Command{
23-
Use: "create",
24-
Short: "Create a new training job from a YAML definition file",
25-
Long: "Create a new training job by providing a YAML job definition file.\n\nExample:\n azd ai training job create --file job.yaml",
24+
Use: "submit",
25+
Short: "Submit a new training job from a YAML definition file",
26+
Long: "Submit a new training job by providing a YAML job definition file.\n\nExample:\n azd ai training job submit --file job.yaml",
2627
Args: cobra.NoArgs,
2728
RunE: func(cmd *cobra.Command, args []string) error {
2829
ctx := azdext.WithAccessToken(cmd.Context())
@@ -75,17 +76,27 @@ func newJobCreateCommand() *cobra.Command {
7576
jobDef.Name = utils.GenerateJobName()
7677
}
7778

79+
// Resolve references (compute name → ARM ID, local paths → datastore URIs)
80+
resolver := service.NewJobResolver(
81+
service.NewDefaultComputeResolver(),
82+
service.NewDefaultCodeResolver(),
83+
service.NewDefaultInputResolver(),
84+
)
85+
if err := resolver.ResolveJobDefinition(ctx, jobDef); err != nil {
86+
return fmt.Errorf("failed to resolve job definition: %w", err)
87+
}
88+
7889
// Build REST payload from YAML definition
7990
jobResource := buildJobResource(jobDef)
8091

81-
fmt.Printf("Creating command job: %s\n\n", jobDef.Name)
92+
fmt.Printf("Submitting command job: %s\n\n", jobDef.Name)
8293

8394
result, err := apiClient.CreateOrUpdateJob(ctx, jobDef.Name, jobResource)
8495
if err != nil {
8596
return fmt.Errorf("failed to create job: %w", err)
8697
}
8798

88-
fmt.Printf("✓ Job '%s' created successfully\n\n", jobDef.Name)
99+
fmt.Printf("✓ Job '%s' submitted successfully\n\n", jobDef.Name)
89100

90101
if err := utils.PrintObject(result, utils.OutputFormat(outputFormat)); err != nil {
91102
return err
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package service
5+
6+
import (
7+
"context"
8+
"fmt"
9+
)
10+
11+
// DefaultCodeResolver is a stub implementation of CodeResolver.
12+
// Replace with actual datastore upload logic to resolve local code path to asset ID.
13+
type DefaultCodeResolver struct{}
14+
15+
func NewDefaultCodeResolver() *DefaultCodeResolver {
16+
return &DefaultCodeResolver{}
17+
}
18+
19+
func (r *DefaultCodeResolver) ResolveCode(ctx context.Context, codePath string) (string, error) {
20+
return "", fmt.Errorf("code resolution not implemented: provide a remote URI for code path '%s'", codePath)
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package service
5+
6+
import (
7+
"context"
8+
"fmt"
9+
)
10+
11+
// DefaultComputeResolver is a stub implementation of ComputeResolver.
12+
// Replace with actual ARM API call to resolve compute name to ARM resource ID.
13+
type DefaultComputeResolver struct{}
14+
15+
func NewDefaultComputeResolver() *DefaultComputeResolver {
16+
return &DefaultComputeResolver{}
17+
}
18+
19+
func (r *DefaultComputeResolver) ResolveCompute(ctx context.Context, computeName string) (string, error) {
20+
return "", fmt.Errorf("compute resolution not implemented: provide a full ARM resource ID for compute '%s'", computeName)
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package service
5+
6+
import (
7+
"context"
8+
"fmt"
9+
)
10+
11+
// DefaultInputResolver is a stub implementation of InputResolver.
12+
// Replace with actual datastore upload logic to resolve local input paths to datastore URIs.
13+
type DefaultInputResolver struct{}
14+
15+
func NewDefaultInputResolver() *DefaultInputResolver {
16+
return &DefaultInputResolver{}
17+
}
18+
19+
func (r *DefaultInputResolver) ResolveInput(ctx context.Context, inputPath string, inputType string) (string, error) {
20+
return "", fmt.Errorf("input resolution not implemented: provide a remote URI for input path '%s'", inputPath)
21+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package service
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"strings"
10+
11+
"azure.ai.customtraining/internal/utils"
12+
)
13+
14+
// ComputeResolver resolves a user-friendly compute name to a full ARM resource ID.
15+
type ComputeResolver interface {
16+
ResolveCompute(ctx context.Context, computeName string) (armID string, err error)
17+
}
18+
19+
// CodeResolver resolves a local code path to a datastore asset ID.
20+
type CodeResolver interface {
21+
ResolveCode(ctx context.Context, codePath string) (codeID string, err error)
22+
}
23+
24+
// InputResolver resolves a local input path to a datastore URI.
25+
type InputResolver interface {
26+
ResolveInput(ctx context.Context, inputPath string, inputType string) (uri string, err error)
27+
}
28+
29+
// JobResolver orchestrates resolution of all references in a JobDefinition.
30+
type JobResolver struct {
31+
compute ComputeResolver
32+
code CodeResolver
33+
input InputResolver
34+
}
35+
36+
// NewJobResolver creates a new JobResolver with the given resolver implementations.
37+
func NewJobResolver(compute ComputeResolver, code CodeResolver, input InputResolver) *JobResolver {
38+
return &JobResolver{
39+
compute: compute,
40+
code: code,
41+
input: input,
42+
}
43+
}
44+
45+
// ResolveJobDefinition resolves all references (compute, code, inputs) in the job definition in place.
46+
func (r *JobResolver) ResolveJobDefinition(ctx context.Context, jobDef *utils.JobDefinition) error {
47+
// Resolve compute: simple name → ARM ID
48+
if jobDef.Compute != "" && !isARMResourceID(jobDef.Compute) {
49+
armID, err := r.compute.ResolveCompute(ctx, jobDef.Compute)
50+
if err != nil {
51+
return fmt.Errorf("failed to resolve compute '%s': %w", jobDef.Compute, err)
52+
}
53+
jobDef.Compute = armID
54+
}
55+
56+
// Resolve code: local path → datastore asset ID
57+
if jobDef.Code != "" && !isRemoteURI(jobDef.Code) {
58+
codeID, err := r.code.ResolveCode(ctx, jobDef.Code)
59+
if err != nil {
60+
return fmt.Errorf("failed to resolve code path '%s': %w", jobDef.Code, err)
61+
}
62+
jobDef.Code = codeID
63+
}
64+
65+
// Resolve inputs: local paths → datastore URIs
66+
for name, input := range jobDef.Inputs {
67+
if input.Path != "" && !isRemoteURI(input.Path) && input.Value == "" {
68+
uri, err := r.input.ResolveInput(ctx, input.Path, input.Type)
69+
if err != nil {
70+
return fmt.Errorf("failed to resolve input '%s' path '%s': %w", name, input.Path, err)
71+
}
72+
input.Path = uri
73+
jobDef.Inputs[name] = input
74+
}
75+
}
76+
77+
return nil
78+
}
79+
80+
// isARMResourceID checks if a string is a full ARM resource ID.
81+
func isARMResourceID(s string) bool {
82+
return strings.HasPrefix(strings.ToLower(s), "/subscriptions/")
83+
}
84+
85+
// isRemoteURI checks if a string is a remote URI (not a local path).
86+
func isRemoteURI(s string) bool {
87+
lower := strings.ToLower(s)
88+
return strings.HasPrefix(lower, "azureml://") ||
89+
strings.HasPrefix(lower, "https://") ||
90+
strings.HasPrefix(lower, "http://")
91+
}

0 commit comments

Comments
 (0)