@@ -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
1518func 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+
109222type environmentOutput struct {
110223 Id string `json:"id"`
111224 ProjectId string `json:"projectId"`
0 commit comments