github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/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  	"strings"
    38  
    39  	"github.com/containerd/log"
    40  	"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
    41  )
    42  
    43  const (
    44  	// DefaultDockerfileName is the Default filename, read by nerdctl build
    45  	DefaultDockerfileName string = "Dockerfile"
    46  	ContainerfileName     string = "Containerfile"
    47  
    48  	TempDockerfileName string = "docker-build-tempdockerfile-"
    49  )
    50  
    51  func BuildctlBinary() (string, error) {
    52  	return exec.LookPath("buildctl")
    53  }
    54  
    55  func BuildctlBaseArgs(buildkitHost string) []string {
    56  	return []string{"--addr=" + buildkitHost}
    57  }
    58  
    59  func GetBuildkitHost(namespace string) (string, error) {
    60  	paths, err := getBuildkitHostCandidates(namespace)
    61  	if err != nil {
    62  		return "", err
    63  	}
    64  
    65  	var errs []error //nolint:prealloc
    66  	for _, buildkitHost := range paths {
    67  		log.L.Debugf("Choosing the buildkit host %q, candidates=%v", buildkitHost, paths)
    68  		_, err := pingBKDaemon(buildkitHost)
    69  		if err == nil {
    70  			log.L.Debugf("Chosen buildkit host %q", buildkitHost)
    71  			return buildkitHost, nil
    72  		}
    73  		errs = append(errs, fmt.Errorf("failed to ping to host %s: %w", buildkitHost, err))
    74  	}
    75  	allErr := errors.Join(errs...)
    76  	log.L.WithError(allErr).Error(getHint())
    77  	return "", fmt.Errorf("no buildkit host is available, tried %d candidates: %w", len(paths), allErr)
    78  }
    79  
    80  func GetWorkerLabels(buildkitHost string) (labels map[string]string, _ error) {
    81  	buildctlBinary, err := BuildctlBinary()
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  	args := BuildctlBaseArgs(buildkitHost)
    86  	args = append(args, "debug", "workers", "--format", "{{json .}}")
    87  	buildctlCheckCmd := exec.Command(buildctlBinary, args...)
    88  	buildctlCheckCmd.Env = os.Environ()
    89  	out, err := buildctlCheckCmd.Output()
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  	var workers []json.RawMessage
    94  	if err := json.Unmarshal(out, &workers); err != nil {
    95  		return nil, err
    96  	}
    97  	if len(workers) == 0 {
    98  		return nil, fmt.Errorf("no worker available")
    99  	}
   100  	metadata := map[string]json.RawMessage{}
   101  	if err := json.Unmarshal(workers[0], &metadata); err != nil {
   102  		return nil, err
   103  	}
   104  	labelsRaw, ok := metadata["labels"]
   105  	if !ok {
   106  		return nil, fmt.Errorf("worker doesn't have labels")
   107  	}
   108  	labels = map[string]string{}
   109  	if err := json.Unmarshal(labelsRaw, &labels); err != nil {
   110  		return nil, err
   111  	}
   112  	return labels, nil
   113  }
   114  
   115  func getHint() string {
   116  	hint := "`buildctl` needs to be installed and `buildkitd` needs to be running, see https://github.com/moby/buildkit"
   117  	if rootlessutil.IsRootless() {
   118  		hint += " , and `containerd-rootless-setuptool.sh install-buildkit` for OCI worker or `containerd-rootless-setuptool.sh install-buildkit-containerd` for containerd worker"
   119  	}
   120  	return hint
   121  }
   122  
   123  func PingBKDaemon(buildkitHost string) error {
   124  	if out, err := pingBKDaemon(buildkitHost); err != nil {
   125  		if out != "" {
   126  			log.L.Error(out)
   127  		}
   128  		return fmt.Errorf(getHint()+": %w", err)
   129  	}
   130  	return nil
   131  }
   132  
   133  // contains open-codes slices.Contains (without generics) from Go 1.21.
   134  // TODO: Replace once Go 1.21 is the minimum supported compiler.
   135  func contains(haystack []string, needle string) bool {
   136  	for i := range haystack {
   137  		if needle == haystack[i] {
   138  			return true
   139  		}
   140  	}
   141  	return false
   142  }
   143  
   144  func pingBKDaemon(buildkitHost string) (output string, _ error) {
   145  	supportedOses := []string{"linux", "freebsd", "windows"}
   146  	if !contains(supportedOses, runtime.GOOS) {
   147  		return "", fmt.Errorf("only %s are supported", strings.Join(supportedOses, ", "))
   148  	}
   149  	buildctlBinary, err := BuildctlBinary()
   150  	if err != nil {
   151  		return "", err
   152  	}
   153  	args := BuildctlBaseArgs(buildkitHost)
   154  	args = append(args, "debug", "workers")
   155  	buildctlCheckCmd := exec.Command(buildctlBinary, args...)
   156  	buildctlCheckCmd.Env = os.Environ()
   157  	if out, err := buildctlCheckCmd.CombinedOutput(); err != nil {
   158  		return string(out), err
   159  	}
   160  	return "", nil
   161  }
   162  
   163  // WriteTempDockerfile is from https://github.com/docker/cli/blob/v20.10.9/cli/command/image/build/context.go#L118
   164  func WriteTempDockerfile(rc io.Reader) (dockerfileDir string, err error) {
   165  	// err is a named return value, due to the defer call below.
   166  	dockerfileDir, err = os.MkdirTemp("", TempDockerfileName)
   167  	if err != nil {
   168  		return "", fmt.Errorf("unable to create temporary context directory: %v", err)
   169  	}
   170  	defer func() {
   171  		if err != nil {
   172  			os.RemoveAll(dockerfileDir)
   173  		}
   174  	}()
   175  
   176  	f, err := os.Create(filepath.Join(dockerfileDir, DefaultDockerfileName))
   177  	if err != nil {
   178  		return "", err
   179  	}
   180  	defer f.Close()
   181  	if _, err := io.Copy(f, rc); err != nil {
   182  		return "", err
   183  	}
   184  	return dockerfileDir, nil
   185  }
   186  
   187  // BuildKitFile returns the values for the following buildctl args
   188  // --localfilename=dockerfile={absDir}
   189  // --opt=filename={file}
   190  func BuildKitFile(dir, inputfile string) (absDir string, file string, err error) {
   191  	file = inputfile
   192  	if file == "" || file == "." {
   193  		file = DefaultDockerfileName
   194  	}
   195  	absDir, err = filepath.Abs(dir)
   196  	if err != nil {
   197  		return "", "", err
   198  	}
   199  	if file != DefaultDockerfileName {
   200  		if _, err := os.Lstat(filepath.Join(absDir, file)); err != nil {
   201  			return "", "", err
   202  		}
   203  	} else {
   204  		_, dErr := os.Lstat(filepath.Join(absDir, file))
   205  		_, cErr := os.Lstat(filepath.Join(absDir, ContainerfileName))
   206  		if dErr == nil && cErr == nil {
   207  			// both files exist, prefer Dockerfile.
   208  			dockerfile, err := os.ReadFile(filepath.Join(absDir, DefaultDockerfileName))
   209  			if err != nil {
   210  				return "", "", err
   211  			}
   212  			containerfile, err := os.ReadFile(filepath.Join(absDir, ContainerfileName))
   213  			if err != nil {
   214  				return "", "", err
   215  			}
   216  			if !bytes.Equal(dockerfile, containerfile) {
   217  				log.L.Warnf("%s and %s have different contents, building with %s", DefaultDockerfileName, ContainerfileName, DefaultDockerfileName)
   218  			}
   219  		}
   220  		if dErr != nil {
   221  			if errors.Is(dErr, fs.ErrNotExist) {
   222  				file = ContainerfileName
   223  			} else {
   224  				return "", "", dErr
   225  			}
   226  			if cErr != nil {
   227  				return "", "", cErr
   228  			}
   229  		}
   230  	}
   231  	return absDir, file, nil
   232  }