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 }