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  }