github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/pkg/api/handlers/compat/images_build.go (about)

     1  package compat
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  
    14  	"github.com/containers/buildah"
    15  	"github.com/containers/buildah/imagebuildah"
    16  	"github.com/containers/image/v5/types"
    17  	"github.com/containers/podman/v2/libpod"
    18  	"github.com/containers/podman/v2/pkg/api/handlers/utils"
    19  	"github.com/containers/podman/v2/pkg/auth"
    20  	"github.com/containers/podman/v2/pkg/channel"
    21  	"github.com/containers/storage/pkg/archive"
    22  	"github.com/gorilla/schema"
    23  	"github.com/pkg/errors"
    24  	"github.com/sirupsen/logrus"
    25  )
    26  
    27  func BuildImage(w http.ResponseWriter, r *http.Request) {
    28  	if hdr, found := r.Header["Content-Type"]; found && len(hdr) > 0 {
    29  		contentType := hdr[0]
    30  		switch contentType {
    31  		case "application/tar":
    32  			logrus.Warnf("tar file content type is  %s, should use \"application/x-tar\" content type", contentType)
    33  		case "application/x-tar":
    34  			break
    35  		default:
    36  			utils.BadRequest(w, "Content-Type", hdr[0],
    37  				fmt.Errorf("Content-Type: %s is not supported. Should be \"application/x-tar\"", hdr[0]))
    38  			return
    39  		}
    40  	}
    41  
    42  	contextDirectory, err := extractTarFile(r)
    43  	if err != nil {
    44  		utils.InternalServerError(w, err)
    45  		return
    46  	}
    47  
    48  	defer func() {
    49  		if logrus.IsLevelEnabled(logrus.DebugLevel) {
    50  			if v, found := os.LookupEnv("PODMAN_RETAIN_BUILD_ARTIFACT"); found {
    51  				if keep, _ := strconv.ParseBool(v); keep {
    52  					return
    53  				}
    54  			}
    55  		}
    56  		err := os.RemoveAll(filepath.Dir(contextDirectory))
    57  		if err != nil {
    58  			logrus.Warn(errors.Wrapf(err, "failed to remove build scratch directory %q", filepath.Dir(contextDirectory)))
    59  		}
    60  	}()
    61  
    62  	query := struct {
    63  		BuildArgs   string   `schema:"buildargs"`
    64  		CacheFrom   string   `schema:"cachefrom"`
    65  		CpuPeriod   uint64   `schema:"cpuperiod"`  // nolint
    66  		CpuQuota    int64    `schema:"cpuquota"`   // nolint
    67  		CpuSetCpus  string   `schema:"cpusetcpus"` // nolint
    68  		CpuShares   uint64   `schema:"cpushares"`  // nolint
    69  		Dockerfile  string   `schema:"dockerfile"`
    70  		ExtraHosts  string   `schema:"extrahosts"`
    71  		ForceRm     bool     `schema:"forcerm"`
    72  		HTTPProxy   bool     `schema:"httpproxy"`
    73  		Labels      string   `schema:"labels"`
    74  		MemSwap     int64    `schema:"memswap"`
    75  		Memory      int64    `schema:"memory"`
    76  		NetworkMode string   `schema:"networkmode"`
    77  		NoCache     bool     `schema:"nocache"`
    78  		Outputs     string   `schema:"outputs"`
    79  		Platform    string   `schema:"platform"`
    80  		Pull        bool     `schema:"pull"`
    81  		Quiet       bool     `schema:"q"`
    82  		Registry    string   `schema:"registry"`
    83  		Remote      string   `schema:"remote"`
    84  		Rm          bool     `schema:"rm"`
    85  		ShmSize     int      `schema:"shmsize"`
    86  		Squash      bool     `schema:"squash"`
    87  		Tag         []string `schema:"t"`
    88  		Target      string   `schema:"target"`
    89  	}{
    90  		Dockerfile: "Dockerfile",
    91  		Registry:   "docker.io",
    92  		Rm:         true,
    93  		ShmSize:    64 * 1024 * 1024,
    94  		Tag:        []string{},
    95  	}
    96  
    97  	decoder := r.Context().Value("decoder").(*schema.Decoder)
    98  	if err := decoder.Decode(&query, r.URL.Query()); err != nil {
    99  		utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err)
   100  		return
   101  	}
   102  
   103  	var output string
   104  	if len(query.Tag) > 0 {
   105  		output = query.Tag[0]
   106  	}
   107  
   108  	var additionalNames []string
   109  	if len(query.Tag) > 1 {
   110  		additionalNames = query.Tag[1:]
   111  	}
   112  
   113  	var buildArgs = map[string]string{}
   114  	if _, found := r.URL.Query()["buildargs"]; found {
   115  		if err := json.Unmarshal([]byte(query.BuildArgs), &buildArgs); err != nil {
   116  			utils.BadRequest(w, "buildargs", query.BuildArgs, err)
   117  			return
   118  		}
   119  	}
   120  
   121  	// convert label formats
   122  	var labels = []string{}
   123  	if _, found := r.URL.Query()["labels"]; found {
   124  		var m = map[string]string{}
   125  		if err := json.Unmarshal([]byte(query.Labels), &m); err != nil {
   126  			utils.BadRequest(w, "labels", query.Labels, err)
   127  			return
   128  		}
   129  
   130  		for k, v := range m {
   131  			labels = append(labels, k+"="+v)
   132  		}
   133  	}
   134  
   135  	pullPolicy := buildah.PullIfMissing
   136  	if _, found := r.URL.Query()["pull"]; found {
   137  		if query.Pull {
   138  			pullPolicy = buildah.PullAlways
   139  		}
   140  	}
   141  
   142  	creds, authfile, key, err := auth.GetCredentials(r)
   143  	if err != nil {
   144  		// Credential value(s) not returned as their value is not human readable
   145  		utils.BadRequest(w, key.String(), "n/a", err)
   146  		return
   147  	}
   148  	defer auth.RemoveAuthfile(authfile)
   149  
   150  	// Channels all mux'ed in select{} below to follow API build protocol
   151  	stdout := channel.NewWriter(make(chan []byte, 1))
   152  	defer stdout.Close()
   153  
   154  	auxout := channel.NewWriter(make(chan []byte, 1))
   155  	defer auxout.Close()
   156  
   157  	stderr := channel.NewWriter(make(chan []byte, 1))
   158  	defer stderr.Close()
   159  
   160  	reporter := channel.NewWriter(make(chan []byte, 1))
   161  	defer reporter.Close()
   162  	buildOptions := imagebuildah.BuildOptions{
   163  		ContextDirectory:               contextDirectory,
   164  		PullPolicy:                     pullPolicy,
   165  		Registry:                       query.Registry,
   166  		IgnoreUnrecognizedInstructions: true,
   167  		Quiet:                          query.Quiet,
   168  		Isolation:                      buildah.IsolationChroot,
   169  		Compression:                    archive.Gzip,
   170  		Args:                           buildArgs,
   171  		Output:                         output,
   172  		AdditionalTags:                 additionalNames,
   173  		Out:                            stdout,
   174  		Err:                            auxout,
   175  		ReportWriter:                   reporter,
   176  		OutputFormat:                   buildah.Dockerv2ImageManifest,
   177  		SystemContext: &types.SystemContext{
   178  			AuthFilePath:     authfile,
   179  			DockerAuthConfig: creds,
   180  		},
   181  		CommonBuildOpts: &buildah.CommonBuildOptions{
   182  			CPUPeriod:  query.CpuPeriod,
   183  			CPUQuota:   query.CpuQuota,
   184  			CPUShares:  query.CpuShares,
   185  			CPUSetCPUs: query.CpuSetCpus,
   186  			HTTPProxy:  query.HTTPProxy,
   187  			Memory:     query.Memory,
   188  			MemorySwap: query.MemSwap,
   189  			ShmSize:    strconv.Itoa(query.ShmSize),
   190  		},
   191  		Squash:                  query.Squash,
   192  		Labels:                  labels,
   193  		NoCache:                 query.NoCache,
   194  		RemoveIntermediateCtrs:  query.Rm,
   195  		ForceRmIntermediateCtrs: query.ForceRm,
   196  		Target:                  query.Target,
   197  	}
   198  
   199  	runtime := r.Context().Value("runtime").(*libpod.Runtime)
   200  	runCtx, cancel := context.WithCancel(context.Background())
   201  	var imageID string
   202  	go func() {
   203  		defer cancel()
   204  		imageID, _, err = runtime.Build(r.Context(), buildOptions, query.Dockerfile)
   205  		if err != nil {
   206  			stderr.Write([]byte(err.Error() + "\n"))
   207  		}
   208  	}()
   209  
   210  	flush := func() {
   211  		if flusher, ok := w.(http.Flusher); ok {
   212  			flusher.Flush()
   213  		}
   214  	}
   215  
   216  	// Send headers and prime client for stream to come
   217  	w.WriteHeader(http.StatusOK)
   218  	w.Header().Add("Content-Type", "application/json")
   219  	flush()
   220  
   221  	var failed bool
   222  
   223  	body := w.(io.Writer)
   224  	if logrus.IsLevelEnabled(logrus.DebugLevel) {
   225  		if v, found := os.LookupEnv("PODMAN_RETAIN_BUILD_ARTIFACT"); found {
   226  			if keep, _ := strconv.ParseBool(v); keep {
   227  				t, _ := ioutil.TempFile("", "build_*_server")
   228  				defer t.Close()
   229  				body = io.MultiWriter(t, w)
   230  			}
   231  		}
   232  	}
   233  
   234  	enc := json.NewEncoder(body)
   235  	enc.SetEscapeHTML(true)
   236  loop:
   237  	for {
   238  		m := struct {
   239  			Stream string `json:"stream,omitempty"`
   240  			Error  string `json:"error,omitempty"`
   241  		}{}
   242  
   243  		select {
   244  		case e := <-stdout.Chan():
   245  			m.Stream = string(e)
   246  			if err := enc.Encode(m); err != nil {
   247  				stderr.Write([]byte(err.Error()))
   248  			}
   249  			flush()
   250  		case e := <-auxout.Chan():
   251  			m.Stream = string(e)
   252  			if err := enc.Encode(m); err != nil {
   253  				stderr.Write([]byte(err.Error()))
   254  			}
   255  			flush()
   256  		case e := <-reporter.Chan():
   257  			m.Stream = string(e)
   258  			if err := enc.Encode(m); err != nil {
   259  				stderr.Write([]byte(err.Error()))
   260  			}
   261  			flush()
   262  		case e := <-stderr.Chan():
   263  			failed = true
   264  			m.Error = string(e)
   265  			if err := enc.Encode(m); err != nil {
   266  				logrus.Warnf("Failed to json encode error %q", err.Error())
   267  			}
   268  			flush()
   269  		case <-runCtx.Done():
   270  			if !failed {
   271  				if !utils.IsLibpodRequest(r) {
   272  					m.Stream = fmt.Sprintf("Successfully built %12.12s\n", imageID)
   273  					if err := enc.Encode(m); err != nil {
   274  						logrus.Warnf("Failed to json encode error %q", err.Error())
   275  					}
   276  					flush()
   277  				}
   278  			}
   279  			break loop
   280  		}
   281  	}
   282  }
   283  
   284  func extractTarFile(r *http.Request) (string, error) {
   285  	// build a home for the request body
   286  	anchorDir, err := ioutil.TempDir("", "libpod_builder")
   287  	if err != nil {
   288  		return "", err
   289  	}
   290  
   291  	path := filepath.Join(anchorDir, "tarBall")
   292  	tarBall, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
   293  	if err != nil {
   294  		return "", err
   295  	}
   296  	defer tarBall.Close()
   297  
   298  	// Content-Length not used as too many existing API clients didn't honor it
   299  	_, err = io.Copy(tarBall, r.Body)
   300  	r.Body.Close()
   301  	if err != nil {
   302  		return "", fmt.Errorf("failed Request: Unable to copy tar file from request body %s", r.RequestURI)
   303  	}
   304  
   305  	buildDir := filepath.Join(anchorDir, "build")
   306  	err = os.Mkdir(buildDir, 0700)
   307  	if err != nil {
   308  		return "", err
   309  	}
   310  
   311  	_, _ = tarBall.Seek(0, 0)
   312  	err = archive.Untar(tarBall, buildDir, nil)
   313  	return buildDir, err
   314  }