github.com/jenspinney/cli@v6.42.1-0.20190207184520-7450c600020e+incompatible/cf/actors/push.go (about)

     1  package actors
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"runtime"
     9  
    10  	"code.cloudfoundry.org/cli/cf/api/applicationbits"
    11  	"code.cloudfoundry.org/cli/cf/api/resources"
    12  	"code.cloudfoundry.org/cli/cf/appfiles"
    13  	. "code.cloudfoundry.org/cli/cf/i18n"
    14  	"code.cloudfoundry.org/cli/cf/models"
    15  	"code.cloudfoundry.org/gofileutils/fileutils"
    16  )
    17  
    18  const windowsPathPrefix = `\\?\`
    19  
    20  //go:generate counterfeiter . PushActor
    21  
    22  type PushActor interface {
    23  	UploadApp(appGUID string, zipFile *os.File, presentFiles []resources.AppFileResource) error
    24  	ProcessPath(dirOrZipFile string, f func(string) error) error
    25  	GatherFiles(localFiles []models.AppFileFields, appDir string, uploadDir string, useCache bool) ([]resources.AppFileResource, bool, error)
    26  	ValidateAppParams(apps []models.AppParams) []error
    27  	MapManifestRoute(routeName string, app models.Application, appParamsFromContext models.AppParams) error
    28  }
    29  
    30  type PushActorImpl struct {
    31  	appBitsRepo applicationbits.Repository
    32  	appfiles    appfiles.AppFiles
    33  	zipper      appfiles.Zipper
    34  	routeActor  RouteActor
    35  }
    36  
    37  func NewPushActor(appBitsRepo applicationbits.Repository, zipper appfiles.Zipper, appfiles appfiles.AppFiles, routeActor RouteActor) PushActor {
    38  	return PushActorImpl{
    39  		appBitsRepo: appBitsRepo,
    40  		appfiles:    appfiles,
    41  		zipper:      zipper,
    42  		routeActor:  routeActor,
    43  	}
    44  }
    45  
    46  // ProcessPath takes in a director of app files or a zip file which contains
    47  // the app files. If given a zip file, it will extract the zip to a temporary
    48  // location, call the provided callback with that location, and then clean up
    49  // the location after the callback has been executed.
    50  //
    51  // This was done so that the caller of ProcessPath wouldn't need to know if it
    52  // was a zip file or an app dir that it was given, and the caller would not be
    53  // responsible for cleaning up the temporary directory ProcessPath creates when
    54  // given a zip.
    55  func (actor PushActorImpl) ProcessPath(dirOrZipFile string, f func(string) error) error {
    56  	if !actor.zipper.IsZipFile(dirOrZipFile) {
    57  		if filepath.IsAbs(dirOrZipFile) {
    58  			appDir, err := filepath.EvalSymlinks(dirOrZipFile)
    59  			if err != nil {
    60  				return err
    61  			}
    62  			err = f(appDir)
    63  			if err != nil {
    64  				return err
    65  			}
    66  		} else {
    67  			absPath, err := filepath.Abs(dirOrZipFile)
    68  			if err != nil {
    69  				return err
    70  			}
    71  			appDir, err := filepath.EvalSymlinks(absPath)
    72  			if err != nil {
    73  				return err
    74  			}
    75  
    76  			err = f(appDir)
    77  			if err != nil {
    78  				return err
    79  			}
    80  		}
    81  
    82  		return nil
    83  	}
    84  
    85  	tempDir, err := ioutil.TempDir("", "unzipped-app")
    86  	if err != nil {
    87  		return err
    88  	}
    89  
    90  	err = actor.zipper.Unzip(dirOrZipFile, tempDir)
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	err = f(tempDir)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	err = os.RemoveAll(tempDir)
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  func (actor PushActorImpl) GatherFiles(localFiles []models.AppFileFields, appDir string, uploadDir string, useCache bool) ([]resources.AppFileResource, bool, error) {
   109  	appFileResource := []resources.AppFileResource{}
   110  	for _, file := range localFiles {
   111  		appFileResource = append(appFileResource, resources.AppFileResource{
   112  			Path: file.Path,
   113  			Sha1: file.Sha1,
   114  			Size: file.Size,
   115  		})
   116  	}
   117  
   118  	var err error
   119  	// CC returns a list of files that it already has, so an empty list of
   120  	// remoteFiles is equivalent to not using resource caching at all
   121  	remoteFiles := []resources.AppFileResource{}
   122  	if useCache {
   123  		remoteFiles, err = actor.appBitsRepo.GetApplicationFiles(appFileResource)
   124  		if err != nil {
   125  			return []resources.AppFileResource{}, false, err
   126  		}
   127  	}
   128  
   129  	filesToUpload := make([]models.AppFileFields, len(localFiles), len(localFiles))
   130  	copy(filesToUpload, localFiles)
   131  
   132  	for _, remoteFile := range remoteFiles {
   133  		for i, fileToUpload := range filesToUpload {
   134  			if remoteFile.Path == fileToUpload.Path {
   135  				filesToUpload = append(filesToUpload[:i], filesToUpload[i+1:]...)
   136  			}
   137  		}
   138  	}
   139  
   140  	err = actor.appfiles.CopyFiles(filesToUpload, appDir, uploadDir)
   141  	if err != nil {
   142  		return []resources.AppFileResource{}, false, err
   143  	}
   144  
   145  	_, err = os.Stat(filepath.Join(appDir, ".cfignore"))
   146  	if err == nil {
   147  		err = fileutils.CopyPathToPath(filepath.Join(appDir, ".cfignore"), filepath.Join(uploadDir, ".cfignore"))
   148  		if err != nil {
   149  			return []resources.AppFileResource{}, false, err
   150  		}
   151  	}
   152  
   153  	for i := range remoteFiles {
   154  		fullPath, err := filepath.Abs(filepath.Join(appDir, remoteFiles[i].Path))
   155  		if err != nil {
   156  			return []resources.AppFileResource{}, false, err
   157  		}
   158  
   159  		if runtime.GOOS == "windows" {
   160  			fullPath = windowsPathPrefix + fullPath
   161  		}
   162  		fileInfo, err := os.Lstat(fullPath)
   163  		if err != nil {
   164  			return []resources.AppFileResource{}, false, err
   165  		}
   166  		fileMode := fileInfo.Mode()
   167  
   168  		if runtime.GOOS == "windows" {
   169  			fileMode = fileMode | 0700
   170  		}
   171  
   172  		remoteFiles[i].Mode = fmt.Sprintf("%#o", fileMode)
   173  	}
   174  
   175  	return remoteFiles, len(filesToUpload) > 0, nil
   176  }
   177  
   178  func (actor PushActorImpl) UploadApp(appGUID string, zipFile *os.File, presentFiles []resources.AppFileResource) error {
   179  	return actor.appBitsRepo.UploadBits(appGUID, zipFile, presentFiles)
   180  }
   181  
   182  func (actor PushActorImpl) ValidateAppParams(apps []models.AppParams) []error {
   183  	errs := []error{}
   184  
   185  	for _, app := range apps {
   186  		appName := app.Name
   187  
   188  		if app.HealthCheckType != nil && *app.HealthCheckType != "http" && app.HealthCheckHTTPEndpoint != nil {
   189  			errs = append(errs, fmt.Errorf(T("Health check type must be 'http' to set a health check HTTP endpoint.")))
   190  		}
   191  
   192  		if app.Routes != nil {
   193  			if app.Hosts != nil {
   194  				errs = append(errs, fmt.Errorf(T("Application {{.AppName}} must not be configured with both 'routes' and 'host'/'hosts'", map[string]interface{}{"AppName": appName})))
   195  			}
   196  
   197  			if app.Domains != nil {
   198  				errs = append(errs, fmt.Errorf(T("Application {{.AppName}} must not be configured with both 'routes' and 'domain'/'domains'", map[string]interface{}{"AppName": appName})))
   199  			}
   200  
   201  			if app.NoHostname != nil {
   202  				errs = append(errs, fmt.Errorf(T("Application {{.AppName}} must not be configured with both 'routes' and 'no-hostname'", map[string]interface{}{"AppName": appName})))
   203  			}
   204  		}
   205  
   206  		if app.BuildpackURL != nil && app.DockerImage != nil {
   207  			errs = append(errs, fmt.Errorf(T("Application {{.AppName}} must not be configured with both 'buildpack' and 'docker'", map[string]interface{}{"AppName": appName})))
   208  		}
   209  
   210  		if app.Path != nil && app.DockerImage != nil {
   211  			errs = append(errs, fmt.Errorf(T("Application {{.AppName}} must not be configured with both 'docker' and 'path'", map[string]interface{}{"AppName": appName})))
   212  		}
   213  	}
   214  
   215  	if len(errs) > 0 {
   216  		return errs
   217  	}
   218  
   219  	return nil
   220  }
   221  
   222  func (actor PushActorImpl) MapManifestRoute(routeName string, app models.Application, appParamsFromContext models.AppParams) error {
   223  	return actor.routeActor.FindAndBindRoute(routeName, app, appParamsFromContext)
   224  }