github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+incompatible/cf/api/buildpack_bits.go (about)

     1  package api
     2  
     3  import (
     4  	"archive/zip"
     5  	"crypto/tls"
     6  	"crypto/x509"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"mime/multipart"
    11  	gonet "net"
    12  	"net/http"
    13  	"os"
    14  	"path"
    15  	"path/filepath"
    16  	"strings"
    17  	"time"
    18  
    19  	"code.cloudfoundry.org/cli/cf/appfiles"
    20  	"code.cloudfoundry.org/cli/cf/configuration/coreconfig"
    21  	"code.cloudfoundry.org/cli/cf/errors"
    22  	. "code.cloudfoundry.org/cli/cf/i18n"
    23  	"code.cloudfoundry.org/cli/cf/models"
    24  	"code.cloudfoundry.org/cli/cf/net"
    25  	"code.cloudfoundry.org/gofileutils/fileutils"
    26  )
    27  
    28  //go:generate counterfeiter . BuildpackBitsRepository
    29  
    30  type BuildpackBitsRepository interface {
    31  	UploadBuildpack(buildpack models.Buildpack, buildpackFile *os.File, zipFileName string) error
    32  	CreateBuildpackZipFile(buildpackPath string) (*os.File, string, error)
    33  }
    34  
    35  type CloudControllerBuildpackBitsRepository struct {
    36  	config       coreconfig.Reader
    37  	gateway      net.Gateway
    38  	zipper       appfiles.Zipper
    39  	TrustedCerts []tls.Certificate
    40  }
    41  
    42  func NewCloudControllerBuildpackBitsRepository(config coreconfig.Reader, gateway net.Gateway, zipper appfiles.Zipper) (repo CloudControllerBuildpackBitsRepository) {
    43  	repo.config = config
    44  	repo.gateway = gateway
    45  	repo.zipper = zipper
    46  	return
    47  }
    48  
    49  func zipErrorHelper(err error) error {
    50  	return fmt.Errorf("%s: %s", T("Couldn't write zip file"), err.Error())
    51  }
    52  
    53  func (repo CloudControllerBuildpackBitsRepository) CreateBuildpackZipFile(buildpackPath string) (*os.File, string, error) {
    54  	zipFileToUpload, err := ioutil.TempFile("", "buildpack-upload")
    55  	if err != nil {
    56  		return nil, "", fmt.Errorf("%s: %s", T("Couldn't create temp file for upload"), err.Error())
    57  	}
    58  
    59  	var success bool
    60  	defer func() {
    61  		if !success {
    62  			os.RemoveAll(zipFileToUpload.Name())
    63  		}
    64  	}()
    65  
    66  	var buildpackFileName string
    67  	if isWebURL(buildpackPath) {
    68  		buildpackFileName = path.Base(buildpackPath)
    69  		repo.downloadBuildpack(buildpackPath, func(downloadFile *os.File, downloadErr error) {
    70  			if downloadErr != nil {
    71  				err = downloadErr
    72  				return
    73  			}
    74  
    75  			downloadErr = normalizeBuildpackArchive(downloadFile, zipFileToUpload)
    76  			if downloadErr != nil {
    77  				err = downloadErr
    78  				return
    79  			}
    80  		})
    81  		if err != nil {
    82  			return nil, "", zipErrorHelper(err)
    83  		}
    84  	} else {
    85  		buildpackFileName = filepath.Base(buildpackPath)
    86  		dir, err := filepath.Abs(buildpackPath)
    87  		if err != nil {
    88  			return nil, "", zipErrorHelper(err)
    89  		}
    90  
    91  		buildpackFileName = filepath.Base(dir)
    92  		stats, err := os.Stat(dir)
    93  		if err != nil {
    94  			return nil, "", fmt.Errorf("%s: %s", T("Error opening buildpack file"), err.Error())
    95  		}
    96  
    97  		if stats.IsDir() {
    98  			buildpackFileName += ".zip" // FIXME: remove once #71167394 is fixed
    99  			err = repo.zipper.Zip(buildpackPath, zipFileToUpload)
   100  			if err != nil {
   101  				return nil, "", zipErrorHelper(err)
   102  			}
   103  		} else {
   104  			specifiedFile, err := os.Open(buildpackPath)
   105  			if err != nil {
   106  				return nil, "", fmt.Errorf("%s: %s", T("Couldn't open buildpack file"), err.Error())
   107  			}
   108  			err = normalizeBuildpackArchive(specifiedFile, zipFileToUpload)
   109  			if err != nil {
   110  				return nil, "", zipErrorHelper(err)
   111  			}
   112  		}
   113  	}
   114  
   115  	success = true
   116  	return zipFileToUpload, buildpackFileName, nil
   117  }
   118  
   119  func normalizeBuildpackArchive(inputFile *os.File, outputFile *os.File) error {
   120  	stats, toplevelErr := inputFile.Stat()
   121  	if toplevelErr != nil {
   122  		return toplevelErr
   123  	}
   124  
   125  	reader, toplevelErr := zip.NewReader(inputFile, stats.Size())
   126  	if toplevelErr != nil {
   127  		return toplevelErr
   128  	}
   129  
   130  	contents := reader.File
   131  
   132  	parentPath, hasBuildpack := findBuildpackPath(contents)
   133  
   134  	if !hasBuildpack {
   135  		return errors.New(T("Zip archive does not contain a buildpack"))
   136  	}
   137  
   138  	writer := zip.NewWriter(outputFile)
   139  
   140  	for _, file := range contents {
   141  		name := file.Name
   142  		if strings.HasPrefix(name, parentPath) {
   143  			relativeFilename := strings.TrimPrefix(name, parentPath+"/")
   144  			if relativeFilename == "" {
   145  				continue
   146  			}
   147  
   148  			fileInfo := file.FileInfo()
   149  			header, err := zip.FileInfoHeader(fileInfo)
   150  			if err != nil {
   151  				return err
   152  			}
   153  			header.Name = relativeFilename
   154  
   155  			w, err := writer.CreateHeader(header)
   156  			if err != nil {
   157  				return err
   158  			}
   159  
   160  			r, err := file.Open()
   161  			if err != nil {
   162  				return err
   163  			}
   164  
   165  			_, err = io.Copy(w, r)
   166  			if err != nil {
   167  				return err
   168  			}
   169  
   170  			err = r.Close()
   171  			if err != nil {
   172  				return err
   173  			}
   174  		}
   175  	}
   176  
   177  	toplevelErr = writer.Close()
   178  	if toplevelErr != nil {
   179  		return toplevelErr
   180  	}
   181  
   182  	_, toplevelErr = outputFile.Seek(0, 0)
   183  	if toplevelErr != nil {
   184  		return toplevelErr
   185  	}
   186  
   187  	return nil
   188  }
   189  
   190  func findBuildpackPath(zipFiles []*zip.File) (parentPath string, foundBuildpack bool) {
   191  	needle := "bin/compile"
   192  
   193  	for _, file := range zipFiles {
   194  		if strings.HasSuffix(file.Name, needle) {
   195  			foundBuildpack = true
   196  			parentPath = path.Join(file.Name, "..", "..")
   197  			if parentPath == "." {
   198  				parentPath = ""
   199  			}
   200  			return
   201  		}
   202  	}
   203  	return
   204  }
   205  
   206  func isWebURL(path string) bool {
   207  	return strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://")
   208  }
   209  
   210  func (repo CloudControllerBuildpackBitsRepository) downloadBuildpack(url string, cb func(*os.File, error)) {
   211  	fileutils.TempFile("buildpack-download", func(tempfile *os.File, err error) {
   212  		if err != nil {
   213  			cb(nil, err)
   214  			return
   215  		}
   216  
   217  		var certPool *x509.CertPool
   218  		if len(repo.TrustedCerts) > 0 {
   219  			certPool = x509.NewCertPool()
   220  			for _, tlsCert := range repo.TrustedCerts {
   221  				cert, _ := x509.ParseCertificate(tlsCert.Certificate[0])
   222  				certPool.AddCert(cert)
   223  			}
   224  		}
   225  
   226  		client := &http.Client{
   227  			Transport: &http.Transport{
   228  				Dial:            (&gonet.Dialer{Timeout: 5 * time.Second}).Dial,
   229  				TLSClientConfig: &tls.Config{RootCAs: certPool},
   230  				Proxy:           http.ProxyFromEnvironment,
   231  			},
   232  		}
   233  
   234  		response, err := client.Get(url)
   235  		if err != nil {
   236  			cb(nil, err)
   237  			return
   238  		}
   239  		defer response.Body.Close()
   240  
   241  		_, err = io.Copy(tempfile, response.Body)
   242  		if err != nil {
   243  			cb(nil, err)
   244  			return
   245  		}
   246  
   247  		_, err = tempfile.Seek(0, 0)
   248  		if err != nil {
   249  			cb(nil, err)
   250  			return
   251  		}
   252  
   253  		cb(tempfile, nil)
   254  	})
   255  }
   256  
   257  func (repo CloudControllerBuildpackBitsRepository) UploadBuildpack(buildpack models.Buildpack, buildpackFile *os.File, buildpackName string) error {
   258  	defer func() {
   259  		buildpackFile.Close()
   260  		os.Remove(buildpackFile.Name())
   261  	}()
   262  	return repo.performMultiPartUpload(
   263  		fmt.Sprintf("%s/v2/buildpacks/%s/bits", repo.config.APIEndpoint(), buildpack.GUID),
   264  		"buildpack",
   265  		buildpackName,
   266  		buildpackFile)
   267  }
   268  
   269  func (repo CloudControllerBuildpackBitsRepository) performMultiPartUpload(url string, fieldName string, fileName string, body io.Reader) error {
   270  	var capturedErr error
   271  
   272  	fileutils.TempFile("requests", func(requestFile *os.File, err error) {
   273  		if err != nil {
   274  			capturedErr = err
   275  			return
   276  		}
   277  
   278  		writer := multipart.NewWriter(requestFile)
   279  		part, err := writer.CreateFormFile(fieldName, fileName)
   280  
   281  		if err != nil {
   282  			_ = writer.Close()
   283  			capturedErr = err
   284  			return
   285  		}
   286  
   287  		_, err = io.Copy(part, body)
   288  		if err != nil {
   289  			capturedErr = fmt.Errorf("%s: %s", T("Error creating upload"), err.Error())
   290  			return
   291  		}
   292  
   293  		err = writer.Close()
   294  		if err != nil {
   295  			capturedErr = err
   296  			return
   297  		}
   298  
   299  		var request *net.Request
   300  		request, err = repo.gateway.NewRequestForFile("PUT", url, repo.config.AccessToken(), requestFile)
   301  		if err != nil {
   302  			capturedErr = err
   303  			return
   304  		}
   305  
   306  		contentType := fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary())
   307  		request.HTTPReq.Header.Set("Content-Type", contentType)
   308  
   309  		_, err = repo.gateway.PerformRequest(request)
   310  		if err != nil {
   311  			capturedErr = err
   312  		}
   313  	})
   314  
   315  	return capturedErr
   316  }