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 }