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