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  }