github.com/jkawamoto/roadie-azure@v0.3.5/roadie/script.go (about) 1 // 2 // roadie/script.go 3 // 4 // Copyright (c) 2017 Junpei Kawamoto 5 // 6 // This file is part of Roadie Azure. 7 // 8 // Roadie Azure is free software: you can redistribute it and/or modify 9 // it under the terms of the GNU General Public License as published by 10 // the Free Software Foundation, either version 3 of the License, or 11 // (at your option) any later version. 12 // 13 // Roadie Azure is distributed in the hope that it will be useful, 14 // but WITHOUT ANY WARRANTY; without even the implied warranty of 15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 // GNU General Public License for more details. 17 // 18 // You should have received a copy of the GNU General Public License 19 // along with Roadie Azure. If not, see <http://www.gnu.org/licenses/>. 20 // 21 22 package roadie 23 24 import ( 25 "bytes" 26 "context" 27 "fmt" 28 "io" 29 "log" 30 "os" 31 "os/exec" 32 "path/filepath" 33 "strings" 34 "text/template" 35 36 "github.com/Azure/azure-sdk-for-go/storage" 37 "golang.org/x/sync/errgroup" 38 39 "github.com/jkawamoto/roadie-azure/assets" 40 "github.com/jkawamoto/roadie/cloud/azure" 41 "github.com/jkawamoto/roadie/script" 42 "github.com/ulikunitz/xz" 43 ) 44 45 const ( 46 // CompressThreshold defines a threshold. 47 // If uploading stdout files exceed this threshold, they will be compressed. 48 CompressThreshold = 1024 * 1024 49 // DefaultImage defines the default base image of sandbox containers. 50 DefaultImage = "ubuntu:latest" 51 ) 52 53 // Script defines a structure to run commands. 54 type Script struct { 55 *script.Script 56 Logger *log.Logger 57 } 58 59 // NewScript creates a new script from a given named file with a logger. 60 func NewScript(filename string, logger *log.Logger) (res *Script, err error) { 61 62 res = new(Script) 63 res.Script, err = script.NewScript(filename) 64 if err != nil { 65 return 66 } 67 68 res.Logger = logger 69 return 70 71 } 72 73 // PrepareSourceCode prepares source code defined in a given task. 74 func (s *Script) PrepareSourceCode(ctx context.Context) (err error) { 75 76 switch { 77 case s.Source == "": 78 return 79 80 case strings.HasSuffix(s.Source, ".git"): 81 s.Logger.Println("Cloning the source repository", s.Source) 82 cmds := []struct { 83 name string 84 args []string 85 }{ 86 {"git", []string{"init"}}, 87 {"git", []string{"remote", "add", "origin", s.Source}}, 88 {"git", []string{"pull", "origin", "master"}}, 89 } 90 for _, c := range cmds { 91 err = ExecCommand(exec.CommandContext(ctx, c.name, c.args...), s.Logger) 92 if err != nil { 93 return 94 } 95 } 96 return 97 98 case strings.HasPrefix(s.Source, "http://") || strings.HasPrefix(s.Source, "https://") || strings.HasPrefix(s.Source, "dropbox://"): 99 // Files hosted on a HTTP server. 100 s.Logger.Println("Downloading the source code", s.Source) 101 var obj *Object 102 obj, err = OpenURL(ctx, s.Source) 103 if err != nil { 104 return 105 } 106 defer obj.Body.Close() 107 108 switch { 109 case strings.HasSuffix(obj.Name, ".gz") || strings.HasSuffix(obj.Name, ".xz") || strings.HasSuffix(obj.Name, ".zip"): 110 // Archived files. 111 return NewExpander(s.Logger).Expand(ctx, obj) 112 113 default: 114 // Plain files. 115 var fp *os.File 116 fp, err = os.OpenFile(filepath.Join(obj.Dest, obj.Name), os.O_CREATE|os.O_WRONLY, 0644) 117 if err != nil { 118 return 119 } 120 defer fp.Close() 121 _, err = io.Copy(fp, obj.Body) 122 return 123 } 124 125 case strings.HasPrefix(s.Source, "file://"): 126 // Local file. 127 s.Logger.Println("Copying the source code", s.Source) 128 filename := s.Source[len("file://"):] 129 130 switch { 131 case strings.HasSuffix(s.Source, ".gz") || strings.HasSuffix(s.Source, ".xz") || strings.HasSuffix(s.Source, ".zip"): 132 // Archived file. 133 s.Logger.Println("Expanding the source file", filename) 134 var fp *os.File 135 fp, err = os.Open(filename) 136 if err != nil { 137 return 138 } 139 defer fp.Close() 140 141 return NewExpander(s.Logger).Expand(ctx, &Object{ 142 Name: filename, 143 Dest: ".", 144 Body: fp, 145 }) 146 147 default: 148 // Plain file. 149 return os.Symlink(filename, filepath.Base(filename)) 150 151 } 152 153 } 154 return fmt.Errorf("Unsupported source file type: %v", s.Source) 155 156 } 157 158 // DownloadDataFiles downloads files specified in data section. 159 func (s *Script) DownloadDataFiles(ctx context.Context) (err error) { 160 161 e := NewExpander(s.Logger) 162 eg, ctx := errgroup.WithContext(ctx) 163 for _, v := range s.Data { 164 165 select { 166 case <-ctx.Done(): 167 break 168 default: 169 } 170 171 url := v 172 eg.Go(func() (err error) { 173 s.Logger.Println("Downloading data file", url) 174 obj, err := OpenURL(ctx, url) 175 if err != nil { 176 return 177 } 178 179 switch { 180 case strings.HasSuffix(obj.Name, ".gz") || strings.HasSuffix(obj.Name, ".xz") || strings.HasSuffix(obj.Name, ".zip"): 181 // Archived file. 182 err = e.Expand(ctx, obj) 183 if err != nil { 184 return 185 } 186 187 default: 188 // Plain file 189 var fp *os.File 190 fp, err = os.OpenFile(filepath.Join(obj.Dest, obj.Name), os.O_CREATE|os.O_WRONLY, 0644) 191 if err != nil { 192 return 193 } 194 _, err = io.Copy(fp, obj.Body) 195 if err != nil { 196 return 197 } 198 199 } 200 s.Logger.Println("Finished downloading data file", url) 201 return 202 203 }) 204 205 } 206 207 return eg.Wait() 208 } 209 210 // UploadResults uploads result files. 211 func (s *Script) UploadResults(ctx context.Context, store *azure.StorageService) (err error) { 212 213 dir := strings.TrimPrefix(s.Name, "task-") 214 215 s.Logger.Println("Uploading result files") 216 eg, ctx := errgroup.WithContext(ctx) 217 for i := range s.Run { 218 219 idx := i 220 eg.Go(func() (err error) { 221 222 var reader io.Reader 223 s.Logger.Printf("Uploading stdout%v.txt\n", idx) 224 225 filename := fmt.Sprintf("/tmp/stdout%v.txt", idx) 226 info, err := os.Stat(filename) 227 if err != nil { 228 s.Logger.Printf("Cannot find stdout%v.txt\n", idx) 229 return 230 } 231 232 fp, err := os.Open(filename) 233 if err != nil { 234 s.Logger.Printf("Cannot find stdout%v.txt\n", idx) 235 return 236 } 237 defer fp.Close() 238 outfile := fmt.Sprintf("%s/stdout%v.txt", dir, idx) 239 reader = fp 240 contentType := "text/plain" 241 242 if info.Size() > CompressThreshold { 243 244 var xzReader io.Reader 245 xzReader, err = xz.NewReader(reader) 246 if err != nil { 247 s.Logger.Println("Cannot compress an uploading file:", err) 248 } else { 249 reader = xzReader 250 outfile = fmt.Sprintf("%v.xz", outfile) 251 contentType = "application/x-xz" 252 } 253 254 } 255 err = store.UploadWithMetadata(ctx, azure.ResultContainer, outfile, reader, &storage.BlobProperties{ 256 ContentType: contentType, 257 }, nil) 258 if err != nil { 259 s.Logger.Printf("Fiald to upload stdout%v.txt", idx) 260 return 261 } 262 s.Logger.Printf("stdout%v.txt is uploaded", idx) 263 return 264 }) 265 266 } 267 268 var matches []string 269 for _, v := range s.Upload { 270 matches, err = filepath.Glob(v) 271 if err != nil { 272 s.Logger.Printf("Not match any files to %v: %v", v, err) 273 continue 274 } 275 276 for _, file := range matches { 277 278 name := file 279 eg.Go(func() (err error) { 280 s.Logger.Println("Uploading", name) 281 fp, err := os.Open(name) 282 if err != nil { 283 s.Logger.Println("Cannot find", name, ":", err.Error()) 284 return 285 } 286 defer fp.Close() 287 288 err = store.UploadWithMetadata(ctx, azure.ResultContainer, fmt.Sprintf("%s/%v", dir, filepath.Base(name)), fp, nil, nil) 289 if err != nil { 290 s.Logger.Println("Cannot upload", name) 291 return 292 } 293 s.Logger.Printf("%v is uploaded", name) 294 return 295 }) 296 297 } 298 299 } 300 301 err = eg.Wait() 302 if err != nil { 303 s.Logger.Printf("Failed uploading result files: %v", err) 304 return 305 } 306 307 s.Logger.Println("Finished uploading result files") 308 return 309 310 } 311 312 // Dockerfile generates a dockerfile for this script. 313 func (s *Script) Dockerfile() (res []byte, err error) { 314 315 if s.Image == "" { 316 s.Image = DefaultImage 317 } 318 319 data, err := assets.Asset("assets/Dockerfile") 320 if err != nil { 321 return 322 } 323 324 temp, err := template.New("").Parse(string(data)) 325 if err != nil { 326 return 327 } 328 329 buf := bytes.NewBuffer(nil) 330 err = temp.Execute(buf, s.Script) 331 res = buf.Bytes() 332 return 333 334 } 335 336 // Entrypoint generates an entrypoint script for this script. 337 func (s *Script) Entrypoint() (res []byte, err error) { 338 339 data, err := assets.Asset("assets/entrypoint.sh") 340 if err != nil { 341 return 342 } 343 344 temp, err := template.New("").Parse(string(data)) 345 if err != nil { 346 return 347 } 348 349 buf := bytes.NewBuffer(nil) 350 err = temp.Execute(buf, s.Script) 351 res = buf.Bytes() 352 return 353 354 }