github.imxd.top/openshift/source-to-image@v1.2.0/pkg/scripts/install.go (about)

     1  package scripts
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/openshift/source-to-image/pkg/api"
    10  	"github.com/openshift/source-to-image/pkg/api/constants"
    11  	"github.com/openshift/source-to-image/pkg/docker"
    12  	s2ierr "github.com/openshift/source-to-image/pkg/errors"
    13  	"github.com/openshift/source-to-image/pkg/util/fs"
    14  )
    15  
    16  // Installer interface is responsible for installing scripts needed to run the
    17  // build.
    18  type Installer interface {
    19  	InstallRequired(scripts []string, dstDir string) ([]api.InstallResult, error)
    20  	InstallOptional(scripts []string, dstDir string) []api.InstallResult
    21  }
    22  
    23  // ScriptHandler provides an interface for various scripts source handlers.
    24  type ScriptHandler interface {
    25  	Get(script string) *api.InstallResult
    26  	Install(*api.InstallResult) error
    27  	SetDestinationDir(string)
    28  	String() string
    29  }
    30  
    31  // URLScriptHandler handles script download using URL.
    32  type URLScriptHandler struct {
    33  	URL            string
    34  	DestinationDir string
    35  	Download       Downloader
    36  	FS             fs.FileSystem
    37  	Name           string
    38  }
    39  
    40  const (
    41  	sourcesRootAbbrev = "<source-dir>"
    42  
    43  	// ScriptURLHandler is the name of the script URL handler
    44  	ScriptURLHandler = "script URL handler"
    45  
    46  	// ImageURLHandler is the name of the image URL handler
    47  	ImageURLHandler = "image URL handler"
    48  
    49  	// SourceHandler is the name of the source script handler
    50  	SourceHandler = "source handler"
    51  )
    52  
    53  var (
    54  	// RequiredScripts must be present to do an s2i build
    55  	RequiredScripts = []string{constants.Assemble, constants.Run}
    56  
    57  	// OptionalScripts may be provided when doing an s2i build
    58  	OptionalScripts = []string{constants.SaveArtifacts}
    59  )
    60  
    61  // SetDestinationDir sets the destination where the scripts should be
    62  // downloaded.
    63  func (s *URLScriptHandler) SetDestinationDir(baseDir string) {
    64  	s.DestinationDir = baseDir
    65  }
    66  
    67  // String implements the String() function.
    68  func (s *URLScriptHandler) String() string {
    69  	return s.Name
    70  }
    71  
    72  // Get parses the provided URL and the script name.
    73  func (s *URLScriptHandler) Get(script string) *api.InstallResult {
    74  	if len(s.URL) == 0 {
    75  		return nil
    76  	}
    77  	scriptURL, err := url.ParseRequestURI(s.URL + "/" + script)
    78  	if err != nil {
    79  		log.Infof("invalid script url %q: %v", s.URL, err)
    80  		return nil
    81  	}
    82  	return &api.InstallResult{
    83  		Script: script,
    84  		URL:    scriptURL.String(),
    85  	}
    86  }
    87  
    88  // Install downloads the script and fix its permissions.
    89  func (s *URLScriptHandler) Install(r *api.InstallResult) error {
    90  	downloadURL, err := url.Parse(r.URL)
    91  	if err != nil {
    92  		return err
    93  	}
    94  	dst := filepath.Join(s.DestinationDir, constants.UploadScripts, r.Script)
    95  	if _, err := s.Download.Download(downloadURL, dst); err != nil {
    96  		if e, ok := err.(s2ierr.Error); ok {
    97  			if e.ErrorCode == s2ierr.ScriptsInsideImageError {
    98  				r.Installed = true
    99  				return nil
   100  			}
   101  		}
   102  		return err
   103  	}
   104  	if err := s.FS.Chmod(dst, 0755); err != nil {
   105  		return err
   106  	}
   107  	r.Installed = true
   108  	r.Downloaded = true
   109  	return nil
   110  }
   111  
   112  // SourceScriptHandler handles the case when the scripts are contained in the
   113  // source code directory.
   114  type SourceScriptHandler struct {
   115  	DestinationDir string
   116  	fs             fs.FileSystem
   117  }
   118  
   119  // Get verifies if the script is present in the source directory and get the
   120  // installation result.
   121  func (s *SourceScriptHandler) Get(script string) *api.InstallResult {
   122  	location := filepath.Join(s.DestinationDir, constants.SourceScripts, script)
   123  	if s.fs.Exists(location) {
   124  		return &api.InstallResult{Script: script, URL: location}
   125  	}
   126  	// TODO: The '.sti/bin' path inside the source code directory is deprecated
   127  	// and this should (and will) be removed soon.
   128  	location = filepath.FromSlash(strings.Replace(filepath.ToSlash(location), "s2i/bin", "sti/bin", 1))
   129  	if s.fs.Exists(location) {
   130  		log.Info("DEPRECATED: Use .s2i/bin instead of .sti/bin")
   131  		return &api.InstallResult{Script: script, URL: location}
   132  	}
   133  	return nil
   134  }
   135  
   136  // String implements the String() function.
   137  func (s *SourceScriptHandler) String() string {
   138  	return SourceHandler
   139  }
   140  
   141  // Install copies the script into upload directory and fix its permissions.
   142  func (s *SourceScriptHandler) Install(r *api.InstallResult) error {
   143  	dst := filepath.Join(s.DestinationDir, constants.UploadScripts, r.Script)
   144  	if err := s.fs.Rename(r.URL, dst); err != nil {
   145  		return err
   146  	}
   147  	if err := s.fs.Chmod(dst, 0755); err != nil {
   148  		return err
   149  	}
   150  	// Make the path to scripts nicer in logs
   151  	parts := strings.Split(filepath.ToSlash(r.URL), "/")
   152  	if len(parts) > 3 {
   153  		r.URL = filepath.FromSlash(sourcesRootAbbrev + "/" + strings.Join(parts[len(parts)-3:], "/"))
   154  	}
   155  	r.Installed = true
   156  	r.Downloaded = true
   157  	return nil
   158  }
   159  
   160  // SetDestinationDir sets the directory where the scripts should be uploaded.
   161  // In case of SourceScriptHandler this is a source directory root.
   162  func (s *SourceScriptHandler) SetDestinationDir(baseDir string) {
   163  	s.DestinationDir = baseDir
   164  }
   165  
   166  // ScriptSourceManager manages various script handlers.
   167  type ScriptSourceManager interface {
   168  	Add(ScriptHandler)
   169  	SetDownloader(Downloader)
   170  	Installer
   171  }
   172  
   173  // DefaultScriptSourceManager manages the default script lookup and installation
   174  // for source-to-image.
   175  type DefaultScriptSourceManager struct {
   176  	Image      string
   177  	ScriptsURL string
   178  	download   Downloader
   179  	docker     docker.Docker
   180  	dockerAuth api.AuthConfig
   181  	sources    []ScriptHandler
   182  	fs         fs.FileSystem
   183  }
   184  
   185  // Add registers a new script source handler.
   186  func (m *DefaultScriptSourceManager) Add(s ScriptHandler) {
   187  	if len(m.sources) == 0 {
   188  		m.sources = []ScriptHandler{}
   189  	}
   190  	m.sources = append(m.sources, s)
   191  }
   192  
   193  // NewInstaller returns a new instance of the default Installer implementation
   194  func NewInstaller(image string, scriptsURL string, proxyConfig *api.ProxyConfig, docker docker.Docker, auth api.AuthConfig, fs fs.FileSystem) Installer {
   195  	m := DefaultScriptSourceManager{
   196  		Image:      image,
   197  		ScriptsURL: scriptsURL,
   198  		dockerAuth: auth,
   199  		docker:     docker,
   200  		fs:         fs,
   201  		download:   NewDownloader(proxyConfig),
   202  	}
   203  	// Order is important here, first we try to get the scripts from provided URL,
   204  	// then we look into sources and check for .s2i/bin scripts.
   205  	if len(m.ScriptsURL) > 0 {
   206  		m.Add(&URLScriptHandler{URL: m.ScriptsURL, Download: m.download, FS: m.fs, Name: ScriptURLHandler})
   207  	}
   208  
   209  	m.Add(&SourceScriptHandler{fs: m.fs})
   210  
   211  	if m.docker != nil {
   212  		// If the detection handlers above fail, try to get the script url from the
   213  		// docker image itself.
   214  		defaultURL, err := m.docker.GetScriptsURL(m.Image)
   215  		if err == nil && defaultURL != "" {
   216  			m.Add(&URLScriptHandler{URL: defaultURL, Download: m.download, FS: m.fs, Name: ImageURLHandler})
   217  		}
   218  	}
   219  	return &m
   220  }
   221  
   222  // InstallRequired Downloads and installs required scripts into dstDir, the result is a
   223  // map of scripts with detailed information about each of the scripts install process
   224  // with error if installing some of them failed
   225  func (m *DefaultScriptSourceManager) InstallRequired(scripts []string, dstDir string) ([]api.InstallResult, error) {
   226  	result := m.InstallOptional(scripts, dstDir)
   227  	failedScripts := []string{}
   228  	var err error
   229  	for _, r := range result {
   230  		if r.Error != nil {
   231  			failedScripts = append(failedScripts, r.Script)
   232  		}
   233  	}
   234  	if len(failedScripts) > 0 {
   235  		err = s2ierr.NewInstallRequiredError(failedScripts, constants.ScriptsURLLabel)
   236  	}
   237  	return result, err
   238  }
   239  
   240  // InstallOptional downloads and installs a set of scripts into dstDir, the result is a
   241  // map of scripts with detailed information about each of the scripts install process
   242  func (m *DefaultScriptSourceManager) InstallOptional(scripts []string, dstDir string) []api.InstallResult {
   243  	result := []api.InstallResult{}
   244  	for _, script := range scripts {
   245  		installed := false
   246  		failedSources := []string{}
   247  		for _, e := range m.sources {
   248  			detected := false
   249  			h := e.(ScriptHandler)
   250  			h.SetDestinationDir(dstDir)
   251  			if r := h.Get(script); r != nil {
   252  				if err := h.Install(r); err != nil {
   253  					failedSources = append(failedSources, h.String())
   254  					// all this means is this source didn't have this particular script
   255  					log.V(4).Infof("script %q found by the %s, but failed to install: %v", script, h, err)
   256  				} else {
   257  					r.FailedSources = failedSources
   258  					result = append(result, *r)
   259  					installed = true
   260  					detected = true
   261  					log.V(4).Infof("Using %q installed from %q", script, r.URL)
   262  				}
   263  			}
   264  			if detected {
   265  				break
   266  			}
   267  		}
   268  		if !installed {
   269  			result = append(result, api.InstallResult{
   270  				FailedSources: failedSources,
   271  				Script:        script,
   272  				Error:         fmt.Errorf("script %q not installed", script),
   273  			})
   274  		}
   275  	}
   276  	return result
   277  }