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
4 changes: 2 additions & 2 deletions cli/azd/extensions/azure.ai.customtraining/extension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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())
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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://")
}