mirror of
https://github.com/containers/buildah.git
synced 2025-04-18 07:04:05 +03:00
360 lines
11 KiB
Go
360 lines
11 KiB
Go
package buildah
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"math/rand"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/containers/buildah/define"
|
|
"github.com/containers/common/libimage"
|
|
"github.com/containers/common/pkg/config"
|
|
"github.com/containers/image/v5/image"
|
|
"github.com/containers/image/v5/manifest"
|
|
"github.com/containers/image/v5/pkg/shortnames"
|
|
"github.com/containers/image/v5/transports"
|
|
"github.com/containers/image/v5/types"
|
|
"github.com/containers/storage"
|
|
"github.com/containers/storage/pkg/stringid"
|
|
digest "github.com/opencontainers/go-digest"
|
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/openshift/imagebuilder"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
// BaseImageFakeName is the "name" of a source image which we interpret
|
|
// as "no image".
|
|
BaseImageFakeName = imagebuilder.NoBaseImageSpecifier
|
|
)
|
|
|
|
func getImageName(name string, img *storage.Image) string {
|
|
imageName := name
|
|
if len(img.Names) > 0 {
|
|
imageName = img.Names[0]
|
|
// When the image used by the container is a tagged image
|
|
// the container name might be set to the original image instead of
|
|
// the image given in the "from" command line.
|
|
// This loop is supposed to fix this.
|
|
for _, n := range img.Names {
|
|
if strings.Contains(n, name) {
|
|
imageName = n
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return imageName
|
|
}
|
|
|
|
func imageNamePrefix(imageName string) string {
|
|
prefix := imageName
|
|
if d, err := digest.Parse(imageName); err == nil {
|
|
prefix = d.Encoded()
|
|
if len(prefix) > 12 {
|
|
prefix = prefix[:12]
|
|
}
|
|
}
|
|
if stringid.ValidateID(prefix) == nil {
|
|
prefix = stringid.TruncateID(prefix)
|
|
}
|
|
s := strings.Split(prefix, ":")
|
|
if len(s) > 0 {
|
|
prefix = s[0]
|
|
}
|
|
s = strings.Split(prefix, "/")
|
|
if len(s) > 0 {
|
|
prefix = s[len(s)-1]
|
|
}
|
|
s = strings.Split(prefix, "@")
|
|
if len(s) > 0 {
|
|
prefix = s[0]
|
|
}
|
|
return prefix
|
|
}
|
|
|
|
func newContainerIDMappingOptions(idmapOptions *define.IDMappingOptions) storage.IDMappingOptions {
|
|
var options storage.IDMappingOptions
|
|
if idmapOptions != nil {
|
|
if idmapOptions.AutoUserNs {
|
|
options.AutoUserNs = true
|
|
options.AutoUserNsOpts = idmapOptions.AutoUserNsOpts
|
|
} else {
|
|
options.HostUIDMapping = idmapOptions.HostUIDMapping
|
|
options.HostGIDMapping = idmapOptions.HostGIDMapping
|
|
uidmap, gidmap := convertRuntimeIDMaps(idmapOptions.UIDMap, idmapOptions.GIDMap)
|
|
if len(uidmap) > 0 && len(gidmap) > 0 {
|
|
options.UIDMap = uidmap
|
|
options.GIDMap = gidmap
|
|
} else {
|
|
options.HostUIDMapping = true
|
|
options.HostGIDMapping = true
|
|
}
|
|
}
|
|
}
|
|
return options
|
|
}
|
|
|
|
func containerNameExist(name string, containers []storage.Container) bool {
|
|
for _, container := range containers {
|
|
if slices.Contains(container.Names, name) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func findUnusedContainer(name string, containers []storage.Container) string {
|
|
suffix := 1
|
|
tmpName := name
|
|
for containerNameExist(tmpName, containers) {
|
|
tmpName = fmt.Sprintf("%s-%d", name, suffix)
|
|
suffix++
|
|
}
|
|
return tmpName
|
|
}
|
|
|
|
func newBuilder(ctx context.Context, store storage.Store, options BuilderOptions) (*Builder, error) {
|
|
var (
|
|
ref types.ImageReference
|
|
img *storage.Image
|
|
err error
|
|
)
|
|
|
|
if options.FromImage == BaseImageFakeName {
|
|
options.FromImage = ""
|
|
}
|
|
|
|
if options.NetworkInterface == nil {
|
|
// create the network interface
|
|
// Note: It is important to do this before we pull any images/create containers.
|
|
// The default backend detection logic needs an empty store to correctly detect
|
|
// that we can use netavark, if the store was not empty it will use CNI to not break existing installs.
|
|
options.NetworkInterface, err = getNetworkInterface(store, options.CNIConfigDir, options.CNIPluginPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
systemContext := getSystemContext(store, options.SystemContext, options.SignaturePolicyPath)
|
|
|
|
if options.FromImage != "" && options.FromImage != BaseImageFakeName {
|
|
imageRuntime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pullPolicy, err := config.ParsePullPolicy(options.PullPolicy.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Note: options.Format does *not* relate to the image we're
|
|
// about to pull (see tests/digests.bats). So we're not
|
|
// forcing a MIMEType in the pullOptions below.
|
|
pullOptions := libimage.PullOptions{}
|
|
pullOptions.RetryDelay = &options.PullRetryDelay
|
|
pullOptions.OciDecryptConfig = options.OciDecryptConfig
|
|
pullOptions.SignaturePolicyPath = options.SignaturePolicyPath
|
|
pullOptions.Writer = options.ReportWriter
|
|
pullOptions.DestinationLookupReferenceFunc = cacheLookupReferenceFunc(options.BlobDirectory, types.PreserveOriginal)
|
|
|
|
maxRetries := uint(options.MaxPullRetries)
|
|
pullOptions.MaxRetries = &maxRetries
|
|
|
|
pulledImages, err := imageRuntime.Pull(ctx, options.FromImage, pullPolicy, &pullOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(pulledImages) > 0 {
|
|
img = pulledImages[0].StorageImage()
|
|
ref, err = pulledImages[0].StorageReference()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
imageSpec := options.FromImage
|
|
imageID := ""
|
|
imageDigest := ""
|
|
topLayer := ""
|
|
if img != nil {
|
|
imageSpec = getImageName(imageNamePrefix(imageSpec), img)
|
|
imageID = img.ID
|
|
topLayer = img.TopLayer
|
|
}
|
|
var src types.Image
|
|
if ref != nil {
|
|
srcSrc, err := ref.NewImageSource(ctx, systemContext)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("instantiating image for %q: %w", transports.ImageName(ref), err)
|
|
}
|
|
defer srcSrc.Close()
|
|
unparsedTop := image.UnparsedInstance(srcSrc, nil)
|
|
manifestBytes, manifestType, err := unparsedTop.Manifest(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading image manifest for %q: %w", transports.ImageName(ref), err)
|
|
}
|
|
if manifestDigest, err := manifest.Digest(manifestBytes); err == nil {
|
|
imageDigest = manifestDigest.String()
|
|
}
|
|
var instanceDigest *digest.Digest
|
|
unparsedInstance := unparsedTop // for instanceDigest
|
|
if manifest.MIMETypeIsMultiImage(manifestType) {
|
|
list, err := manifest.ListFromBlob(manifestBytes, manifestType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing image manifest for %q as list: %w", transports.ImageName(ref), err)
|
|
}
|
|
instance, err := list.ChooseInstance(systemContext)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("finding an appropriate image in manifest list %q: %w", transports.ImageName(ref), err)
|
|
}
|
|
instanceDigest = &instance
|
|
unparsedInstance = image.UnparsedInstance(srcSrc, instanceDigest)
|
|
}
|
|
src, err = image.FromUnparsedImage(ctx, systemContext, unparsedInstance)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("instantiating image for %q instance %q: %w", transports.ImageName(ref), instanceDigest, err)
|
|
}
|
|
}
|
|
|
|
name := "working-container"
|
|
if options.ContainerSuffix != "" {
|
|
name = options.ContainerSuffix
|
|
}
|
|
if options.Container != "" {
|
|
name = options.Container
|
|
} else {
|
|
if imageSpec != "" {
|
|
name = imageNamePrefix(imageSpec) + "-" + name
|
|
}
|
|
}
|
|
var container *storage.Container
|
|
tmpName := name
|
|
if options.Container == "" {
|
|
containers, err := store.Containers()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to check for container names: %w", err)
|
|
}
|
|
tmpName = findUnusedContainer(tmpName, containers)
|
|
}
|
|
|
|
suffixDigitsModulo := 100
|
|
for {
|
|
var flags map[string]any
|
|
// check if we have predefined ProcessLabel and MountLabel
|
|
// this could be true if this is another stage in a build
|
|
if options.ProcessLabel != "" && options.MountLabel != "" {
|
|
flags = map[string]any{
|
|
"ProcessLabel": options.ProcessLabel,
|
|
"MountLabel": options.MountLabel,
|
|
}
|
|
}
|
|
coptions := storage.ContainerOptions{
|
|
LabelOpts: options.CommonBuildOpts.LabelOpts,
|
|
IDMappingOptions: newContainerIDMappingOptions(options.IDMappingOptions),
|
|
Flags: flags,
|
|
Volatile: true,
|
|
}
|
|
container, err = store.CreateContainer("", []string{tmpName}, imageID, "", "", &coptions)
|
|
if err == nil {
|
|
name = tmpName
|
|
break
|
|
}
|
|
if !errors.Is(err, storage.ErrDuplicateName) || options.Container != "" {
|
|
return nil, fmt.Errorf("creating container: %w", err)
|
|
}
|
|
tmpName = fmt.Sprintf("%s-%d", name, rand.Int()%suffixDigitsModulo)
|
|
if suffixDigitsModulo < 1_000_000_000 {
|
|
suffixDigitsModulo *= 10
|
|
}
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
if err2 := store.DeleteContainer(container.ID); err2 != nil {
|
|
logrus.Errorf("error deleting container %q: %v", container.ID, err2)
|
|
}
|
|
}
|
|
}()
|
|
|
|
uidmap, gidmap := convertStorageIDMaps(container.UIDMap, container.GIDMap)
|
|
|
|
defaultNamespaceOptions, err := DefaultNamespaceOptions()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
namespaceOptions := defaultNamespaceOptions
|
|
namespaceOptions.AddOrReplace(options.NamespaceOptions...)
|
|
|
|
builder := &Builder{
|
|
store: store,
|
|
Type: containerType,
|
|
FromImage: imageSpec,
|
|
FromImageID: imageID,
|
|
FromImageDigest: imageDigest,
|
|
GroupAdd: options.GroupAdd,
|
|
Container: name,
|
|
ContainerID: container.ID,
|
|
ImageAnnotations: map[string]string{},
|
|
ImageCreatedBy: "",
|
|
ProcessLabel: container.ProcessLabel(),
|
|
MountLabel: container.MountLabel(),
|
|
DefaultMountsFilePath: options.DefaultMountsFilePath,
|
|
Isolation: options.Isolation,
|
|
NamespaceOptions: namespaceOptions,
|
|
ConfigureNetwork: options.ConfigureNetwork,
|
|
CNIPluginPath: options.CNIPluginPath,
|
|
CNIConfigDir: options.CNIConfigDir,
|
|
IDMappingOptions: define.IDMappingOptions{
|
|
HostUIDMapping: len(uidmap) == 0,
|
|
HostGIDMapping: len(uidmap) == 0,
|
|
UIDMap: uidmap,
|
|
GIDMap: gidmap,
|
|
},
|
|
Capabilities: slices.Clone(options.Capabilities),
|
|
CommonBuildOpts: options.CommonBuildOpts,
|
|
TopLayer: topLayer,
|
|
Args: maps.Clone(options.Args),
|
|
Format: options.Format,
|
|
TempVolumes: map[string]bool{},
|
|
Devices: options.Devices,
|
|
DeviceSpecs: options.DeviceSpecs,
|
|
Logger: options.Logger,
|
|
NetworkInterface: options.NetworkInterface,
|
|
CDIConfigDir: options.CDIConfigDir,
|
|
}
|
|
|
|
if options.Mount {
|
|
_, err = builder.Mount(container.MountLabel())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mounting build container %q: %w", builder.ContainerID, err)
|
|
}
|
|
}
|
|
|
|
if err := builder.initConfig(ctx, systemContext, src, &options); err != nil {
|
|
return nil, fmt.Errorf("preparing image configuration: %w", err)
|
|
}
|
|
|
|
if !options.PreserveBaseImageAnns {
|
|
builder.SetAnnotation(v1.AnnotationBaseImageDigest, imageDigest)
|
|
if !shortnames.IsShortName(imageSpec) {
|
|
// If the base image was specified as a fully-qualified
|
|
// image name, let's set it.
|
|
builder.SetAnnotation(v1.AnnotationBaseImageName, imageSpec)
|
|
} else {
|
|
builder.UnsetAnnotation(v1.AnnotationBaseImageName)
|
|
}
|
|
}
|
|
|
|
err = builder.Save()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("saving builder state for container %q: %w", builder.ContainerID, err)
|
|
}
|
|
|
|
return builder, nil
|
|
}
|