Skip to content

Commit d621e52

Browse files
author
Sujit Kamireddy
committed
update deploy
1 parent aaa8d4f commit d621e52

2 files changed

Lines changed: 124 additions & 2 deletions

File tree

cli/azd/extensions/azure.ai.rle/internal/cmd/client.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"bytes"
88
"context"
99
"encoding/json"
10+
"errors"
1011
"fmt"
1112
"io"
1213
"net/http"
@@ -130,6 +131,14 @@ func (e *rleHTTPError) Error() string {
130131
return fmt.Sprintf("RLE control plane returned HTTP %d: %s", e.statusCode, strings.TrimSpace(e.body))
131132
}
132133

134+
// isNotFoundError reports whether err is an RLE control plane error with HTTP 404 status.
135+
func isNotFoundError(err error) bool {
136+
if httpErr, ok := errors.AsType[*rleHTTPError](err); ok {
137+
return httpErr.statusCode == http.StatusNotFound
138+
}
139+
return false
140+
}
141+
133142
func newRleClient(endpoint string) *rleClient {
134143
return &rleClient{
135144
baseUrl: strings.TrimRight(endpoint, "/"),

cli/azd/extensions/azure.ai.rle/internal/cmd/deploy.go

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@ import (
88
"errors"
99
"fmt"
1010
"os"
11+
"os/exec"
12+
"strings"
1113

14+
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
1215
"github.com/spf13/cobra"
1316
)
1417

1518
func newDeployCommand() *cobra.Command {
19+
flags := struct {
20+
registry string
21+
skipBuild bool
22+
}{}
23+
1624
cmd := &cobra.Command{
1725
Use: "deploy",
1826
Short: "Create or update the RLE environment",
@@ -42,6 +50,24 @@ func newDeployCommand() *cobra.Command {
4250
if err != nil {
4351
return err
4452
}
53+
54+
// Host-qualify the image so the control plane / ADC can pull it: the disk-image
55+
// conversion uses the image reference verbatim as the pull source, so a bare
56+
// "name:tag" is rewritten to "<registry-login-server>/name:tag".
57+
loginServer, repoTag := splitImageHost(image)
58+
if loginServer == "" {
59+
loginServer = normalizeRegistryLoginServer(flags.registry)
60+
}
61+
if loginServer == "" {
62+
return &azdext.LocalError{
63+
Message: "No container registry was specified for the environment image.",
64+
Code: "rle_registry_required",
65+
Category: azdext.LocalErrorCategoryUser,
66+
Suggestion: "Pass --registry <name> (e.g. devrle) or use a host-qualified image reference.",
67+
}
68+
}
69+
image = loginServer + "/" + repoTag
70+
4571
environmentId := firstNonEmpty(state.EnvironmentId, slug(state.Name))
4672
client := newRleClient(resolveControlPlaneEndpoint(state.Endpoint))
4773
request := v1EnvironmentRequest{
@@ -55,16 +81,57 @@ func newDeployCommand() *cobra.Command {
5581
if !created {
5682
action = "Updating"
5783
}
58-
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "Skipping build; using existing image '%s'.\n", image); err != nil {
59-
return err
84+
85+
// Build the local Dockerfile and push it to the registry before registering, so the
86+
// environment always points at an image that actually exists in the target ACR.
87+
registryName := registryShortName(loginServer)
88+
switch {
89+
case flags.skipBuild:
90+
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "Skipping build (--skip-build); using image '%s'.\n", image); err != nil {
91+
return err
92+
}
93+
case !fileExists("Dockerfile"):
94+
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "No Dockerfile in current directory; skipping build and using image '%s'.\n", image); err != nil {
95+
return err
96+
}
97+
default:
98+
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "Building and pushing '%s' to registry '%s' (az acr build) ...\n", repoTag, registryName); err != nil {
99+
return err
100+
}
101+
build := exec.CommandContext(cmd.Context(), "az", "acr", "build", "--registry", registryName, "--image", repoTag, ".")
102+
build.Stdout = cmd.OutOrStdout()
103+
build.Stderr = cmd.ErrOrStderr()
104+
if err := build.Run(); err != nil {
105+
return &azdext.LocalError{
106+
Message: fmt.Sprintf("Failed to build and push image '%s' to registry '%s': %v", repoTag, registryName, err),
107+
Code: "rle_acr_build_failed",
108+
Category: azdext.LocalErrorCategoryUser,
109+
Suggestion: "Ensure 'az login' is done, you have push access to the registry, and a valid Dockerfile is present. Use --skip-build to register a prebuilt image.",
110+
}
111+
}
60112
}
113+
61114
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s environment '%s' (image=%s) ...\n", action, state.Name, image); err != nil {
62115
return err
63116
}
64117
if state.EnvironmentId == "" {
65118
environment, err = client.createV1Environment(cmd.Context(), state.Project, request)
66119
} else {
67120
environment, err = client.updateV1Environment(cmd.Context(), state.Project, environmentId, request)
121+
if isNotFoundError(err) {
122+
// The recorded environment no longer exists in the target project
123+
// (e.g. the project changed or the control plane was reset). Recreate it.
124+
if _, msgErr := fmt.Fprintf(
125+
cmd.OutOrStdout(),
126+
"Environment '%s' not found in project '%s'; creating a new one.\n",
127+
environmentId,
128+
state.Project,
129+
); msgErr != nil {
130+
return msgErr
131+
}
132+
created = true
133+
environment, err = client.createV1Environment(cmd.Context(), state.Project, request)
134+
}
68135
}
69136
if err != nil {
70137
return serviceError(err)
@@ -103,9 +170,55 @@ func newDeployCommand() *cobra.Command {
103170
},
104171
}
105172

173+
cmd.Flags().StringVar(&flags.registry, "registry", "devrle",
174+
"Container registry (short name or login server) to build and push the environment image into")
175+
cmd.Flags().BoolVar(&flags.skipBuild, "skip-build", false,
176+
"Skip building/pushing the image and register the existing image reference as-is")
177+
106178
return cmd
107179
}
108180

181+
// splitImageHost splits a container image reference into its registry host (login server)
182+
// and the remaining repository:tag. The leading segment is treated as a host only when it
183+
// looks like one (contains '.' or ':' or is "localhost"); otherwise there is no host.
184+
func splitImageHost(image string) (host string, repoTag string) {
185+
image = strings.TrimSpace(image)
186+
if slash := strings.IndexByte(image, '/'); slash > 0 {
187+
first := image[:slash]
188+
if first == "localhost" || strings.ContainsAny(first, ".:") {
189+
return first, image[slash+1:]
190+
}
191+
}
192+
return "", image
193+
}
194+
195+
// normalizeRegistryLoginServer turns a registry short name ("devrle") into a login server
196+
// ("devrle.azurecr.io"). A value that already contains a '.' is assumed to be a login server.
197+
func normalizeRegistryLoginServer(registry string) string {
198+
registry = strings.TrimSpace(registry)
199+
if registry == "" {
200+
return ""
201+
}
202+
if strings.Contains(registry, ".") {
203+
return registry
204+
}
205+
return registry + ".azurecr.io"
206+
}
207+
208+
// registryShortName returns the ACR short name (the part before ".azurecr.io") for use with
209+
// "az acr build --registry".
210+
func registryShortName(loginServer string) string {
211+
if host, _, ok := strings.Cut(loginServer, "."); ok {
212+
return host
213+
}
214+
return loginServer
215+
}
216+
217+
func fileExists(path string) bool {
218+
info, err := os.Stat(path)
219+
return err == nil && !info.IsDir()
220+
}
221+
109222
type environmentOutput struct {
110223
Id string `json:"id"`
111224
ProjectId string `json:"projectId"`

0 commit comments

Comments
 (0)