github.com/argoproj/argo-cd/v3@v3.2.1/util/manifeststream/stream.go (about)

     1  package manifeststream
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"crypto/sha256"
     7  	"encoding/hex"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  
    13  	log "github.com/sirupsen/logrus"
    14  
    15  	applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
    16  	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    17  	"github.com/argoproj/argo-cd/v3/util/io/files"
    18  	"github.com/argoproj/argo-cd/v3/util/tgzstream"
    19  )
    20  
    21  // Defines the contract for the application sender, i.e. the CLI
    22  type ApplicationStreamSender interface {
    23  	Send(*applicationpkg.ApplicationManifestQueryWithFilesWrapper) error
    24  }
    25  
    26  // Defines the contract for the application receiver, i.e. API server
    27  type ApplicationStreamReceiver interface {
    28  	Recv() (*applicationpkg.ApplicationManifestQueryWithFilesWrapper, error)
    29  }
    30  
    31  // Defines the contract for the repo stream sender, i.e. the API server
    32  type RepoStreamSender interface {
    33  	Send(*apiclient.ManifestRequestWithFiles) error
    34  }
    35  
    36  // Defines the contract for the repo stream receiver, i.e. the repo server
    37  type RepoStreamReceiver interface {
    38  	Recv() (*apiclient.ManifestRequestWithFiles, error)
    39  }
    40  
    41  // SendApplicationManifestQueryWithFiles compresses a folder and sends it over the stream
    42  func SendApplicationManifestQueryWithFiles(ctx context.Context, stream ApplicationStreamSender, appName string, appNs string, dir string, inclusions []string) error {
    43  	f, filesWritten, checksum, err := tgzstream.CompressFiles(dir, inclusions, nil)
    44  	if err != nil {
    45  		return fmt.Errorf("failed to compress files: %w", err)
    46  	}
    47  	if filesWritten == 0 {
    48  		return errors.New("no files to send")
    49  	}
    50  
    51  	err = stream.Send(&applicationpkg.ApplicationManifestQueryWithFilesWrapper{
    52  		Part: &applicationpkg.ApplicationManifestQueryWithFilesWrapper_Query{
    53  			Query: &applicationpkg.ApplicationManifestQueryWithFiles{
    54  				Name:         &appName,
    55  				Checksum:     &checksum,
    56  				AppNamespace: &appNs,
    57  			},
    58  		},
    59  	})
    60  	if err != nil {
    61  		return fmt.Errorf("failed to send manifest stream header: %w", err)
    62  	}
    63  
    64  	err = sendFile(ctx, stream, f)
    65  	if err != nil {
    66  		return fmt.Errorf("failed to send manifest stream file: %w", err)
    67  	}
    68  
    69  	return nil
    70  }
    71  
    72  func sendFile(ctx context.Context, sender ApplicationStreamSender, file *os.File) error {
    73  	reader := bufio.NewReader(file)
    74  	chunk := make([]byte, 1024)
    75  	for {
    76  		if ctx != nil {
    77  			if err := ctx.Err(); err != nil {
    78  				return fmt.Errorf("client stream context error: %w", err)
    79  			}
    80  		}
    81  		n, err := reader.Read(chunk)
    82  		if n > 0 {
    83  			fr := &applicationpkg.ApplicationManifestQueryWithFilesWrapper{
    84  				Part: &applicationpkg.ApplicationManifestQueryWithFilesWrapper_Chunk{
    85  					Chunk: &applicationpkg.FileChunk{
    86  						Chunk: chunk[:n],
    87  					},
    88  				},
    89  			}
    90  			if e := sender.Send(fr); e != nil {
    91  				return fmt.Errorf("error sending stream: %w", e)
    92  			}
    93  		}
    94  		if err != nil {
    95  			if err == io.EOF {
    96  				break
    97  			}
    98  			return fmt.Errorf("buffer reader error: %w", err)
    99  		}
   100  	}
   101  	return nil
   102  }
   103  
   104  func ReceiveApplicationManifestQueryWithFiles(stream ApplicationStreamReceiver) (*applicationpkg.ApplicationManifestQueryWithFiles, error) {
   105  	header, err := stream.Recv()
   106  	if err != nil {
   107  		return nil, fmt.Errorf("failed to receive header: %w", err)
   108  	}
   109  	if header == nil || header.GetQuery() == nil {
   110  		return nil, errors.New("error getting stream query: query is nil")
   111  	}
   112  	return header.GetQuery(), nil
   113  }
   114  
   115  func SendRepoStream(repoStream RepoStreamSender, appStream ApplicationStreamReceiver, req *apiclient.ManifestRequest, checksum string) error {
   116  	err := repoStream.Send(&apiclient.ManifestRequestWithFiles{
   117  		Part: &apiclient.ManifestRequestWithFiles_Request{
   118  			Request: req,
   119  		},
   120  	})
   121  	if err != nil {
   122  		return fmt.Errorf("error sending request: %w", err)
   123  	}
   124  
   125  	err = repoStream.Send(&apiclient.ManifestRequestWithFiles{
   126  		Part: &apiclient.ManifestRequestWithFiles_Metadata{
   127  			Metadata: &apiclient.ManifestFileMetadata{
   128  				Checksum: checksum,
   129  			},
   130  		},
   131  	})
   132  	if err != nil {
   133  		return fmt.Errorf("error sending metadata: %w", err)
   134  	}
   135  
   136  	for {
   137  		part, err := appStream.Recv()
   138  		if err != nil {
   139  			if errors.Is(err, io.EOF) {
   140  				break
   141  			}
   142  			return fmt.Errorf("stream Recv error: %w", err)
   143  		}
   144  		if part == nil || part.GetChunk() == nil {
   145  			return errors.New("error getting stream chunk: chunk is nil")
   146  		}
   147  
   148  		err = repoStream.Send(&apiclient.ManifestRequestWithFiles{
   149  			Part: &apiclient.ManifestRequestWithFiles_Chunk{
   150  				Chunk: &apiclient.ManifestFileChunk{
   151  					Chunk: part.GetChunk().GetChunk(),
   152  				},
   153  			},
   154  		})
   155  		if err != nil {
   156  			return fmt.Errorf("error sending chunk: %w", err)
   157  		}
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  func ReceiveManifestFileStream(ctx context.Context, receiver RepoStreamReceiver, destDir string, maxTarSize int64, maxExtractedSize int64) (*apiclient.ManifestRequest, *apiclient.ManifestFileMetadata, error) {
   164  	header, err := receiver.Recv()
   165  	if err != nil {
   166  		return nil, nil, fmt.Errorf("failed to receive header: %w", err)
   167  	}
   168  	if header == nil || header.GetRequest() == nil {
   169  		return nil, nil, errors.New("error getting stream request: request is nil")
   170  	}
   171  	request := header.GetRequest()
   172  
   173  	header2, err := receiver.Recv()
   174  	if err != nil {
   175  		return nil, nil, fmt.Errorf("failed to receive header: %w", err)
   176  	}
   177  	if header2 == nil || header2.GetMetadata() == nil {
   178  		return nil, nil, errors.New("error getting stream metadata: metadata is nil")
   179  	}
   180  	metadata := header2.GetMetadata()
   181  
   182  	tgzFile, err := receiveFile(ctx, receiver, metadata.GetChecksum(), maxTarSize)
   183  	if err != nil {
   184  		return nil, nil, fmt.Errorf("error receiving tgz file: %w", err)
   185  	}
   186  	err = files.Untgz(destDir, tgzFile, maxExtractedSize, false)
   187  	if err != nil {
   188  		return nil, nil, fmt.Errorf("error decompressing tgz file: %w", err)
   189  	}
   190  	err = os.Remove(tgzFile.Name())
   191  	if err != nil {
   192  		log.Warnf("error removing the tgz file %q: %s", tgzFile.Name(), err)
   193  	}
   194  	return request, metadata, nil
   195  }
   196  
   197  // receiveFile will receive the file from the gRPC stream and save it in the dst folder.
   198  // Returns error if checksum doesn't match the one provided in the fileMetadata.
   199  // It is responsibility of the caller to close the returned file.
   200  func receiveFile(ctx context.Context, receiver RepoStreamReceiver, checksum string, maxSize int64) (*os.File, error) {
   201  	hasher := sha256.New()
   202  	tmpDir, err := files.CreateTempDir("")
   203  	if err != nil {
   204  		return nil, fmt.Errorf("error creating tmp dir: %w", err)
   205  	}
   206  	file, err := os.CreateTemp(tmpDir, "")
   207  	if err != nil {
   208  		return nil, fmt.Errorf("error creating file: %w", err)
   209  	}
   210  	size := 0
   211  	for {
   212  		if ctx != nil {
   213  			if err := ctx.Err(); err != nil {
   214  				return nil, fmt.Errorf("stream context error: %w", err)
   215  			}
   216  		}
   217  		req, err := receiver.Recv()
   218  		if err != nil {
   219  			if errors.Is(err, io.EOF) {
   220  				break
   221  			}
   222  			return nil, fmt.Errorf("stream Recv error: %w", err)
   223  		}
   224  		c := req.GetChunk()
   225  		if c == nil {
   226  			return nil, errors.New("stream request chunk is nil")
   227  		}
   228  		size += len(c.Chunk)
   229  		if size > int(maxSize) {
   230  			return nil, fmt.Errorf("file exceeded max size of %d bytes", maxSize)
   231  		}
   232  		_, err = file.Write(c.Chunk)
   233  		if err != nil {
   234  			return nil, fmt.Errorf("error writing file: %w", err)
   235  		}
   236  		_, err = hasher.Write(c.Chunk)
   237  		if err != nil {
   238  			return nil, fmt.Errorf("error writing hasher: %w", err)
   239  		}
   240  	}
   241  	hasherChecksum := hex.EncodeToString(hasher.Sum(nil))
   242  	if hasherChecksum != checksum {
   243  		return nil, fmt.Errorf("file checksum validation error: calc %s sent %s", hasherChecksum, checksum)
   244  	}
   245  
   246  	_, err = file.Seek(0, io.SeekStart)
   247  	if err != nil {
   248  		tgzstream.CloseAndDelete(file)
   249  		return nil, fmt.Errorf("seek error: %w", err)
   250  	}
   251  	return file, nil
   252  }