sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/resultstore/files.go (about)

     1  /*
     2  Copyright 2023 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 resultstore
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"mime"
    24  	"path/filepath"
    25  	"strings"
    26  
    27  	"google.golang.org/genproto/googleapis/devtools/resultstore/v2"
    28  	"google.golang.org/protobuf/types/known/wrapperspb"
    29  	pio "sigs.k8s.io/prow/pkg/io"
    30  	"sigs.k8s.io/prow/pkg/io/providers"
    31  )
    32  
    33  // fileFinder is the subset of pio.Opener required.
    34  type fileFinder interface {
    35  	Iterator(ctx context.Context, prefix, delimiter string) (pio.ObjectIterator, error)
    36  }
    37  
    38  // DefaultFile describes a file that should exist in ArtifactOpts.Dir.
    39  // If the file is not present, these values will be used instead.
    40  type DefaultFile struct {
    41  	Name string
    42  	Size int64
    43  }
    44  type ArtifactOpts struct {
    45  	// Dir is the top-level directory, including the provider, e.g.
    46  	// "gs://some-bucket/path"; include all files here.
    47  	Dir string
    48  	// ArtifactsDirOnly includes only the "Dir/artifacts/" directory,
    49  	// instead of files in the tree rooted there. Experimental.
    50  	ArtifactsDirOnly bool
    51  	// DefaultFiles are files in directory Dir (not nested) that are
    52  	// included in the output if they don't exist.
    53  	DefaultFiles []DefaultFile
    54  }
    55  
    56  // ArtifactFiles returns the files based on ArtifactOpts.
    57  //
    58  // In the event of error, returns any files collected so far in the
    59  // interest of best effort.
    60  func ArtifactFiles(ctx context.Context, opener fileFinder, o ArtifactOpts) ([]*resultstore.File, error) {
    61  	prefix := ensureTrailingSlash(o.Dir)
    62  	c, err := newFilesCollector(opener, prefix)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	// Collect the files in the top-level dir.
    68  	if err := c.collect(ctx, prefix, "/"); err != nil {
    69  		return c.builder.files, err
    70  	}
    71  
    72  	c.addDefaultFiles(prefix, o.DefaultFiles)
    73  
    74  	if o.ArtifactsDirOnly {
    75  		artifacts := prefix + "artifacts/"
    76  		match := func(name string) bool {
    77  			fmt.Printf("\nname: %q\n", name)
    78  			return name == artifacts
    79  		}
    80  		if err := c.collectDirs(ctx, prefix, match); err != nil {
    81  			return c.builder.files, err
    82  		}
    83  		return c.builder.files, nil
    84  	}
    85  
    86  	// Collect the entire artifacts/ subtree.
    87  	if err := c.collect(ctx, prefix+"artifacts/", ""); err != nil {
    88  		return c.builder.files, err
    89  	}
    90  	return c.builder.files, nil
    91  }
    92  
    93  func ensureTrailingSlash(p string) string {
    94  	if strings.HasSuffix(p, "/") {
    95  		return p
    96  	}
    97  	return p + "/"
    98  }
    99  
   100  type filesCollector struct {
   101  	finder fileFinder
   102  	// The bucket, including provider, e.g. "gs://some-bucket/".
   103  	bucket  string
   104  	builder *filesBuilder
   105  }
   106  
   107  // bucket returns a string of the provider and bucket name, with a
   108  // trailing slash.
   109  func bucket(path string) (string, error) {
   110  	provider, bucket, _, err := providers.ParseStoragePath(path)
   111  	if err != nil {
   112  		return "", err
   113  	}
   114  	return fmt.Sprintf("%s://%s/", provider, bucket), nil
   115  }
   116  
   117  func newFilesCollector(opener fileFinder, prefix string) (*filesCollector, error) {
   118  	b, err := bucket(prefix)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  	return &filesCollector{
   123  		finder:  opener,
   124  		bucket:  b,
   125  		builder: newFilesBuilder(prefix),
   126  	}, nil
   127  }
   128  
   129  // collect collects files from storage using GCS List semantics:
   130  // - prefix should be a "/" terminated path.
   131  // - delimiter should be "/" to search a single dir
   132  // - delimiter should be "" to search the tree below prefix
   133  func (c *filesCollector) collect(ctx context.Context, prefix, delimiter string) error {
   134  	iter, err := c.finder.Iterator(ctx, prefix, delimiter)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	for {
   139  		f, err := iter.Next(ctx)
   140  		if err != nil {
   141  			if err == io.EOF {
   142  				break
   143  			}
   144  			return err
   145  		}
   146  		if f.IsDir {
   147  			continue
   148  		}
   149  		c.builder.Add(c.bucket+f.Name, f.Size)
   150  	}
   151  	return nil
   152  }
   153  
   154  // addDefaultFiles adds default files if not already collected.
   155  func (c *filesCollector) addDefaultFiles(prefix string, files []DefaultFile) {
   156  	if len(files) == 0 {
   157  		return
   158  	}
   159  	seen := map[string]bool{}
   160  	for _, f := range c.builder.files {
   161  		seen[f.Uri] = true
   162  	}
   163  	for _, f := range files {
   164  		name := prefix + f.Name
   165  		if seen[name] {
   166  			continue
   167  		}
   168  		c.builder.Add(name, f.Size)
   169  	}
   170  }
   171  
   172  // collectDirs collects directories in prefix where match is true.
   173  func (c *filesCollector) collectDirs(ctx context.Context, prefix string, match func(string) bool) error {
   174  	iter, err := c.finder.Iterator(ctx, prefix, "/")
   175  	if err != nil {
   176  		return err
   177  	}
   178  	for {
   179  		f, err := iter.Next(ctx)
   180  		if err != nil {
   181  			if err == io.EOF {
   182  				break
   183  			}
   184  			return err
   185  		}
   186  		if !f.IsDir {
   187  			continue
   188  		}
   189  		name := c.bucket + f.Name
   190  		if match(name) {
   191  			c.builder.AddDir(name)
   192  		}
   193  	}
   194  	return nil
   195  }
   196  
   197  type filesBuilder struct {
   198  	prefix string
   199  	trim   func(string) string
   200  	files  []*resultstore.File
   201  }
   202  
   203  func newFilesBuilder(prefix string) *filesBuilder {
   204  	return &filesBuilder{
   205  		prefix: prefix,
   206  		// Trims the prefix from names to produce File.Uid.
   207  		trim: strings.NewReplacer(prefix, "").Replace,
   208  	}
   209  }
   210  
   211  func (b *filesBuilder) Add(name string, size int64) {
   212  	uid := b.trim(name)
   213  	switch uid {
   214  	case "build.log":
   215  		// This file name is unexpected and would cause an upload
   216  		// exception, since ResultStore requires unique Uids.
   217  		return
   218  	case "build-log.txt":
   219  		// This Uid is used to populate the "Build Log" tab in the
   220  		// GUI. We want build-log.txt to appear there.
   221  		uid = "build.log"
   222  	}
   223  	b.files = append(b.files, &resultstore.File{
   224  		Uid:         uid,
   225  		Uri:         name,
   226  		Length:      &wrapperspb.Int64Value{Value: size},
   227  		ContentType: contentType(uid),
   228  	})
   229  }
   230  
   231  func (b *filesBuilder) AddDir(name string) {
   232  	uid := b.trim(name)
   233  	b.files = append(b.files, &resultstore.File{
   234  		Uid: uid,
   235  		Uri: name,
   236  	})
   237  }
   238  
   239  func init() {
   240  	// Avoid the default of "text/x-log" for log files.
   241  	mime.AddExtensionType(".log", "text/plain")
   242  	// May not exist in the container.
   243  	mime.AddExtensionType(".txt", "text/plain")
   244  }
   245  
   246  func contentType(name string) string {
   247  	ps := strings.SplitN(mime.TypeByExtension(filepath.Ext(name)), ";", 2)
   248  	return ps[0]
   249  }