@@ -13,6 +13,8 @@ import (
1313 "net/url"
1414 "slices"
1515 "strings"
16+ "sync"
17+ "time"
1618
1719 "github.com/Azure/azure-sdk-for-go/sdk/azcore"
1820 "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
@@ -25,6 +27,7 @@ import (
2527 "github.com/azure/azure-dev/cli/azd/pkg/azure"
2628 "github.com/azure/azure-dev/cli/azd/pkg/httputil"
2729 "github.com/azure/azure-dev/cli/azd/pkg/tools/docker"
30+ "golang.org/x/sync/singleflight"
2831)
2932
3033// Credentials for authenticating with a docker registry,
@@ -61,6 +64,12 @@ type containerRegistryService struct {
6164 docker * docker.Cli
6265 armClientOptions * arm.ClientOptions
6366 coreClientOptions * azcore.ClientOptions
67+ // loginGroup deduplicates concurrent login attempts to the same registry.
68+ // When multiple services share one ACR, only the first goroutine performs
69+ // the credential exchange + docker login; others wait for its result.
70+ loginGroup singleflight.Group
71+ // loginDone tracks registries that have already been authenticated this session.
72+ loginDone sync.Map
6473}
6574
6675// Creates a new instance of the ContainerRegistryService
@@ -118,20 +127,51 @@ func (crs *containerRegistryService) FindContainerRegistryResourceGroup(
118127}
119128
120129func (crs * containerRegistryService ) Login (ctx context.Context , subscriptionId string , loginServer string ) error {
121- dockerCreds , err := crs .Credentials (ctx , subscriptionId , loginServer )
122- if err != nil {
123- return err
130+ cacheKey := subscriptionId + ":" + loginServer
131+ if _ , ok := crs .loginDone .Load (cacheKey ); ok {
132+ log .Printf ("skipping redundant login to '%s' (already authenticated this session)\n " , loginServer )
133+ return nil
124134 }
125135
126- err = crs .docker .Login (ctx , dockerCreds .LoginServer , dockerCreds .Username , dockerCreds .Password )
127- if err != nil {
128- return fmt .Errorf (
129- "failed logging into docker registry %s: %w" ,
130- loginServer ,
131- err )
132- }
136+ // singleflight deduplicates concurrent login attempts to the same registry.
137+ // We use DoChan so each caller can select on its own ctx.Done(), avoiding the
138+ // problem where one caller's cancellation fails all waiters.
139+ ch := crs .loginGroup .DoChan (cacheKey , func () (any , error ) {
140+ // Double-check after winning the singleflight race.
141+ if _ , ok := crs .loginDone .Load (cacheKey ); ok {
142+ return nil , nil
143+ }
144+
145+ // Use context.WithoutCancel so the shared work isn't tied to a single
146+ // caller's context. Add a bounded timeout so the shared login cannot
147+ // hang indefinitely if Credentials or docker login gets stuck.
148+ const loginTimeout = 5 * time .Minute
149+ opCtx , cancel := context .WithTimeout (context .WithoutCancel (ctx ), loginTimeout )
150+ defer cancel ()
151+
152+ dockerCreds , err := crs .Credentials (opCtx , subscriptionId , loginServer )
153+ if err != nil {
154+ return nil , err
155+ }
156+
157+ err = crs .docker .Login (opCtx , dockerCreds .LoginServer , dockerCreds .Username , dockerCreds .Password )
158+ if err != nil {
159+ return nil , fmt .Errorf (
160+ "failed logging into docker registry %s: %w" ,
161+ loginServer ,
162+ err )
163+ }
133164
134- return nil
165+ crs .loginDone .Store (cacheKey , true )
166+ return nil , nil
167+ })
168+
169+ select {
170+ case <- ctx .Done ():
171+ return ctx .Err ()
172+ case res := <- ch :
173+ return res .Err
174+ }
135175}
136176
137177// Credentials gets the credentials that could be used to login to the specified container registry. It prefers to use
0 commit comments