@@ -1034,10 +1034,57 @@ func (c *Client) CopyFromPod(ctx context.Context, namespace, podName, remotePath
10341034}
10351035
10361036func (c * Client ) CopyFromPodInContainer (ctx context.Context , namespace , podName , container , remotePath , localPath string ) error {
1037+ var lastErr error
1038+ for attempt := 0 ; attempt < 3 ; attempt ++ {
1039+ lastErr = c .copyFromPodInContainerOnce (ctx , namespace , podName , container , remotePath , localPath )
1040+ if lastErr == nil {
1041+ return nil
1042+ }
1043+ if ! isRetryableCopyStreamError (lastErr ) || attempt == 2 {
1044+ return lastErr
1045+ }
1046+ time .Sleep (time .Duration (attempt + 1 ) * 500 * time .Millisecond )
1047+ }
1048+ return lastErr
1049+ }
1050+
1051+ func (c * Client ) copyFromPodInContainerOnce (ctx context.Context , namespace , podName , container , remotePath , localPath string ) error {
10371052 cs , cfg , err := c .clientset ()
10381053 if err != nil {
10391054 return err
10401055 }
1056+ parent := filepath .Dir (remotePath )
1057+ base := filepath .Base (remotePath )
1058+ cmd := []string {"sh" , "-lc" , fmt .Sprintf ("tar cf - -C %s %s" , shellQuote (parent ), shellQuote (base ))}
1059+ tarFile , err := os .CreateTemp ("" , "okdev-copy-*.tar" )
1060+ if err != nil {
1061+ return err
1062+ }
1063+ tarPath := tarFile .Name ()
1064+ defer os .Remove (tarPath )
1065+ var errBuf bytes.Buffer
1066+ if err := c .execStream (ctx , cs , cfg , namespace , podName , container , cmd , nil , tarFile , & errBuf , false ); err != nil {
1067+ _ = tarFile .Close ()
1068+ return err
1069+ }
1070+ if _ , err := tarFile .Seek (0 , io .SeekStart ); err != nil {
1071+ _ = tarFile .Close ()
1072+ return err
1073+ }
1074+ if err := extractSingleFileFromTar (tarFile , localPath ); err != nil {
1075+ _ = tarFile .Close ()
1076+ return err
1077+ }
1078+ return tarFile .Close ()
1079+ }
1080+
1081+ func createTempDownloadFile (localPath string ) (* os.File , error ) {
1082+ dir := filepath .Dir (localPath )
1083+ base := filepath .Base (localPath )
1084+ return os .CreateTemp (dir , base + ".tmp-*" )
1085+ }
1086+
1087+ func extractSingleFileFromTar (r io.Reader , localPath string ) error {
10411088 if err := os .MkdirAll (filepath .Dir (localPath ), 0o755 ); err != nil {
10421089 return err
10431090 }
@@ -1053,25 +1100,45 @@ func (c *Client) CopyFromPodInContainer(ctx context.Context, namespace, podName,
10531100 }
10541101 _ = os .Remove (tempPath )
10551102 }()
1056- var errBuf bytes.Buffer
1057- cmd := []string {"sh" , "-lc" , fmt .Sprintf ("cat %s" , shellQuote (remotePath ))}
1058- if err := c .execStream (ctx , cs , cfg , namespace , podName , container , cmd , nil , tempFile , & errBuf , false ); err != nil {
1103+
1104+ tr := tar .NewReader (r )
1105+ foundFile := false
1106+ var fileMode os.FileMode = 0o644
1107+ for {
1108+ hdr , err := tr .Next ()
1109+ if err == io .EOF {
1110+ break
1111+ }
1112+ if err != nil {
1113+ return err
1114+ }
1115+ switch hdr .Typeflag {
1116+ case tar .TypeDir , tar .TypeXHeader , tar .TypeXGlobalHeader , tar .TypeGNULongName , tar .TypeGNULongLink :
1117+ continue
1118+ case tar .TypeReg , tar .TypeRegA :
1119+ if foundFile {
1120+ return fmt .Errorf ("tar archive contains multiple files" )
1121+ }
1122+ if _ , err := io .Copy (tempFile , tr ); err != nil {
1123+ return err
1124+ }
1125+ fileMode = os .FileMode (hdr .Mode )
1126+ foundFile = true
1127+ default :
1128+ return fmt .Errorf ("tar archive entry %q has unsupported type" , hdr .Name )
1129+ }
1130+ }
1131+ if ! foundFile {
1132+ return fmt .Errorf ("tar archive contained no file" )
1133+ }
1134+ if err := tempFile .Chmod (fileMode ); err != nil {
10591135 return err
10601136 }
10611137 if err := tempFile .Close (); err != nil {
10621138 return err
10631139 }
10641140 closed = true
1065- if err := os .Rename (tempPath , localPath ); err != nil {
1066- return err
1067- }
1068- return nil
1069- }
1070-
1071- func createTempDownloadFile (localPath string ) (* os.File , error ) {
1072- dir := filepath .Dir (localPath )
1073- base := filepath .Base (localPath )
1074- return os .CreateTemp (dir , base + ".tmp-*" )
1141+ return os .Rename (tempPath , localPath )
10751142}
10761143
10771144func (c * Client ) StreamFromPod (ctx context.Context , namespace , podName , script string , stdout io.Writer ) error {
@@ -1185,6 +1252,21 @@ func (c *Client) CopyDirToPod(ctx context.Context, namespace, pod, container, lo
11851252
11861253// CopyDirFromPod streams a remote directory as a tar archive and extracts it locally.
11871254func (c * Client ) CopyDirFromPod (ctx context.Context , namespace , pod , container , remoteDir , localDir string ) error {
1255+ var lastErr error
1256+ for attempt := 0 ; attempt < 3 ; attempt ++ {
1257+ lastErr = c .copyDirFromPodOnce (ctx , namespace , pod , container , remoteDir , localDir )
1258+ if lastErr == nil {
1259+ return nil
1260+ }
1261+ if ! isRetryableCopyStreamError (lastErr ) || attempt == 2 {
1262+ return lastErr
1263+ }
1264+ time .Sleep (time .Duration (attempt + 1 ) * 500 * time .Millisecond )
1265+ }
1266+ return lastErr
1267+ }
1268+
1269+ func (c * Client ) copyDirFromPodOnce (ctx context.Context , namespace , pod , container , remoteDir , localDir string ) error {
11881270 cs , cfg , err := c .clientset ()
11891271 if err != nil {
11901272 return err
@@ -1215,6 +1297,40 @@ func (c *Client) CopyDirFromPod(ctx context.Context, namespace, pod, container,
12151297 return tarFile .Close ()
12161298}
12171299
1300+ func isRetryableCopyStreamError (err error ) bool {
1301+ if err == nil {
1302+ return false
1303+ }
1304+ msg := strings .ToLower (err .Error ())
1305+ nonRetryable := []string {
1306+ "not found" ,
1307+ "no such file" ,
1308+ "permission denied" ,
1309+ "is a directory" ,
1310+ "tar archive contains multiple files" ,
1311+ "tar archive contained no file" ,
1312+ "unsupported type" ,
1313+ }
1314+ for _ , s := range nonRetryable {
1315+ if strings .Contains (msg , s ) {
1316+ return false
1317+ }
1318+ }
1319+ retryable := []string {
1320+ "unexpected eof" ,
1321+ "context deadline exceeded" ,
1322+ "unexpected error when reading response body" ,
1323+ "connection reset by peer" ,
1324+ "timeout" ,
1325+ }
1326+ for _ , s := range retryable {
1327+ if strings .Contains (msg , s ) {
1328+ return true
1329+ }
1330+ }
1331+ return false
1332+ }
1333+
12181334// IsRemoteDir probes whether remotePath is a directory on the pod.
12191335func (c * Client ) IsRemoteDir (ctx context.Context , namespace , pod , container , remotePath string ) (bool , error ) {
12201336 cs , cfg , err := c .clientset ()
0 commit comments