github.com/containerd/nerdctl@v1.7.7/pkg/buildkitutil/buildkitutil.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  /*
    18     Portions from https://github.com/docker/cli/blob/v20.10.9/cli/command/image/build/context.go
    19     Copyright (C) Docker authors.
    20     Licensed under the Apache License, Version 2.0
    21     NOTICE: https://github.com/docker/cli/blob/v20.10.9/NOTICE
    22  */
    23  
    24  package buildkitutil
    25  
    26  import (
    27  	"bytes"
    28  	"encoding/json"
    29  	"errors"
    30  	"fmt"
    31  	"io"
    32  	"io/fs"
    33  	"os"
    34  	"os/exec"
    35  	"path/filepath"
    36  	"runtime"
    37  
    38  	"github.com/containerd/log"
    39  	"github.com/containerd/nerdctl/pkg/rootlessutil"
    40  )
    41  
    42  const (
    43  	// DefaultDockerfileName is the Default filename, read by nerdctl build
    44  	DefaultDockerfileName string = "Dockerfile"
    45  	ContainerfileName     string = "Containerfile"
    46  
    47  	TempDockerfileName string = "docker-build-tempdockerfile-"
    48  )
    49  
    50  func BuildctlBinary() (string, error) {
    51  	return exec.LookPath("buildctl")
    52  }
    53  
    54  func BuildctlBaseArgs(buildkitHost string) []string {
    55  	return []string{"--addr=" + buildkitHost}
    56  }
    57  
    58  func GetBuildkitHost(namespace string) (string, error) {
    59  	if namespace == "" {
    60  		return "", fmt.Errorf("namespace must be specified")
    61  	}
    62  	// Try candidate locations of the current containerd namespace.
    63  	run := "/run/"
    64  	if rootlessutil.IsRootless() {
    65  		var err error
    66  		run, err = rootlessutil.XDGRuntimeDir()
    67  		if err != nil {
    68  			log.L.Warn(err)
    69  			run = fmt.Sprintf("/run/user/%d", rootlessutil.ParentEUID())
    70  		}
    71  	}
    72  	var hostRel []string
    73  	if namespace != "default" {
    74  		hostRel = append(hostRel, fmt.Sprintf("buildkit-%s/buildkitd.sock", namespace))
    75  	}
    76  	hostRel = append(hostRel, "buildkit-default/buildkitd.sock", "buildkit/buildkitd.sock")
    77  	var errs []error //nolint:prealloc
    78  	for _, p := range hostRel {
    79  		log.L.Debugf("Choosing the buildkit host %q, candidates=%v (in %q)", p, hostRel, run)
    80  		buildkitHost := "unix://" + filepath.Join(run, p)
    81  		_, err := pingBKDaemon(buildkitHost)
    82  		if err == nil {
    83  			log.L.Debugf("Chosen buildkit host %q", buildkitHost)
    84  			return buildkitHost, nil
    85  		}
    86  		errs = append(errs, fmt.Errorf("failed to ping to host %s: %w", buildkitHost, err))
    87  	}
    88  	allErr := errors.Join(errs...)
    89  	log.L.WithError(allErr).Error(getHint())
    90  	return "", fmt.Errorf("no buildkit host is available, tried %d candidates: %w", len(hostRel), allErr)
    91  }
    92  
    93  func GetWorkerLabels(buildkitHost string) (labels map[string]string, _ error) {
    94  	buildctlBinary, err := BuildctlBinary()
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	args := BuildctlBaseArgs(buildkitHost)
    99  	args = append(args, "debug", "workers", "--format", "{{json .}}")
   100  	buildctlCheckCmd := exec.Command(buildctlBinary, args...)
   101  	buildctlCheckCmd.Env = os.Environ()
   102  	out, err := buildctlCheckCmd.Output()
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	var workers []json.RawMessage
   107  	if err := json.Unmarshal(out, &workers); err != nil {
   108  		return nil, err
   109  	}
   110  	if len(workers) == 0 {
   111  		return nil, fmt.Errorf("no worker available")
   112  	}
   113  	metadata := map[string]json.RawMessage{}
   114  	if err := json.Unmarshal(workers[0], &metadata); err != nil {
   115  		return nil, err
   116  	}
   117  	labelsRaw, ok := metadata["labels"]
   118  	if !ok {
   119  		return nil, fmt.Errorf("worker doesn't have labels")
   120  	}
   121  	labels = map[string]string{}
   122  	if err := json.Unmarshal(labelsRaw, &labels); err != nil {
   123  		return nil, err
   124  	}
   125  	return labels, nil
   126  }
   127  
   128  func getHint() string {
   129  	hint := "`buildctl` needs to be installed and `buildkitd` needs to be running, see https://github.com/moby/buildkit"
   130  	if rootlessutil.IsRootless() {
   131  		hint += " , and `containerd-rootless-setuptool.sh install-buildkit` for OCI worker or `containerd-rootless-setuptool.sh install-buildkit-containerd` for containerd worker"
   132  	}
   133  	return hint
   134  }
   135  
   136  func PingBKDaemon(buildkitHost string) error {
   137  	if out, err := pingBKDaemon(buildkitHost); err != nil {
   138  		if out != "" {
   139  			log.L.Error(out)
   140  		}
   141  		return fmt.Errorf(getHint()+": %w", err)
   142  	}
   143  	return nil
   144  }
   145  
   146  func pingBKDaemon(buildkitHost string) (output string, _ error) {
   147  	if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
   148  		return "", errors.New("only linux and freebsd are supported")
   149  	}
   150  	buildctlBinary, err := BuildctlBinary()
   151  	if err != nil {
   152  		return "", err
   153  	}
   154  	args := BuildctlBaseArgs(buildkitHost)
   155  	args = append(args, "debug", "workers")
   156  	buildctlCheckCmd := exec.Command(buildctlBinary, args...)
   157  	buildctlCheckCmd.Env = os.Environ()
   158  	if out, err := buildctlCheckCmd.CombinedOutput(); err != nil {
   159  		return string(out), err
   160  	}
   161  	return "", nil
   162  }
   163  
   164  // WriteTempDockerfile is from https://github.com/docker/cli/blob/v20.10.9/cli/command/image/build/context.go#L118
   165  func WriteTempDockerfile(rc io.Reader) (dockerfileDir string, err error) {
   166  	// err is a named return value, due to the defer call below.
   167  	dockerfileDir, err = os.MkdirTemp("", TempDockerfileName)
   168  	if err != nil {
   169  		return "", fmt.Errorf("unable to create temporary context directory: %v", err)
   170  	}
   171  	defer func() {
   172  		if err != nil {
   173  			os.RemoveAll(dockerfileDir)
   174  		}
   175  	}()
   176  
   177  	f, err := os.Create(filepath.Join(dockerfileDir, DefaultDockerfileName))
   178  	if err != nil {
   179  		return "", err
   180  	}
   181  	defer f.Close()
   182  	if _, err := io.Copy(f, rc); err != nil {
   183  		return "", err
   184  	}
   185  	return dockerfileDir, nil
   186  }
   187  
   188  // BuildKitFile returns the values for the following buildctl args
   189  // --localfilename=dockerfile={absDir}
   190  // --opt=filename={file}
   191  func BuildKitFile(dir, inputfile string) (absDir string, file string, err error) {
   192  	file = inputfile
   193  	if file == "" || file == "." {
   194  		file = DefaultDockerfileName
   195  	}
   196  	absDir, err = filepath.Abs(dir)
   197  	if err != nil {
   198  		return "", "", err
   199  	}
   200  	if file != DefaultDockerfileName {
   201  		if _, err := os.Lstat(filepath.Join(absDir, file)); err != nil {
   202  			return "", "", err
   203  		}
   204  	} else {
   205  		_, dErr := os.Lstat(filepath.Join(absDir, file))
   206  		_, cErr := os.Lstat(filepath.Join(absDir, ContainerfileName))
   207  		if dErr == nil && cErr == nil {
   208  			// both files exist, prefer Dockerfile.
   209  			dockerfile, err := os.ReadFile(filepath.Join(absDir, DefaultDockerfileName))
   210  			if err != nil {
   211  				return "", "", err
   212  			}
   213  			containerfile, err := os.ReadFile(filepath.Join(absDir, ContainerfileName))
   214  			if err != nil {
   215  				return "", "", err
   216  			}
   217  			if !bytes.Equal(dockerfile, containerfile) {
   218  				log.L.Warnf("%s and %s have different contents, building with %s", DefaultDockerfileName, ContainerfileName, DefaultDockerfileName)
   219  			}
   220  		}
   221  		if dErr != nil {
   222  			if errors.Is(dErr, fs.ErrNotExist) {
   223  				file = ContainerfileName
   224  			} else {
   225  				return "", "", dErr
   226  			}
   227  			if cErr != nil {
   228  				return "", "", cErr
   229  			}
   230  		}
   231  	}
   232  	return absDir, file, nil
   233  }