github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/gcsupload/run.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package gcsupload 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "mime" 24 "net/url" 25 "os" 26 "path" 27 "path/filepath" 28 "strings" 29 30 "github.com/sirupsen/logrus" 31 32 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 33 "sigs.k8s.io/prow/pkg/pod-utils/downwardapi" 34 "sigs.k8s.io/prow/pkg/pod-utils/gcs" 35 ) 36 37 // Run will upload files to GCS as prescribed by 38 // the options. Any extra files can be passed as 39 // a parameter and will have the prefix prepended 40 // to their destination in GCS, so the caller can 41 // operate relative to the base of the GCS dir. 42 func (o Options) Run(ctx context.Context, spec *downwardapi.JobSpec, extra map[string]gcs.UploadFunc) error { 43 logrus.WithField("options", o).Debug("Uploading to blob storage") 44 45 for extension, mediaType := range o.GCSConfiguration.MediaTypes { 46 mime.AddExtensionType("."+extension, mediaType) 47 } 48 49 uploadTargets, extraTargets, err := o.assembleTargets(spec, extra) 50 if err != nil { 51 return fmt.Errorf("assembleTargets: %w", err) 52 } 53 54 err = completeUpload(ctx, o, uploadTargets) 55 56 if extraErr := completeUpload(ctx, o, extraTargets); extraErr != nil { 57 if err == nil { 58 err = extraErr 59 } else { 60 logrus.WithError(extraErr).Info("Also failed to upload extra targets") 61 } 62 } 63 64 return err 65 } 66 67 func completeUpload(ctx context.Context, o Options, uploadTargets map[string]gcs.UploadFunc) error { 68 if o.DryRun { 69 for destination := range uploadTargets { 70 logrus.WithField("dest", destination).Info("Would upload") 71 } 72 return nil 73 } 74 75 if o.LocalOutputDir == "" { 76 if err := gcs.Upload(ctx, o.Bucket, o.StorageClientOptions.GCSCredentialsFile, o.StorageClientOptions.S3CredentialsFile, o.CompressFileTypes, uploadTargets); err != nil { 77 return fmt.Errorf("failed to upload to blob storage: %w", err) 78 } 79 logrus.Info("Finished upload to blob storage") 80 } else { 81 if err := gcs.LocalExport(ctx, o.LocalOutputDir, uploadTargets); err != nil { 82 return fmt.Errorf("failed to copy files to %q: %w", o.LocalOutputDir, err) 83 } 84 logrus.Infof("Finished copying files to %q.", o.LocalOutputDir) 85 } 86 return nil 87 } 88 89 func (o Options) assembleTargets(spec *downwardapi.JobSpec, extra map[string]gcs.UploadFunc) (map[string]gcs.UploadFunc, map[string]gcs.UploadFunc, error) { 90 jobBasePath, blobStoragePath, builder := PathsForJob(o.GCSConfiguration, spec, o.SubDir) 91 92 uploadTargets := map[string]gcs.UploadFunc{} 93 94 // Skip the alias and latest build files in local mode. 95 if o.LocalOutputDir == "" { 96 // ensure that an alias exists for any 97 // job we're uploading artifacts for 98 if alias := gcs.AliasForSpec(spec); alias != "" { 99 parsedBucket, err := url.Parse(o.Bucket) 100 if err != nil { 101 return nil, nil, fmt.Errorf("parse bucket %q: %w", o.Bucket, err) 102 } 103 // only add gs:// prefix if o.Bucket itself doesn't already have a scheme prefix 104 var fullBasePath string 105 if parsedBucket.Scheme == "" { 106 fullBasePath = "gs://" + path.Join(o.Bucket, jobBasePath) 107 } else { 108 fullBasePath = fmt.Sprintf("%s/%s", o.Bucket, jobBasePath) 109 } 110 newReader := newStringReadCloser(fullBasePath) 111 uploadTargets[alias] = gcs.DataUploadWithMetadata(newReader, map[string]string{ 112 "x-goog-meta-link": fullBasePath, 113 }) 114 } 115 116 if latestBuilds := gcs.LatestBuildForSpec(spec, builder); len(latestBuilds) > 0 { 117 for _, latestBuild := range latestBuilds { 118 dir, filename := path.Split(latestBuild) 119 metadataFromFileName, writerOptions := gcs.WriterOptionsFromFileName(filename) 120 newReader := newStringReadCloser(spec.BuildID) 121 uploadTargets[path.Join(dir, metadataFromFileName)] = gcs.DataUploadWithOptions(newReader, writerOptions) 122 } 123 } 124 } else { 125 // Remove the gcs path prefix in local mode so that items are rooted in the output dir without 126 // excessive directory nesting. 127 blobStoragePath = "" 128 } 129 130 for _, item := range o.Items { 131 info, err := os.Stat(item) 132 if err != nil { 133 logrus.Warnf("Encountered error in resolving items to upload for %s: %v", item, err) 134 continue 135 } 136 if info.IsDir() { 137 gatherArtifacts(item, blobStoragePath, info.Name(), uploadTargets) 138 } else { 139 metadataFromFileName, writerOptions := gcs.WriterOptionsFromFileName(info.Name()) 140 destination := path.Join(blobStoragePath, metadataFromFileName) 141 if _, exists := uploadTargets[destination]; exists { 142 logrus.Warnf("Encountered duplicate upload of %s, skipping...", destination) 143 continue 144 } 145 uploadTargets[destination] = gcs.FileUploadWithOptions(item, writerOptions) 146 } 147 } 148 149 if len(extra) == 0 { 150 return uploadTargets, nil, nil 151 } 152 153 extraTargets := make(map[string]gcs.UploadFunc, len(extra)) 154 for destination, upload := range extra { 155 extraTargets[path.Join(blobStoragePath, destination)] = upload 156 } 157 158 return uploadTargets, extraTargets, nil 159 } 160 161 // PathsForJob determines the following for a job: 162 // - path in blob storage under the bucket where job artifacts will be uploaded for: 163 // - the job 164 // - this specific run of the job (if any subdir is present) 165 // 166 // The builder for the job is also returned for use in other path resolution. 167 func PathsForJob(options *prowapi.GCSConfiguration, spec *downwardapi.JobSpec, subdir string) (string, string, gcs.RepoPathBuilder) { 168 builder := builderForStrategy(options.PathStrategy, options.DefaultOrg, options.DefaultRepo) 169 jobBasePath := gcs.PathForSpec(spec, builder) 170 if options.PathPrefix != "" { 171 jobBasePath = path.Join(options.PathPrefix, jobBasePath) 172 } 173 var blobStoragePath string 174 if subdir == "" { 175 blobStoragePath = jobBasePath 176 } else { 177 blobStoragePath = path.Join(jobBasePath, subdir) 178 } 179 180 return jobBasePath, blobStoragePath, builder 181 } 182 183 func builderForStrategy(strategy, defaultOrg, defaultRepo string) gcs.RepoPathBuilder { 184 var builder gcs.RepoPathBuilder 185 switch strategy { 186 case prowapi.PathStrategyExplicit: 187 builder = gcs.NewExplicitRepoPathBuilder() 188 case prowapi.PathStrategyLegacy: 189 builder = gcs.NewLegacyRepoPathBuilder(defaultOrg, defaultRepo) 190 case prowapi.PathStrategySingle: 191 builder = gcs.NewSingleDefaultRepoPathBuilder(defaultOrg, defaultRepo) 192 } 193 194 return builder 195 } 196 197 func gatherArtifacts(artifactDir, blobStoragePath, subDir string, uploadTargets map[string]gcs.UploadFunc) { 198 logrus.Printf("Gathering artifacts from artifact directory: %s", artifactDir) 199 filepath.Walk(artifactDir, func(fspath string, info os.FileInfo, err error) error { 200 if info == nil || info.IsDir() { 201 return nil 202 } 203 204 // we know path will be below artifactDir, but we can't 205 // communicate that to the filepath module. We can ignore 206 // this error as we can be certain it won't occur and best- 207 // effort upload is OK in any case 208 if relPath, err := filepath.Rel(artifactDir, fspath); err == nil { 209 dir, filename := path.Split(path.Join(blobStoragePath, subDir, relPath)) 210 metadataFromFileName, writerOptions := gcs.WriterOptionsFromFileName(filename) 211 destination := escapeFileName(path.Join(dir, metadataFromFileName)) 212 if _, exists := uploadTargets[destination]; exists { 213 logrus.Warnf("Encountered duplicate upload of %s, skipping...", destination) 214 return nil 215 } 216 logrus.Printf("Found %s in artifact directory. Uploading as %s\n", fspath, destination) 217 uploadTargets[destination] = gcs.FileUploadWithOptions(fspath, writerOptions) 218 } else { 219 logrus.Warnf("Encountered error in relative path calculation for %s under %s: %v", fspath, artifactDir, err) 220 } 221 return nil 222 }) 223 } 224 225 // escapeFileName escapes a file name to meet https://cloud.google.com/storage/docs/naming-objects requirements 226 func escapeFileName(filename string) string { 227 return strings.ReplaceAll(filename, "#", "%23") 228 } 229 230 func newStringReadCloser(s string) gcs.ReaderFunc { 231 return func() (io.ReadCloser, error) { 232 return io.NopCloser(strings.NewReader(s)), nil 233 } 234 }