github.com/sealerio/sealer@v0.11.1-0.20240507115618-f4f89c5853ae/pkg/imageengine/buildah/build.go (about)

     1  // Copyright © 2022 Alibaba Group Holding Ltd.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package buildah
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/sealerio/sealer/pkg/define/options"
    27  
    28  	"github.com/containers/buildah/define"
    29  	"github.com/containers/buildah/imagebuildah"
    30  	buildahcli "github.com/containers/buildah/pkg/cli"
    31  	"github.com/containers/buildah/pkg/parse"
    32  	buildahutil "github.com/containers/buildah/pkg/util"
    33  	"github.com/containers/buildah/util"
    34  	"github.com/containers/common/pkg/auth"
    35  	"github.com/pkg/errors"
    36  	"github.com/sirupsen/logrus"
    37  )
    38  
    39  type buildFlagsWrapper struct {
    40  	*buildahcli.BudResults
    41  	*buildahcli.LayerResults
    42  	*buildahcli.FromAndBudResults
    43  	*buildahcli.NameSpaceResults
    44  	*buildahcli.UserNSResults
    45  }
    46  
    47  func (engine *Engine) Build(opts *options.BuildOptions) (string, error) {
    48  	// The following block is to init buildah default options.
    49  	// And call migrateFlags2BuildahBuild to set flags based on sealer build options.
    50  	wrapper := &buildFlagsWrapper{
    51  		BudResults:        &buildahcli.BudResults{},
    52  		LayerResults:      &buildahcli.LayerResults{},
    53  		FromAndBudResults: &buildahcli.FromAndBudResults{},
    54  		NameSpaceResults:  &buildahcli.NameSpaceResults{},
    55  		UserNSResults:     &buildahcli.UserNSResults{},
    56  	}
    57  
    58  	flags := engine.Flags()
    59  	buildFlags := buildahcli.GetBudFlags(wrapper.BudResults)
    60  	buildFlags.StringVar(&wrapper.Runtime, "runtime", util.Runtime(), "`path` to an alternate runtime. Use BUILDAH_RUNTIME environment variable to override.")
    61  
    62  	layerFlags := buildahcli.GetLayerFlags(wrapper.LayerResults)
    63  	fromAndBudFlags, err := buildahcli.GetFromAndBudFlags(wrapper.FromAndBudResults, wrapper.UserNSResults, wrapper.NameSpaceResults)
    64  	if err != nil {
    65  		return "", fmt.Errorf("failed to setup From and Build flags: %v", err)
    66  	}
    67  
    68  	flags.AddFlagSet(&buildFlags)
    69  	flags.AddFlagSet(&layerFlags)
    70  	flags.AddFlagSet(&fromAndBudFlags)
    71  	flags.SetNormalizeFunc(buildahcli.AliasFlags)
    72  
    73  	err = engine.migrateFlags2Wrapper(opts, wrapper)
    74  	if err != nil {
    75  		return "", err
    76  	}
    77  
    78  	opt, kubeFiles, err := engine.wrapper2Options(opts, wrapper)
    79  	if err != nil {
    80  		return "", err
    81  	}
    82  
    83  	return engine.build(getContext(), kubeFiles, opt)
    84  }
    85  
    86  // this function aims to set buildah configuration based on sealer image engine flags.
    87  func (engine *Engine) migrateFlags2Wrapper(opts *options.BuildOptions, wrapper *buildFlagsWrapper) error {
    88  	flags := engine.Flags()
    89  	// imageengine cache related flags
    90  	// cache intermediate layers during build, it is enabled when len(opts.Platforms) <= 1 and "no-cache" is false
    91  	wrapper.Layers = len(opts.Platforms) <= 1 && !opts.NoCache
    92  	wrapper.NoCache = opts.NoCache
    93  	// tags. Like -t kubernetes:v1.16
    94  	wrapper.Tag = []string{opts.Tag}
    95  	// Hardcoded for network configuration.
    96  	// check parse.NamespaceOptions for detailed logic.
    97  	// this network setup for stage container, especially for RUN wget and so on.
    98  	// so I think we can set as host network.
    99  	err := flags.Set("network", "host")
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	// use tmp dockerfile as build file
   105  	wrapper.File = []string{opts.DockerFilePath}
   106  	wrapper.Pull = opts.PullPolicy
   107  	wrapper.Label = append(wrapper.Label, opts.Labels...)
   108  	wrapper.Annotation = append(wrapper.Annotation, opts.Annotations...)
   109  	return nil
   110  }
   111  
   112  func (engine *Engine) wrapper2Options(opts *options.BuildOptions, wrapper *buildFlagsWrapper) (define.BuildOptions, []string, error) {
   113  	output := ""
   114  	tags := wrapper.Tag
   115  	if len(tags) > 0 {
   116  		output = tags[0]
   117  		tags = tags[1:]
   118  	}
   119  	if engine.Flag("manifest").Changed {
   120  		for _, tag := range tags {
   121  			if tag == wrapper.Manifest {
   122  				return define.BuildOptions{}, []string{}, errors.New("the same name must not be specified for both '--tag' and '--manifest'")
   123  			}
   124  		}
   125  	}
   126  
   127  	args, err := parseArgs(opts.BuildArgs)
   128  	if err != nil {
   129  		return define.BuildOptions{}, nil, err
   130  	}
   131  
   132  	systemCxt := engine.SystemContext()
   133  
   134  	if err := auth.CheckAuthFile(systemCxt.AuthFilePath); err != nil {
   135  		return define.BuildOptions{}, []string{}, err
   136  	}
   137  	tempAuthFile, cleanTmpFile :=
   138  		buildahutil.MirrorToTempFileIfPathIsDescriptor(systemCxt.AuthFilePath)
   139  	if cleanTmpFile {
   140  		defer os.Remove(tempAuthFile)
   141  	}
   142  
   143  	// Allow for --pull, --pull=true, --pull=false, --pull=never, --pull=always
   144  	// --pull-always and --pull-never.  The --pull-never and --pull-always options
   145  	// will not be documented.
   146  	pullPolicy := define.PullIfMissing
   147  	if strings.EqualFold(strings.TrimSpace(wrapper.Pull), "true") {
   148  		pullPolicy = define.PullIfNewer
   149  	}
   150  	if wrapper.PullAlways || strings.EqualFold(strings.TrimSpace(wrapper.Pull), "always") {
   151  		pullPolicy = define.PullAlways
   152  	}
   153  	if wrapper.PullNever || strings.EqualFold(strings.TrimSpace(wrapper.Pull), "never") {
   154  		pullPolicy = define.PullNever
   155  	}
   156  	logrus.Debugf("Pull Policy for pull [%v]", pullPolicy)
   157  
   158  	format, err := getImageType(wrapper.Format)
   159  	if err != nil {
   160  		return define.BuildOptions{}, []string{}, err
   161  	}
   162  
   163  	layers := wrapper.Layers
   164  
   165  	contextDir := opts.ContextDir
   166  
   167  	// Nothing provided, we assume the current working directory as build
   168  	// context
   169  	if len(contextDir) == 0 {
   170  		contextDir, err = os.Getwd()
   171  		if err != nil {
   172  			return define.BuildOptions{}, []string{}, errors.Wrapf(err, "unable to choose current working directory as build context")
   173  		}
   174  	} else {
   175  		// It was local.  Use it as is.
   176  		contextDir, err = filepath.Abs(contextDir)
   177  		if err != nil {
   178  			return define.BuildOptions{}, []string{}, errors.Wrapf(err, "error determining path to directory")
   179  		}
   180  	}
   181  
   182  	kubefiles := getKubeFiles(wrapper.File)
   183  	if len(kubefiles) == 0 {
   184  		kubefile, err := DiscoverKubefile(contextDir)
   185  		if err != nil {
   186  			return define.BuildOptions{}, []string{}, err
   187  		}
   188  		kubefiles = append(kubefiles, kubefile)
   189  	}
   190  
   191  	contextDir, err = filepath.EvalSymlinks(contextDir)
   192  	if err != nil {
   193  		return define.BuildOptions{}, []string{}, errors.Wrapf(err, "error evaluating symlinks in build context path")
   194  	}
   195  
   196  	var stdin io.Reader
   197  	if wrapper.Stdin {
   198  		stdin = os.Stdin
   199  	}
   200  	var stdout, stderr, reporter = os.Stdout, os.Stderr, os.Stderr
   201  	if engine.Flag("logfile").Changed {
   202  		f, err := os.OpenFile(wrapper.Logfile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
   203  		if err != nil {
   204  			return define.BuildOptions{}, []string{}, errors.Errorf("error opening logfile %q: %v", wrapper.Logfile, err)
   205  		}
   206  		defer func() {
   207  			// this will incur GoSec warning
   208  			_ = f.Close()
   209  		}()
   210  		logrus.SetOutput(f)
   211  		stdout = f
   212  		stderr = f
   213  		reporter = f
   214  	}
   215  
   216  	isolation, err := defaultIsolationOption()
   217  	if err != nil {
   218  		return define.BuildOptions{}, []string{}, err
   219  	}
   220  
   221  	runtimeFlags := []string{}
   222  	for _, arg := range wrapper.RuntimeFlags {
   223  		runtimeFlags = append(runtimeFlags, "--"+arg)
   224  	}
   225  
   226  	commonOpts, err := parse.CommonBuildOptions(engine.Command)
   227  	if err != nil {
   228  		return define.BuildOptions{}, []string{}, err
   229  	}
   230  
   231  	namespaceOptions, networkPolicy := defaultNamespaceOptions()
   232  
   233  	usernsOption, idmappingOptions, err := parse.IDMappingOptions(engine.Command, isolation)
   234  	if err != nil {
   235  		return define.BuildOptions{}, []string{}, errors.Wrapf(err, "error parsing ID mapping options")
   236  	}
   237  	namespaceOptions.AddOrReplace(usernsOption...)
   238  
   239  	platforms, err := parsePlatformsFromOptions(opts.Platforms)
   240  	if err != nil {
   241  		return define.BuildOptions{}, nil, err
   242  	}
   243  
   244  	var excludes []string
   245  	if wrapper.IgnoreFile != "" {
   246  		if excludes, _, err = parse.ContainerIgnoreFile(contextDir, wrapper.IgnoreFile); err != nil {
   247  			return define.BuildOptions{}, []string{}, err
   248  		}
   249  	}
   250  
   251  	var timestamp *time.Time
   252  	if engine.Command.Flag("timestamp").Changed {
   253  		t := time.Unix(wrapper.Timestamp, 0).UTC()
   254  		timestamp = &t
   255  	}
   256  
   257  	compression := define.Gzip
   258  	if wrapper.DisableCompression {
   259  		compression = define.Uncompressed
   260  	}
   261  
   262  	options := define.BuildOptions{
   263  		AddCapabilities:         wrapper.CapAdd,
   264  		AdditionalTags:          tags,
   265  		AllPlatforms:            wrapper.AllPlatforms,
   266  		Annotations:             wrapper.Annotation,
   267  		Architecture:            systemCxt.ArchitectureChoice,
   268  		Args:                    args,
   269  		BlobDirectory:           wrapper.BlobCache,
   270  		CNIConfigDir:            wrapper.CNIConfigDir,
   271  		CNIPluginPath:           wrapper.CNIPlugInPath,
   272  		CommonBuildOpts:         commonOpts,
   273  		Compression:             compression,
   274  		ConfigureNetwork:        networkPolicy,
   275  		ContextDirectory:        contextDir,
   276  		DefaultMountsFilePath:   "",
   277  		Devices:                 wrapper.Devices,
   278  		DropCapabilities:        wrapper.CapDrop,
   279  		Err:                     stderr,
   280  		ForceRmIntermediateCtrs: wrapper.ForceRm,
   281  		From:                    wrapper.From,
   282  		IDMappingOptions:        idmappingOptions,
   283  		IIDFile:                 wrapper.Iidfile,
   284  		In:                      stdin,
   285  		Isolation:               isolation,
   286  		IgnoreFile:              wrapper.IgnoreFile,
   287  		Labels:                  wrapper.Label,
   288  		Layers:                  layers,
   289  		LogRusage:               wrapper.LogRusage,
   290  		Manifest:                wrapper.Manifest,
   291  		MaxPullPushRetries:      maxPullPushRetries,
   292  		NamespaceOptions:        namespaceOptions,
   293  		NoCache:                 wrapper.NoCache,
   294  		OS:                      systemCxt.OSChoice,
   295  		Out:                     stdout,
   296  		Output:                  output,
   297  		OutputFormat:            format,
   298  		PullPolicy:              pullPolicy,
   299  		PullPushRetryDelay:      pullPushRetryDelay,
   300  		Quiet:                   wrapper.Quiet,
   301  		RemoveIntermediateCtrs:  wrapper.Rm,
   302  		ReportWriter:            reporter,
   303  		Runtime:                 wrapper.Runtime,
   304  		RuntimeArgs:             runtimeFlags,
   305  		RusageLogFile:           wrapper.RusageLogFile,
   306  		SignBy:                  wrapper.SignBy,
   307  		SignaturePolicyPath:     wrapper.SignaturePolicy,
   308  		Squash:                  wrapper.Squash,
   309  		SystemContext:           systemCxt,
   310  		Target:                  wrapper.Target,
   311  		TransientMounts:         wrapper.Volumes,
   312  		Jobs:                    &wrapper.Jobs,
   313  		Excludes:                excludes,
   314  		Timestamp:               timestamp,
   315  		Platforms:               platforms,
   316  		UnsetEnvs:               wrapper.UnsetEnvs,
   317  	}
   318  
   319  	if wrapper.Quiet {
   320  		options.ReportWriter = io.Discard
   321  	}
   322  
   323  	return options, kubefiles, nil
   324  }
   325  
   326  func (engine *Engine) build(cxt context.Context, kubefiles []string, options define.BuildOptions) (id string, err error) {
   327  	id, ref, err := imagebuildah.BuildDockerfiles(cxt, engine.ImageStore(), options, kubefiles...)
   328  	if err == nil && options.Manifest != "" {
   329  		logrus.Debugf("manifest list id = %q, ref = %q", id, ref.String())
   330  	}
   331  	if err != nil {
   332  		return "", fmt.Errorf("failed to build image %v: %v", options.AdditionalTags, err)
   333  	}
   334  
   335  	return id, nil
   336  }
   337  
   338  func parseArgs(buildArgs []string) (map[string]string, error) {
   339  	res := map[string]string{}
   340  	for _, arg := range buildArgs {
   341  		kvs := strings.Split(arg, "=")
   342  		if len(kvs) < 2 {
   343  			return nil, errors.New("build args should be key=value")
   344  		}
   345  
   346  		res[kvs[0]] = kvs[1]
   347  	}
   348  
   349  	return res, nil
   350  }
   351  
   352  func getKubeFiles(files []string) []string {
   353  	var kubefiles []string
   354  	for _, f := range files {
   355  		if f == "-" {
   356  			kubefiles = append(kubefiles, "/dev/stdin")
   357  		} else {
   358  			kubefiles = append(kubefiles, f)
   359  		}
   360  	}
   361  	return kubefiles
   362  }
   363  
   364  func parsePlatformsFromOptions(platformSpecs []string) (platforms []struct{ OS, Arch, Variant string }, err error) {
   365  	var _os, arch, variant string
   366  
   367  	for _, pf := range platformSpecs {
   368  		if _os, arch, variant, err = parse.Platform(pf); err != nil {
   369  			return nil, fmt.Errorf("unable to parse platform %q: %w", pf, err)
   370  		}
   371  		platforms = append(platforms, struct{ OS, Arch, Variant string }{_os, arch, variant})
   372  	}
   373  
   374  	return platforms, nil
   375  }