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 }