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