@@ -105,60 +105,103 @@ def get_oci_cmd(repository, version):
105105 private_registry = re .match (private_ecr_pattern , repository ).groupdict ()
106106 public_registry = re .match (public_ecr_pattern , repository ).groupdict ()
107107
108+ # Build helm pull command as array
109+ helm_cmd = ['helm' , 'pull' , repository , '--version' , version , '--untar' ]
110+
108111 if private_registry ['registry' ] is not None :
109112 logger .info ("Found AWS private repository" )
110- cmnd = [
111- f"aws ecr get-login-password --region { private_registry ['region' ]} | " \
112- f"helm registry login --username AWS --password-stdin { private_registry ['registry' ]} ; helm pull { repository } --version { version } --untar"
113- ]
113+ ecr_login = ['aws' , 'ecr' , 'get-login-password' , '--region' , private_registry ['region' ]]
114+ helm_registry_login = ['helm' , 'registry' , 'login' , '--username' , 'AWS' , '--password-stdin' , private_registry ['registry' ]]
115+ return {'ecr_login' : ecr_login , 'registry_login' : helm_registry_login , 'helm' : helm_cmd }
114116 elif public_registry ['registry' ] is not None :
115117 logger .info ("Found AWS public repository, will use default region as deployment" )
116118 region = os .environ .get ('AWS_REGION' , 'us-east-1' )
117119
118120 if is_ecr_public_available (region ):
119- cmnd = [
120- f" aws ecr-public get-login-password --region us-east-1 | " \
121- f" helm registry login --username AWS --password-stdin { public_registry ['registry' ]} ; helm pull { repository } --version { version } --untar"
122- ]
121+ # Public ECR auth is always in us-east-1: https://docs.aws.amazon.com/AmazonECR/latest/public/public-registry-auth.html
122+ ecr_login = [ ' aws' , ' ecr-public' , ' get-login-password' , ' --region' , ' us-east-1' ]
123+ helm_registry_login = [ ' helm' , ' registry' , ' login' , ' --username' , ' AWS' , ' --password-stdin' , public_registry ['registry' ]]
124+ return { 'ecr_login' : ecr_login , 'helm_registry_login' : helm_registry_login , 'helm' : helm_cmd }
123125 else :
124- # `aws ecr-public get- login-password` and `helm registry login` not required as ecr public is not available in current region
126+ # No login required for public ECR in non-aws regions
125127 # see https://helm.sh/docs/helm/helm_registry_login/
126- cmnd = [ f" helm pull { repository } --version { version } --untar" ]
128+ return { ' helm' : helm_cmd }
127129 else :
128130 logger .error ("OCI repository format not recognized, falling back to helm pull" )
129- cmnd = [f"helm pull { repository } --version { version } --untar" ]
130-
131- return cmnd
131+ return {'helm' : helm_cmd }
132132
133133
134- def get_chart_from_oci (tmpdir , repository = None , version = None ):
134+ def get_chart_from_oci (tmpdir , repository = None , version = None ):
135+ from subprocess import Popen , PIPE
135136
136- cmnd = get_oci_cmd (repository , version )
137+ commands = get_oci_cmd (repository , version )
137138
138139 maxAttempts = 3
139140 retry = maxAttempts
140141 while retry > 0 :
141142 try :
142- logger .info (cmnd )
143- output = subprocess .check_output (cmnd , stderr = subprocess .STDOUT , cwd = tmpdir , shell = True )
143+ # Execute login commands if needed
144+ if 'ecr_login' in commands and 'helm_registry_login' in commands :
145+ logger .info ("Running login command: %s" , commands ['ecr_login' ])
146+ logger .info ("Running registry login command: %s" , commands ['helm_registry_login' ])
147+
148+ # Start first process: aws ecr get-login-password
149+ # NOTE: We do NOT call p1.wait() here before starting p2.
150+ # Doing so could deadlock if p1's output fills the pipe buffer
151+ # before p2 starts reading. Instead, start p2 immediately so it
152+ # can consume p1's stdout as it's produced.
153+ p1 = Popen (commands ['ecr_login' ], stdout = PIPE , stderr = PIPE , cwd = tmpdir )
154+
155+ # Start second process: helm registry login
156+ p2 = Popen (commands ['helm_registry_login' ], stdin = p1 .stdout , stdout = PIPE , stderr = PIPE , cwd = tmpdir )
157+ p1 .stdout .close () # Allow p1 to receive SIGPIPE if p2 exits early
158+
159+ # Wait for p2 to finish first (ensures full pipeline completes)
160+ _ , p2_err = p2 .communicate ()
161+
162+ # Now wait for p1 so we have a complete stderr and an exit code
163+ p1 .wait ()
164+
165+ # Handle p1 failure
166+ if p1 .returncode != 0 :
167+ p1_err = p1 .stderr .read ().decode ('utf-8' , errors = 'replace' ) if p1 .stderr else ''
168+ logger .error (
169+ "ECR get-login-password failed for repository %s. Error: %s" ,
170+ repository ,
171+ p1_err or "No error details"
172+ )
173+ raise subprocess .CalledProcessError (p1 .returncode , commands ['ecr_login' ], p1_err .encode ())
174+
175+ # Handle p2 failure
176+ if p2 .returncode != 0 :
177+ logger .error (
178+ "Helm registry authentication failed for repository %s. Error: %s" ,
179+ repository ,
180+ p2_err .decode ('utf-8' , errors = 'replace' ) or "No error details"
181+ )
182+ raise subprocess .CalledProcessError (p2 .returncode , commands ['helm_registry_login' ], p2_err )
183+
184+ # Execute helm pull command
185+ logger .info ("Running helm command: %s" , commands ['helm' ])
186+ output = subprocess .check_output (commands ['helm' ], stderr = subprocess .STDOUT , cwd = tmpdir )
144187 logger .info (output )
145188
146189 # effectively returns "$tmpDir/$lastPartOfOCIUrl", because this is how helm pull saves OCI artifact.
147190 # Eg. if we have oci://9999999999.dkr.ecr.us-east-1.amazonaws.com/foo/bar/pet-service repository, helm saves artifact under $tmpDir/pet-service
148191 return os .path .join (tmpdir , repository .rpartition ('/' )[- 1 ])
192+
149193 except subprocess .CalledProcessError as exc :
150194 output = exc .output
151195 if b'Broken pipe' in output :
152196 retry = retry - 1
153197 logger .info ("Broken pipe, retries left: %s" % retry )
154198 else :
155199 raise Exception (output )
200+
156201 raise Exception (f'Operation failed after { maxAttempts } attempts: { output } ' )
157202
158203
159204def helm (verb , release , chart = None , repo = None , file = None , namespace = None , version = None , wait = False , timeout = None , create_namespace = None , skip_crds = False , atomic = False ):
160- import subprocess
161-
162205 cmnd = ['helm' , verb , release ]
163206 if not chart is None :
164207 cmnd .append (chart )
0 commit comments