1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
|
package imagebuildah
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/containers/buildah"
"github.com/containers/common/pkg/config"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/types"
encconfig "github.com/containers/ocicrypt/config"
"github.com/containers/storage"
"github.com/containers/storage/pkg/archive"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/openshift/imagebuilder"
"github.com/openshift/imagebuilder/dockerfile/parser"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
PullIfMissing = buildah.PullIfMissing
PullAlways = buildah.PullAlways
PullIfNewer = buildah.PullIfNewer
PullNever = buildah.PullNever
Gzip = archive.Gzip
Bzip2 = archive.Bzip2
Xz = archive.Xz
Zstd = archive.Zstd
Uncompressed = archive.Uncompressed
)
// Mount is a mountpoint for the build container.
type Mount specs.Mount
// BuildOptions can be used to alter how an image is built.
type BuildOptions struct {
// ContextDirectory is the default source location for COPY and ADD
// commands.
ContextDirectory string
// PullPolicy controls whether or not we pull images. It should be one
// of PullIfMissing, PullAlways, PullIfNewer, or PullNever.
PullPolicy buildah.PullPolicy
// Registry is a value which is prepended to the image's name, if it
// needs to be pulled and the image name alone can not be resolved to a
// reference to a source image. No separator is implicitly added.
Registry string
// IgnoreUnrecognizedInstructions tells us to just log instructions we
// don't recognize, and try to keep going.
IgnoreUnrecognizedInstructions bool
// Manifest Name to which the image will be added.
Manifest string
// Quiet tells us whether or not to announce steps as we go through them.
Quiet bool
// Isolation controls how Run() runs things.
Isolation buildah.Isolation
// Runtime is the name of the command to run for RUN instructions when
// Isolation is either IsolationDefault or IsolationOCI. It should
// accept the same arguments and flags that runc does.
Runtime string
// RuntimeArgs adds global arguments for the runtime.
RuntimeArgs []string
// TransientMounts is a list of mounts that won't be kept in the image.
TransientMounts []string
// Compression specifies the type of compression which is applied to
// layer blobs. The default is to not use compression, but
// archive.Gzip is recommended.
Compression archive.Compression
// Arguments which can be interpolated into Dockerfiles
Args map[string]string
// Name of the image to write to.
Output string
// Additional tags to add to the image that we write, if we know of a
// way to add them.
AdditionalTags []string
// Log is a callback that will print a progress message. If no value
// is supplied, the message will be sent to Err (or os.Stderr, if Err
// is nil) by default.
Log func(format string, args ...interface{})
// In is connected to stdin for RUN instructions.
In io.Reader
// Out is a place where non-error log messages are sent.
Out io.Writer
// Err is a place where error log messages should be sent.
Err io.Writer
// SignaturePolicyPath specifies an override location for the signature
// policy which should be used for verifying the new image as it is
// being written. Except in specific circumstances, no value should be
// specified, indicating that the shared, system-wide default policy
// should be used.
SignaturePolicyPath string
// ReportWriter is an io.Writer which will be used to report the
// progress of the (possible) pulling of the source image and the
// writing of the new image.
ReportWriter io.Writer
// OutputFormat is the format of the output image's manifest and
// configuration data.
// Accepted values are buildah.OCIv1ImageManifest and buildah.Dockerv2ImageManifest.
OutputFormat string
// SystemContext holds parameters used for authentication.
SystemContext *types.SystemContext
// NamespaceOptions controls how we set up namespaces processes that we
// might need when handling RUN instructions.
NamespaceOptions []buildah.NamespaceOption
// ConfigureNetwork controls whether or not network interfaces and
// routing are configured for a new network namespace (i.e., when not
// joining another's namespace and not just using the host's
// namespace), effectively deciding whether or not the process has a
// usable network.
ConfigureNetwork buildah.NetworkConfigurationPolicy
// CNIPluginPath is the location of CNI plugin helpers, if they should be
// run from a location other than the default location.
CNIPluginPath string
// CNIConfigDir is the location of CNI configuration files, if the files in
// the default configuration directory shouldn't be used.
CNIConfigDir string
// ID mapping options to use if we're setting up our own user namespace
// when handling RUN instructions.
IDMappingOptions *buildah.IDMappingOptions
// AddCapabilities is a list of capabilities to add to the default set when
// handling RUN instructions.
AddCapabilities []string
// DropCapabilities is a list of capabilities to remove from the default set
// when handling RUN instructions. If a capability appears in both lists, it
// will be dropped.
DropCapabilities []string
// CommonBuildOpts is *required*.
CommonBuildOpts *buildah.CommonBuildOptions
// DefaultMountsFilePath is the file path holding the mounts to be mounted in "host-path:container-path" format
DefaultMountsFilePath string
// IIDFile tells the builder to write the image ID to the specified file
IIDFile string
// Squash tells the builder to produce an image with a single layer
// instead of with possibly more than one layer.
Squash bool
// Labels metadata for an image
Labels []string
// Annotation metadata for an image
Annotations []string
// OnBuild commands to be run by images based on this image
OnBuild []string
// Layers tells the builder to create a cache of images for each step in the Dockerfile
Layers bool
// NoCache tells the builder to build the image from scratch without checking for a cache.
// It creates a new set of cached images for the build.
NoCache bool
// RemoveIntermediateCtrs tells the builder whether to remove intermediate containers used
// during the build process. Default is true.
RemoveIntermediateCtrs bool
// ForceRmIntermediateCtrs tells the builder to remove all intermediate containers even if
// the build was unsuccessful.
ForceRmIntermediateCtrs bool
// BlobDirectory is a directory which we'll use for caching layer blobs.
BlobDirectory string
// Target the targeted FROM in the Dockerfile to build.
Target string
// Devices are the additional devices to add to the containers.
Devices []string
// SignBy is the fingerprint of a GPG key to use for signing images.
SignBy string
// Architecture specifies the target architecture of the image to be built.
Architecture string
// Timestamp sets the created timestamp to the specified time, allowing
// for deterministic, content-addressable builds.
Timestamp *time.Time
// OS is the specifies the operating system of the image to be built.
OS string
// MaxPullPushRetries is the maximum number of attempts we'll make to pull or push any one
// image from or to an external registry if the first attempt fails.
MaxPullPushRetries int
// PullPushRetryDelay is how long to wait before retrying a pull or push attempt.
PullPushRetryDelay time.Duration
// OciDecryptConfig contains the config that can be used to decrypt an image if it is
// encrypted if non-nil. If nil, it does not attempt to decrypt an image.
OciDecryptConfig *encconfig.DecryptConfig
// Jobs is the number of stages to run in parallel. If not specified it defaults to 1.
Jobs *int
// LogRusage logs resource usage for each step.
LogRusage bool
// Excludes is a list of excludes to be used instead of the .dockerignore file.
Excludes []string
// From is the image name to use to replace the value specified in the first
// FROM instruction in the Containerfile
From string
}
// BuildDockerfiles parses a set of one or more Dockerfiles (which may be
// URLs), creates a new Executor, and then runs Prepare/Execute/Commit/Delete
// over the entire set of instructions.
func BuildDockerfiles(ctx context.Context, store storage.Store, options BuildOptions, paths ...string) (string, reference.Canonical, error) {
if len(paths) == 0 {
return "", nil, errors.Errorf("error building: no dockerfiles specified")
}
var dockerfiles []io.ReadCloser
defer func(dockerfiles ...io.ReadCloser) {
for _, d := range dockerfiles {
d.Close()
}
}(dockerfiles...)
for _, dfile := range paths {
var data io.ReadCloser
if strings.HasPrefix(dfile, "http://") || strings.HasPrefix(dfile, "https://") {
logrus.Debugf("reading remote Dockerfile %q", dfile)
resp, err := http.Get(dfile)
if err != nil {
return "", nil, err
}
if resp.ContentLength == 0 {
resp.Body.Close()
return "", nil, errors.Errorf("no contents in %q", dfile)
}
data = resp.Body
} else {
dinfo, err := os.Stat(dfile)
if err != nil {
// If the Dockerfile isn't available, try again with
// context directory prepended (if not prepended yet).
if !strings.HasPrefix(dfile, options.ContextDirectory) {
dfile = filepath.Join(options.ContextDirectory, dfile)
dinfo, err = os.Stat(dfile)
}
}
if err != nil {
return "", nil, err
}
// If given a directory, add '/Dockerfile' to it.
if dinfo.Mode().IsDir() {
dfile = filepath.Join(dfile, "Dockerfile")
}
logrus.Debugf("reading local Dockerfile %q", dfile)
contents, err := os.Open(dfile)
if err != nil {
return "", nil, err
}
dinfo, err = contents.Stat()
if err != nil {
contents.Close()
return "", nil, errors.Wrapf(err, "error reading info about %q", dfile)
}
if dinfo.Mode().IsRegular() && dinfo.Size() == 0 {
contents.Close()
return "", nil, errors.Errorf("no contents in %q", dfile)
}
data = contents
}
// pre-process Dockerfiles with ".in" suffix
if strings.HasSuffix(dfile, ".in") {
pData, err := preprocessDockerfileContents(data, options.ContextDirectory)
if err != nil {
return "", nil, err
}
data = *pData
}
dockerfiles = append(dockerfiles, data)
}
mainNode, err := imagebuilder.ParseDockerfile(dockerfiles[0])
if err != nil {
return "", nil, errors.Wrapf(err, "error parsing main Dockerfile: %s", dockerfiles[0])
}
warnOnUnsetBuildArgs(mainNode, options.Args)
for _, d := range dockerfiles[1:] {
additionalNode, err := imagebuilder.ParseDockerfile(d)
if err != nil {
return "", nil, errors.Wrapf(err, "error parsing additional Dockerfile %s", d)
}
mainNode.Children = append(mainNode.Children, additionalNode.Children...)
}
exec, err := NewExecutor(store, options, mainNode)
if err != nil {
return "", nil, errors.Wrapf(err, "error creating build executor")
}
b := imagebuilder.NewBuilder(options.Args)
defaultContainerConfig, err := config.Default()
if err != nil {
return "", nil, errors.Wrapf(err, "failed to get container config")
}
b.Env = append(defaultContainerConfig.GetDefaultEnv(), b.Env...)
stages, err := imagebuilder.NewStages(mainNode, b)
if err != nil {
return "", nil, errors.Wrap(err, "error reading multiple stages")
}
if options.Target != "" {
stagesTargeted, ok := stages.ThroughTarget(options.Target)
if !ok {
return "", nil, errors.Errorf("The target %q was not found in the provided Dockerfile", options.Target)
}
stages = stagesTargeted
}
return exec.Build(ctx, stages)
}
func warnOnUnsetBuildArgs(node *parser.Node, args map[string]string) {
for _, child := range node.Children {
switch strings.ToUpper(child.Value) {
case "ARG":
argName := child.Next.Value
if _, ok := args[argName]; !strings.Contains(argName, "=") && !ok {
logrus.Warnf("missing %q build argument. Try adding %q to the command line", argName, fmt.Sprintf("--build-arg %s=<VALUE>", argName))
}
default:
continue
}
}
}
// preprocessDockerfileContents runs CPP(1) in preprocess-only mode on the input
// dockerfile content and will use ctxDir as the base include path.
//
// Note: we cannot use cmd.StdoutPipe() as cmd.Wait() closes it.
func preprocessDockerfileContents(r io.Reader, ctxDir string) (rdrCloser *io.ReadCloser, err error) {
cppPath := "/usr/bin/cpp"
if _, err = os.Stat(cppPath); err != nil {
if os.IsNotExist(err) {
err = errors.Errorf("error: Dockerfile.in support requires %s to be installed", cppPath)
}
return nil, err
}
stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
cmd := exec.Command(cppPath, "-E", "-iquote", ctxDir, "-traditional", "-undef", "-")
cmd.Stdout = &stdout
cmd.Stderr = &stderr
pipe, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
defer func() {
if err != nil {
pipe.Close()
}
}()
if err = cmd.Start(); err != nil {
return nil, err
}
if _, err = io.Copy(pipe, r); err != nil {
return nil, err
}
pipe.Close()
if err = cmd.Wait(); err != nil {
if stderr.Len() > 0 {
err = errors.Wrapf(err, "%v", strings.TrimSpace(stderr.String()))
}
return nil, errors.Wrapf(err, "error pre-processing Dockerfile")
}
rc := ioutil.NopCloser(bytes.NewReader(stdout.Bytes()))
return &rc, nil
}
|