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 }