github.com/argoproj/argo-cd/v3@v3.2.1/util/cmp/stream.go (about) 1 package cmp 2 3 import ( 4 "bufio" 5 "context" 6 "crypto/sha256" 7 "encoding/hex" 8 "errors" 9 "fmt" 10 "io" 11 "math" 12 "os" 13 "path/filepath" 14 "strings" 15 16 log "github.com/sirupsen/logrus" 17 18 pluginclient "github.com/argoproj/argo-cd/v3/cmpserver/apiclient" 19 "github.com/argoproj/argo-cd/v3/common" 20 "github.com/argoproj/argo-cd/v3/util/io/files" 21 "github.com/argoproj/argo-cd/v3/util/tgzstream" 22 ) 23 24 // StreamSender defines the contract to send App files over stream 25 type StreamSender interface { 26 Send(*pluginclient.AppStreamRequest) error 27 } 28 29 // StreamReceiver defines the contract for receiving Application's files 30 // over gRPC stream 31 type StreamReceiver interface { 32 Recv() (*pluginclient.AppStreamRequest, error) 33 } 34 35 // ReceiveRepoStream will receive the repository files and save them 36 // in destDir. Will return the stream metadata if no error. Metadata 37 // will be nil in case of errors. 38 func ReceiveRepoStream(ctx context.Context, receiver StreamReceiver, destDir string, preserveFileMode bool) (*pluginclient.ManifestRequestMetadata, error) { 39 header, err := receiver.Recv() 40 if err != nil { 41 return nil, fmt.Errorf("error receiving stream header: %w", err) 42 } 43 if header == nil || header.GetMetadata() == nil { 44 return nil, errors.New("error getting stream metadata: metadata is nil") 45 } 46 metadata := header.GetMetadata() 47 48 tgzFile, err := receiveFile(ctx, receiver, metadata.GetChecksum(), destDir) 49 if err != nil { 50 return nil, fmt.Errorf("error receiving tgz file: %w", err) 51 } 52 err = files.Untgz(destDir, tgzFile, math.MaxInt64, preserveFileMode) 53 if err != nil { 54 return nil, fmt.Errorf("error decompressing tgz file: %w", err) 55 } 56 err = os.Remove(tgzFile.Name()) 57 if err != nil { 58 log.Warnf("error removing the tgz file %q: %s", tgzFile.Name(), err) 59 } 60 return metadata, nil 61 } 62 63 // SenderOption defines the function type to by used by specific options 64 type SenderOption func(*senderOption) 65 66 type senderOption struct { 67 chunkSize int 68 tarDoneChan chan<- bool 69 } 70 71 func newSenderOption(opts ...SenderOption) *senderOption { 72 so := &senderOption{ 73 chunkSize: common.GetCMPChunkSize(), 74 } 75 for _, opt := range opts { 76 opt(so) 77 } 78 return so 79 } 80 81 func WithTarDoneChan(ch chan<- bool) SenderOption { 82 return func(opt *senderOption) { 83 opt.tarDoneChan = ch 84 } 85 } 86 87 // SendRepoStream will compress the files under the given rootPath and send 88 // them using the plugin stream sender. 89 func SendRepoStream(ctx context.Context, appPath, rootPath string, sender StreamSender, env []string, excludedGlobs []string, opts ...SenderOption) error { 90 opt := newSenderOption(opts...) 91 92 tgz, mr, err := GetCompressedRepoAndMetadata(rootPath, appPath, env, excludedGlobs, opt) 93 if err != nil { 94 return err 95 } 96 defer tgzstream.CloseAndDelete(tgz) 97 err = sender.Send(mr) 98 if err != nil { 99 return fmt.Errorf("error sending generate manifest metadata to cmp-server: %w", err) 100 } 101 102 // send the compressed file 103 err = sendFile(ctx, sender, tgz, opt) 104 if err != nil { 105 return fmt.Errorf("error sending tgz file to cmp-server: %w", err) 106 } 107 return nil 108 } 109 110 func GetCompressedRepoAndMetadata(rootPath string, appPath string, env []string, excludedGlobs []string, opt *senderOption) (*os.File, *pluginclient.AppStreamRequest, error) { 111 // compress all files in rootPath in tgz 112 tgz, filesWritten, checksum, err := tgzstream.CompressFiles(rootPath, nil, excludedGlobs) 113 if err != nil { 114 return nil, nil, fmt.Errorf("error compressing repo files: %w", err) 115 } 116 if filesWritten == 0 { 117 return nil, nil, fmt.Errorf("no files to send(%s)", rootPath) 118 } 119 if opt != nil && opt.tarDoneChan != nil { 120 opt.tarDoneChan <- true 121 close(opt.tarDoneChan) 122 } 123 124 fi, err := tgz.Stat() 125 if err != nil { 126 return nil, nil, fmt.Errorf("error getting tgz stat: %w", err) 127 } 128 appRelPath, err := files.RelativePath(appPath, rootPath) 129 if err != nil { 130 return nil, nil, fmt.Errorf("error building app relative path: %w", err) 131 } 132 // send metadata first 133 mr := appMetadataRequest(filepath.Base(appPath), appRelPath, env, checksum, fi.Size()) 134 return tgz, mr, err 135 } 136 137 // sendFile will send the file over the gRPC stream using a 138 // buffer. 139 func sendFile(ctx context.Context, sender StreamSender, file *os.File, opt *senderOption) error { 140 reader := bufio.NewReader(file) 141 chunk := make([]byte, opt.chunkSize) 142 for { 143 if ctx != nil { 144 if err := ctx.Err(); err != nil { 145 return fmt.Errorf("client stream context error: %w", err) 146 } 147 } 148 n, err := reader.Read(chunk) 149 if n > 0 { 150 fr := AppFileRequest(chunk[:n]) 151 if e := sender.Send(fr); e != nil { 152 return fmt.Errorf("error sending stream: %w", e) 153 } 154 } 155 if err != nil { 156 if err == io.EOF { 157 break 158 } 159 return fmt.Errorf("buffer reader error: %w", err) 160 } 161 } 162 return nil 163 } 164 165 // receiveFile will receive the file from the gRPC stream and save it in the dst folder. 166 // Returns error if checksum doesn't match the one provided in the fileMetadata. 167 // It is responsibility of the caller to close the returned file. 168 func receiveFile(ctx context.Context, receiver StreamReceiver, checksum, dst string) (*os.File, error) { 169 hasher := sha256.New() 170 file, err := os.CreateTemp(dst, "") 171 if err != nil { 172 return nil, fmt.Errorf("error creating file: %w", err) 173 } 174 for { 175 if ctx != nil { 176 if err := ctx.Err(); err != nil { 177 return nil, fmt.Errorf("stream context error: %w", err) 178 } 179 } 180 req, err := receiver.Recv() 181 if err != nil { 182 if errors.Is(err, io.EOF) { 183 break 184 } 185 return nil, fmt.Errorf("stream Recv error: %w", err) 186 } 187 f := req.GetFile() 188 if f == nil { 189 return nil, errors.New("stream request file is nil") 190 } 191 _, err = file.Write(f.Chunk) 192 if err != nil { 193 return nil, fmt.Errorf("error writing file: %w", err) 194 } 195 _, err = hasher.Write(f.Chunk) 196 if err != nil { 197 return nil, fmt.Errorf("error writing hasher: %w", err) 198 } 199 } 200 if hex.EncodeToString(hasher.Sum(nil)) != checksum { 201 return nil, errors.New("file checksum validation error") 202 } 203 204 _, err = file.Seek(0, io.SeekStart) 205 if err != nil { 206 tgzstream.CloseAndDelete(file) 207 return nil, fmt.Errorf("seek error: %w", err) 208 } 209 return file, nil 210 } 211 212 // AppFileRequest build the file payload for the ManifestRequest 213 func AppFileRequest(chunk []byte) *pluginclient.AppStreamRequest { 214 return &pluginclient.AppStreamRequest{ 215 Request: &pluginclient.AppStreamRequest_File{ 216 File: &pluginclient.File{ 217 Chunk: chunk, 218 }, 219 }, 220 } 221 } 222 223 // appMetadataRequest build the metadata payload for the ManifestRequest 224 func appMetadataRequest(appName, appRelPath string, env []string, checksum string, size int64) *pluginclient.AppStreamRequest { 225 return &pluginclient.AppStreamRequest{ 226 Request: &pluginclient.AppStreamRequest_Metadata{ 227 Metadata: &pluginclient.ManifestRequestMetadata{ 228 AppName: appName, 229 AppRelPath: appRelPath, 230 Checksum: checksum, 231 Size_: size, 232 Env: toEnvEntry(env), 233 }, 234 }, 235 } 236 } 237 238 func toEnvEntry(envVars []string) []*pluginclient.EnvEntry { 239 envEntry := make([]*pluginclient.EnvEntry, 0) 240 for _, env := range envVars { 241 pair := strings.SplitN(env, "=", 2) 242 if len(pair) < 2 { 243 continue 244 } 245 envEntry = append(envEntry, &pluginclient.EnvEntry{Name: pair[0], Value: pair[1]}) 246 } 247 return envEntry 248 }