@@ -8,14 +8,18 @@ import (
88 "errors"
99 "fmt"
1010 "os"
11+ "os/exec"
12+ "strings"
1113
1214 "github.com/azure/azure-dev/cli/azd/pkg/azdext"
1315 "github.com/spf13/cobra"
1416)
1517
1618type rleDeployFlags struct {
17- project string
18- image string
19+ project string
20+ image string
21+ registry string
22+ skipBuild bool
1923}
2024
2125func newDeployCommand () * cobra.Command {
@@ -97,6 +101,23 @@ func newDeployCommand() *cobra.Command {
97101 image = defaultRegistryLoginServer + "/" + slug (state .Name ) + ":latest"
98102 }
99103
104+ // Host-qualify the image so the control plane / ADC can pull it: the disk-image
105+ // conversion uses the image reference verbatim as the pull source, so a bare
106+ // "name:tag" is rewritten to "<registry-login-server>/name:tag".
107+ loginServer , repoTag := splitImageHost (image )
108+ if loginServer == "" {
109+ loginServer = normalizeRegistryLoginServer (flags .registry )
110+ }
111+ if loginServer == "" {
112+ return & azdext.LocalError {
113+ Message : "No container registry was specified for the environment image." ,
114+ Code : "rle_registry_required" ,
115+ Category : azdext .LocalErrorCategoryUser ,
116+ Suggestion : "Pass --registry <name> (e.g. devrle) or use a host-qualified image reference." ,
117+ }
118+ }
119+ image = loginServer + "/" + repoTag
120+
100121 environmentId := firstNonEmpty (state .EnvironmentId , slug (state .Name ))
101122 client := newRleClient (resolveControlPlaneEndpoint ("" ))
102123 request := v1EnvironmentRequest {
@@ -111,8 +132,41 @@ func newDeployCommand() *cobra.Command {
111132 action = "Updating"
112133 }
113134
114- if _ , err := fmt .Fprintf (cmd .OutOrStdout (), "Skipping build; using existing image '%s'.\n " , image ); err != nil {
115- return err
135+ // Build the local Dockerfile and push it to the registry before registering, so the
136+ // environment always points at an image that actually exists in the target ACR.
137+ registryName := registryShortName (loginServer )
138+ switch {
139+ case flags .skipBuild :
140+ if _ , err := fmt .Fprintf (cmd .OutOrStdout (),
141+ "Skipping build (--skip-build); using image '%s'.\n " , image ); err != nil {
142+ return err
143+ }
144+ case ! fileExists ("Dockerfile" ):
145+ if _ , err := fmt .Fprintf (cmd .OutOrStdout (),
146+ "No Dockerfile in current directory; skipping build and using image '%s'.\n " , image ); err != nil {
147+ return err
148+ }
149+ default :
150+ if _ , err := fmt .Fprintf (cmd .OutOrStdout (),
151+ "Building and pushing '%s' to registry '%s' (az acr build) ...\n " ,
152+ repoTag , registryName ); err != nil {
153+ return err
154+ }
155+ build := exec .CommandContext (cmd .Context (),
156+ "az" , "acr" , "build" , "--registry" , registryName , "--image" , repoTag , "." )
157+ build .Stdout = cmd .OutOrStdout ()
158+ build .Stderr = cmd .ErrOrStderr ()
159+ if err := build .Run (); err != nil {
160+ return & azdext.LocalError {
161+ Message : fmt .Sprintf (
162+ "Failed to build and push image '%s' to registry '%s': %v" ,
163+ repoTag , registryName , err ),
164+ Code : "rle_acr_build_failed" ,
165+ Category : azdext .LocalErrorCategoryUser ,
166+ Suggestion : "Ensure 'az login' is done, you have push access to the registry, " +
167+ "and a valid Dockerfile is present. Use --skip-build to register a prebuilt image." ,
168+ }
169+ }
116170 }
117171
118172 if _ , err := fmt .Fprintf (cmd .OutOrStdout (), "%s environment '%s' (image=%s) ...\n " , action , state .Name , image ); err != nil {
@@ -178,9 +232,54 @@ func newDeployCommand() *cobra.Command {
178232 "RLE project name. Defaults to the project saved in .azd-rle.json." )
179233 cmd .Flags ().StringVar (& flags .image , "image" , "" ,
180234 "Image reference to deploy (overrides the per-environment default derived from the environment name)" )
235+ cmd .Flags ().StringVar (& flags .registry , "registry" , "devrle" ,
236+ "Container registry (short name or login server) to build and push the environment image into" )
237+ cmd .Flags ().BoolVar (& flags .skipBuild , "skip-build" , false ,
238+ "Skip building/pushing the image and register the existing image reference as-is" )
181239 return cmd
182240}
183241
242+ // splitImageHost splits a container image reference into its registry host (login server)
243+ // and the remaining repository:tag. The leading segment is treated as a host only when it
244+ // looks like one (contains '.' or ':' or is "localhost"); otherwise there is no host.
245+ func splitImageHost (image string ) (host string , repoTag string ) {
246+ image = strings .TrimSpace (image )
247+ if slash := strings .IndexByte (image , '/' ); slash > 0 {
248+ first := image [:slash ]
249+ if first == "localhost" || strings .ContainsAny (first , ".:" ) {
250+ return first , image [slash + 1 :]
251+ }
252+ }
253+ return "" , image
254+ }
255+
256+ // normalizeRegistryLoginServer turns a registry short name ("devrle") into a login server
257+ // ("devrle.azurecr.io"). A value that already contains a '.' is assumed to be a login server.
258+ func normalizeRegistryLoginServer (registry string ) string {
259+ registry = strings .TrimSpace (registry )
260+ if registry == "" {
261+ return ""
262+ }
263+ if strings .Contains (registry , "." ) {
264+ return registry
265+ }
266+ return registry + ".azurecr.io"
267+ }
268+
269+ // registryShortName returns the ACR short name (the part before ".azurecr.io") for use with
270+ // "az acr build --registry".
271+ func registryShortName (loginServer string ) string {
272+ if host , _ , ok := strings .Cut (loginServer , "." ); ok {
273+ return host
274+ }
275+ return loginServer
276+ }
277+
278+ func fileExists (path string ) bool {
279+ info , err := os .Stat (path )
280+ return err == nil && ! info .IsDir ()
281+ }
282+
184283type environmentOutput struct {
185284 Id string `json:"id"`
186285 ProjectId string `json:"projectId"`
0 commit comments