mirror of
https://github.com/linuxkit/linuxkit.git
synced 2025-07-19 09:16:29 +00:00
add support for input-tar
Signed-off-by: Avi Deitcher <avi@deitcher.net>
This commit is contained in:
parent
a610332100
commit
632b4065d4
@ -55,6 +55,7 @@ func buildCmd() *cobra.Command {
|
|||||||
outputTypes = moby.OutputTypes()
|
outputTypes = moby.OutputTypes()
|
||||||
noSbom bool
|
noSbom bool
|
||||||
sbomOutputFilename string
|
sbomOutputFilename string
|
||||||
|
inputTar string
|
||||||
sbomCurrentTime bool
|
sbomCurrentTime bool
|
||||||
dryRun bool
|
dryRun bool
|
||||||
)
|
)
|
||||||
@ -94,7 +95,7 @@ The generated image can be in one of multiple formats which can be run on variou
|
|||||||
if len(buildFormats) > 1 {
|
if len(buildFormats) > 1 {
|
||||||
for _, o := range buildFormats {
|
for _, o := range buildFormats {
|
||||||
if moby.Streamable(o) {
|
if moby.Streamable(o) {
|
||||||
return fmt.Errorf("Format type %s must be the only format specified", o)
|
return fmt.Errorf("format type %s must be the only format specified", o)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,23 +110,27 @@ The generated image can be in one of multiple formats which can be run on variou
|
|||||||
} else {
|
} else {
|
||||||
err := moby.ValidateFormats(buildFormats, cacheDir.String())
|
err := moby.ValidateFormats(buildFormats, cacheDir.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error parsing formats: %v", err)
|
return fmt.Errorf("error parsing formats: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if inputTar != "" && pull {
|
||||||
|
return fmt.Errorf("cannot use --input-tar and --pull together")
|
||||||
|
}
|
||||||
|
|
||||||
var outfile *os.File
|
var outfile *os.File
|
||||||
if outputFile != "" {
|
if outputFile != "" {
|
||||||
if len(buildFormats) > 1 {
|
if len(buildFormats) > 1 {
|
||||||
return fmt.Errorf("The -output option can only be specified when generating a single output format")
|
return fmt.Errorf("the -output option can only be specified when generating a single output format")
|
||||||
}
|
}
|
||||||
if name != "" {
|
if name != "" {
|
||||||
return fmt.Errorf("The -output option cannot be specified with -name")
|
return fmt.Errorf("the -output option cannot be specified with -name")
|
||||||
}
|
}
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
return fmt.Errorf("The -output option cannot be specified with -dir")
|
return fmt.Errorf("the -output option cannot be specified with -dir")
|
||||||
}
|
}
|
||||||
if !moby.Streamable(buildFormats[0]) {
|
if !moby.Streamable(buildFormats[0]) {
|
||||||
return fmt.Errorf("The -output option cannot be specified for build type %s as it cannot be streamed", buildFormats[0])
|
return fmt.Errorf("the -output option cannot be specified for build type %s as it cannot be streamed", buildFormats[0])
|
||||||
}
|
}
|
||||||
if outputFile == "-" {
|
if outputFile == "-" {
|
||||||
outfile = os.Stdout
|
outfile = os.Stdout
|
||||||
@ -133,7 +138,7 @@ The generated image can be in one of multiple formats which can be run on variou
|
|||||||
var err error
|
var err error
|
||||||
outfile, err = os.Create(outputFile)
|
outfile, err = os.Create(outputFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Cannot open output file: %v", err)
|
log.Fatalf("cannot open output file: %v", err)
|
||||||
}
|
}
|
||||||
defer outfile.Close()
|
defer outfile.Close()
|
||||||
}
|
}
|
||||||
@ -141,7 +146,7 @@ The generated image can be in one of multiple formats which can be run on variou
|
|||||||
|
|
||||||
size, err := getDiskSizeMB(sizeString)
|
size, err := getDiskSizeMB(sizeString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Unable to parse disk size: %v", err)
|
log.Fatalf("unable to parse disk size: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -154,25 +159,25 @@ The generated image can be in one of multiple formats which can be run on variou
|
|||||||
var err error
|
var err error
|
||||||
config, err = io.ReadAll(os.Stdin)
|
config, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Cannot read stdin: %v", err)
|
return fmt.Errorf("cannot read stdin: %v", err)
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
|
} else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
response, err := http.Get(arg)
|
response, err := http.Get(arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Cannot fetch remote yaml file: %v", err)
|
return fmt.Errorf("cannot fetch remote yaml file: %v", err)
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
_, err = io.Copy(buffer, response.Body)
|
_, err = io.Copy(buffer, response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error reading http body: %v", err)
|
return fmt.Errorf("error reading http body: %v", err)
|
||||||
}
|
}
|
||||||
config = buffer.Bytes()
|
config = buffer.Bytes()
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
config, err = os.ReadFile(conf)
|
config, err = os.ReadFile(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Cannot open config file: %v", err)
|
return fmt.Errorf("cannot open config file: %v", err)
|
||||||
}
|
}
|
||||||
// templates are only supported for local files
|
// templates are only supported for local files
|
||||||
templatesSupported = true
|
templatesSupported = true
|
||||||
@ -183,18 +188,18 @@ The generated image can be in one of multiple formats which can be run on variou
|
|||||||
}
|
}
|
||||||
c, err := moby.NewConfig(config, pkgFinder)
|
c, err := moby.NewConfig(config, pkgFinder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Invalid config: %v", err)
|
return fmt.Errorf("invalid config: %v", err)
|
||||||
}
|
}
|
||||||
m, err = moby.AppendConfig(m, c)
|
m, err = moby.AppendConfig(m, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Cannot append config files: %v", err)
|
return fmt.Errorf("cannot append config files: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dryRun {
|
if dryRun {
|
||||||
yml, err := yaml.Marshal(m)
|
yml, err := yaml.Marshal(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error generating YAML: %v", err)
|
return fmt.Errorf("error generating YAML: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Println(string(yml))
|
fmt.Println(string(yml))
|
||||||
return nil
|
return nil
|
||||||
@ -206,7 +211,7 @@ The generated image can be in one of multiple formats which can be run on variou
|
|||||||
w = outfile
|
w = outfile
|
||||||
} else {
|
} else {
|
||||||
if tf, err = os.CreateTemp("", ""); err != nil {
|
if tf, err = os.CreateTemp("", ""); err != nil {
|
||||||
log.Fatalf("Error creating tempfile: %v", err)
|
log.Fatalf("error creating tempfile: %v", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(tf.Name())
|
defer os.Remove(tf.Name())
|
||||||
w = tf
|
w = tf
|
||||||
@ -225,7 +230,7 @@ The generated image can be in one of multiple formats which can be run on variou
|
|||||||
return fmt.Errorf("error creating sbom generator: %v", err)
|
return fmt.Errorf("error creating sbom generator: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = moby.Build(m, w, moby.BuildOpts{Pull: pull, BuilderType: tp, DecompressKernel: decompressKernel, CacheDir: cacheDir.String(), DockerCache: docker, Arch: arch, SbomGenerator: sbomGenerator})
|
err = moby.Build(m, w, moby.BuildOpts{Pull: pull, BuilderType: tp, DecompressKernel: decompressKernel, CacheDir: cacheDir.String(), DockerCache: docker, Arch: arch, SbomGenerator: sbomGenerator, InputTar: inputTar})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%v", err)
|
return fmt.Errorf("%v", err)
|
||||||
}
|
}
|
||||||
@ -233,13 +238,13 @@ The generated image can be in one of multiple formats which can be run on variou
|
|||||||
if outfile == nil {
|
if outfile == nil {
|
||||||
image := tf.Name()
|
image := tf.Name()
|
||||||
if err := tf.Close(); err != nil {
|
if err := tf.Close(); err != nil {
|
||||||
return fmt.Errorf("Error closing tempfile: %v", err)
|
return fmt.Errorf("error closing tempfile: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Create outputs:")
|
log.Infof("Create outputs:")
|
||||||
err = moby.Formats(filepath.Join(dir, name), image, buildFormats, size, arch, cacheDir.String())
|
err = moby.Formats(filepath.Join(dir, name), image, buildFormats, size, arch, cacheDir.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error writing outputs: %v", err)
|
return fmt.Errorf("error writing outputs: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -255,6 +260,7 @@ The generated image can be in one of multiple formats which can be run on variou
|
|||||||
cmd.Flags().BoolVar(&decompressKernel, "decompress-kernel", false, "Decompress the Linux kernel (default false)")
|
cmd.Flags().BoolVar(&decompressKernel, "decompress-kernel", false, "Decompress the Linux kernel (default false)")
|
||||||
cmd.Flags().StringVar(&arch, "arch", runtime.GOARCH, "target architecture for which to build")
|
cmd.Flags().StringVar(&arch, "arch", runtime.GOARCH, "target architecture for which to build")
|
||||||
cmd.Flags().VarP(&buildFormats, "format", "f", "Formats to create [ "+strings.Join(outputTypes, " ")+" ]")
|
cmd.Flags().VarP(&buildFormats, "format", "f", "Formats to create [ "+strings.Join(outputTypes, " ")+" ]")
|
||||||
|
cmd.Flags().StringVar(&inputTar, "input-tar", "", "path to tar from previous linuxkit build to use as input; if provided, will take files from images from this tar, using OCI images only to replace or update files. Always copies to a temporary working directory to avoid overwriting. Only works if input-tar file has the linuxkit.yaml used to build it in the exact same location. Incompatible with --pull")
|
||||||
cacheDir = flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir}
|
cacheDir = flagOverEnvVarOverDefaultString{def: defaultLinuxkitCache(), envVar: envVarCacheDir}
|
||||||
cmd.Flags().Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir))
|
cmd.Flags().Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir))
|
||||||
cmd.Flags().BoolVar(&noSbom, "no-sbom", false, "suppress consolidation of sboms on input container images to a single sbom and saving in the output filesystem")
|
cmd.Flags().BoolVar(&noSbom, "no-sbom", false, "suppress consolidation of sboms on input container images to a single sbom and saving in the output filesystem")
|
||||||
|
@ -83,7 +83,7 @@ func OutputTypes() []string {
|
|||||||
return ts
|
return ts
|
||||||
}
|
}
|
||||||
|
|
||||||
func outputImage(image *Image, section string, prefix string, m Moby, idMap map[string]uint32, dupMap map[string]string, iw *tar.Writer, opts BuildOpts) error {
|
func outputImage(image *Image, section string, index int, prefix string, m Moby, idMap map[string]uint32, dupMap map[string]string, iw *tar.Writer, opts BuildOpts) error {
|
||||||
log.Infof(" Create OCI config for %s", image.Image)
|
log.Infof(" Create OCI config for %s", image.Image)
|
||||||
imageName := util.ReferenceExpand(image.Image)
|
imageName := util.ReferenceExpand(image.Image)
|
||||||
ref, err := reference.Parse(imageName)
|
ref, err := reference.Parse(imageName)
|
||||||
@ -108,14 +108,15 @@ func outputImage(image *Image, section string, prefix string, m Moby, idMap map[
|
|||||||
}
|
}
|
||||||
path := path.Join("containers", section, prefix+image.Name)
|
path := path.Join("containers", section, prefix+image.Name)
|
||||||
readonly := oci.Root.Readonly
|
readonly := oci.Root.Readonly
|
||||||
err = ImageBundle(path, section, image.ref, config, runtime, iw, readonly, dupMap, opts)
|
err = ImageBundle(path, fmt.Sprintf("%s[%d]", section, index), image.ref, config, runtime, iw, readonly, dupMap, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to extract root filesystem for %s: %v", image.Image, err)
|
return fmt.Errorf("failed to extract root filesystem for %s: %v", image.Image, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build performs the actual build process
|
// Build performs the actual build process. The output is the filesystem
|
||||||
|
// in a tar stream written to w.
|
||||||
func Build(m Moby, w io.Writer, opts BuildOpts) error {
|
func Build(m Moby, w io.Writer, opts BuildOpts) error {
|
||||||
if MobyDir == "" {
|
if MobyDir == "" {
|
||||||
MobyDir = defaultMobyConfigDir()
|
MobyDir = defaultMobyConfigDir()
|
||||||
@ -126,6 +127,68 @@ func Build(m Moby, w io.Writer, opts BuildOpts) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// find the Moby config file from the existing tar
|
||||||
|
var metadataLocation string
|
||||||
|
if m.Files != nil {
|
||||||
|
for _, f := range m.Files {
|
||||||
|
if f.Metadata == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metadataLocation = strings.TrimPrefix(f.Path, "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
oldConfig *Moby
|
||||||
|
tmpfile *os.File
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if metadataLocation != "" && opts.InputTar != "" {
|
||||||
|
// copy the file over, in case it ends up being the same output
|
||||||
|
tmpfile, err = os.CreateTemp("", "linuxkit-input.tar")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temporary file: %w", err)
|
||||||
|
}
|
||||||
|
defer tmpfile.Close()
|
||||||
|
in, err := os.Open(opts.InputTar)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open input tar: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(tmpfile, in); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy input tar: %w", err)
|
||||||
|
}
|
||||||
|
if err := in.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close input file: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := tmpfile.Seek(0, 0); err != nil {
|
||||||
|
return fmt.Errorf("failed to seek to beginning of tmpfile: %w", err)
|
||||||
|
}
|
||||||
|
// for efficiency, get the trimmed metadata path in advance
|
||||||
|
tmpTar := tar.NewReader(tmpfile)
|
||||||
|
// read the tar until we find the metadata file
|
||||||
|
for {
|
||||||
|
hdr, err := tmpTar.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read input tar: %w", err)
|
||||||
|
}
|
||||||
|
if strings.TrimPrefix(hdr.Name, "/") == metadataLocation {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if _, err := buf.ReadFrom(tmpTar); err != nil {
|
||||||
|
return fmt.Errorf("failed to read metadata file from input tar: %w", err)
|
||||||
|
}
|
||||||
|
config, err := NewConfig(buf.Bytes(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid config in existing tar file: %v", err)
|
||||||
|
}
|
||||||
|
oldConfig = &config
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do we have an inTar
|
||||||
iw := tar.NewWriter(w)
|
iw := tar.NewWriter(w)
|
||||||
|
|
||||||
// add additions
|
// add additions
|
||||||
@ -151,16 +214,24 @@ func Build(m Moby, w io.Writer, opts BuildOpts) error {
|
|||||||
dupMap := map[string]string{}
|
dupMap := map[string]string{}
|
||||||
|
|
||||||
if m.Kernel.ref != nil {
|
if m.Kernel.ref != nil {
|
||||||
// get kernel and initrd tarball and ucode cpio archive from container
|
// first check if the existing one had it
|
||||||
log.Infof("Extract kernel image: %s", m.Kernel.ref)
|
//if config != nil && len(oldConfig.initRefs) > index+1 && oldConfig.initRefs[index].String() == image {
|
||||||
kf := newKernelFilter(m.Kernel.ref, iw, m.Kernel.Cmdline, m.Kernel.Binary, m.Kernel.Tar, m.Kernel.UCode, opts.DecompressKernel)
|
if oldConfig != nil && oldConfig.Kernel.ref != nil && oldConfig.Kernel.ref.String() == m.Kernel.ref.String() {
|
||||||
err := ImageTar("kernel", m.Kernel.ref, "", kf, "", opts)
|
if err := extractPackageFilesFromTar(tmpfile, iw, m.Kernel.ref.String(), "kernel"); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return fmt.Errorf("failed to extract kernel image and tarball: %v", err)
|
}
|
||||||
}
|
} else {
|
||||||
err = kf.Close()
|
// get kernel and initrd tarball and ucode cpio archive from container
|
||||||
if err != nil {
|
log.Infof("Extract kernel image: %s", m.Kernel.ref)
|
||||||
return fmt.Errorf("close error: %v", err)
|
kf := newKernelFilter(m.Kernel.ref, iw, m.Kernel.Cmdline, m.Kernel.Binary, m.Kernel.Tar, m.Kernel.UCode, opts.DecompressKernel)
|
||||||
|
err := ImageTar("kernel", m.Kernel.ref, "", kf, "", opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to extract kernel image and tarball: %v", err)
|
||||||
|
}
|
||||||
|
err = kf.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("close error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,11 +240,17 @@ func Build(m Moby, w io.Writer, opts BuildOpts) error {
|
|||||||
log.Infof("Add init containers:")
|
log.Infof("Add init containers:")
|
||||||
}
|
}
|
||||||
apkTar := newAPKTarWriter(iw, "init")
|
apkTar := newAPKTarWriter(iw, "init")
|
||||||
for _, ii := range m.initRefs {
|
for i, ii := range m.initRefs {
|
||||||
log.Infof("Process init image: %s", ii)
|
if oldConfig != nil && len(oldConfig.initRefs) > i && oldConfig.initRefs[i].String() == ii.String() {
|
||||||
err := ImageTar("init", ii, "", apkTar, resolvconfSymlink, opts)
|
if err := extractPackageFilesFromTar(tmpfile, apkTar, ii.String(), fmt.Sprintf("init[%d]", i)); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return fmt.Errorf("failed to build init tarball from %s: %v", ii, err)
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("Process init image: %s", ii)
|
||||||
|
err := ImageTar(fmt.Sprintf("init[%d]", i), ii, "", apkTar, resolvconfSymlink, opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build init tarball from %s: %v", ii, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := apkTar.WriteAPKDB(); err != nil {
|
if err := apkTar.WriteAPKDB(); err != nil {
|
||||||
@ -184,9 +261,15 @@ func Build(m Moby, w io.Writer, opts BuildOpts) error {
|
|||||||
log.Infof("Add onboot containers:")
|
log.Infof("Add onboot containers:")
|
||||||
}
|
}
|
||||||
for i, image := range m.Onboot {
|
for i, image := range m.Onboot {
|
||||||
so := fmt.Sprintf("%03d", i)
|
if oldConfig != nil && len(oldConfig.Onboot) > i && oldConfig.Onboot[i].Equal(image) {
|
||||||
if err := outputImage(image, "onboot", so+"-", m, idMap, dupMap, iw, opts); err != nil {
|
if err := extractPackageFilesFromTar(tmpfile, iw, image.Image, fmt.Sprintf("onboot[%d]", i)); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
so := fmt.Sprintf("%03d", i)
|
||||||
|
if err := outputImage(image, "onboot", i, so+"-", m, idMap, dupMap, iw, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,24 +277,35 @@ func Build(m Moby, w io.Writer, opts BuildOpts) error {
|
|||||||
log.Infof("Add onshutdown containers:")
|
log.Infof("Add onshutdown containers:")
|
||||||
}
|
}
|
||||||
for i, image := range m.Onshutdown {
|
for i, image := range m.Onshutdown {
|
||||||
so := fmt.Sprintf("%03d", i)
|
if oldConfig != nil && len(oldConfig.Onshutdown) > i && oldConfig.Onshutdown[i].Equal(image) {
|
||||||
if err := outputImage(image, "onshutdown", so+"-", m, idMap, dupMap, iw, opts); err != nil {
|
if err := extractPackageFilesFromTar(tmpfile, iw, image.Image, fmt.Sprintf("onshutdown[%d]", i)); err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
so := fmt.Sprintf("%03d", i)
|
||||||
|
if err := outputImage(image, "onshutdown", i, so+"-", m, idMap, dupMap, iw, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m.Services) != 0 {
|
if len(m.Services) != 0 {
|
||||||
log.Infof("Add service containers:")
|
log.Infof("Add service containers:")
|
||||||
}
|
}
|
||||||
for _, image := range m.Services {
|
for i, image := range m.Services {
|
||||||
if err := outputImage(image, "services", "", m, idMap, dupMap, iw, opts); err != nil {
|
if oldConfig != nil && len(oldConfig.Services) > i && oldConfig.Services[i].Equal(image) {
|
||||||
return err
|
if err := extractPackageFilesFromTar(tmpfile, iw, image.Image, fmt.Sprintf("services[%d]", i)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := outputImage(image, "services", i, "", m, idMap, dupMap, iw, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add files
|
// add files
|
||||||
err := filesystem(m, iw, idMap)
|
if err := filesystem(m, iw, idMap); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to add filesystem parts: %v", err)
|
return fmt.Errorf("failed to add filesystem parts: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -711,3 +805,35 @@ func filesystem(m Moby, tw *tar.Writer, idMap map[string]uint32) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractPackageFilesFromTar reads files from the input tar and extracts those that have the correct
|
||||||
|
// PAXRecords - keys and values - to the tarWriter.
|
||||||
|
func extractPackageFilesFromTar(inTar *os.File, tw tarWriter, image, section string) error {
|
||||||
|
log.Infof("Copy %s files from input tar: %s", section, image)
|
||||||
|
// copy kernel files over
|
||||||
|
if _, err := inTar.Seek(0, 0); err != nil {
|
||||||
|
return fmt.Errorf("failed to seek to beginning of input tar: %w", err)
|
||||||
|
}
|
||||||
|
tr := tar.NewReader(inTar)
|
||||||
|
for {
|
||||||
|
hdr, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read input tar: %w", err)
|
||||||
|
}
|
||||||
|
if hdr.PAXRecords == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if hdr.PAXRecords[PaxRecordLinuxkitSource] == image && hdr.PAXRecords[PaxRecordLinuxkitLocation] == section {
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return fmt.Errorf("failed to write header: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(tw, tr); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy %s file: %w", section, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package moby
|
package moby
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
@ -62,6 +63,27 @@ type Image struct {
|
|||||||
ImageConfig `yaml:",inline"`
|
ImageConfig `yaml:",inline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equal check if another Image is functionally equal to this one.
|
||||||
|
// Takes the easy path by marshaling both into yaml and then comparing the yaml.
|
||||||
|
// There may be a more efficient way to do this, but this is simplest.
|
||||||
|
func (i *Image) Equal(o *Image) bool {
|
||||||
|
// if we are going to compare, we must canonicalized both image names
|
||||||
|
i0 := i
|
||||||
|
i0.Image = util.ReferenceExpand(i.Image)
|
||||||
|
iy, err := yaml.Marshal(i0)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
o0 := o
|
||||||
|
o0.Image = util.ReferenceExpand(o.Image)
|
||||||
|
oy, err := yaml.Marshal(o)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return bytes.Equal(iy, oy)
|
||||||
|
}
|
||||||
|
|
||||||
// ImageConfig is the configuration part of Image, it is the subset
|
// ImageConfig is the configuration part of Image, it is the subset
|
||||||
// which is valid in a "org.mobyproject.config" label on an image.
|
// which is valid in a "org.mobyproject.config" label on an image.
|
||||||
// Everything except Runtime and ref is used to build the OCI spec
|
// Everything except Runtime and ref is used to build the OCI spec
|
||||||
|
@ -9,4 +9,5 @@ type BuildOpts struct {
|
|||||||
DockerCache bool
|
DockerCache bool
|
||||||
Arch string
|
Arch string
|
||||||
SbomGenerator *SbomGenerator
|
SbomGenerator *SbomGenerator
|
||||||
|
InputTar string
|
||||||
}
|
}
|
||||||
|
26
test/cases/000_build/010_reproducible/004_input_tar/test.sh
Normal file
26
test/cases/000_build/010_reproducible/004_input_tar/test.sh
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SUMMARY: Check that tar output format build is reproducible after leveraging input tar
|
||||||
|
# LABELS:
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Source libraries. Uncomment if needed/defined
|
||||||
|
#. "${RT_LIB}"
|
||||||
|
. "${RT_PROJECT_ROOT}/_lib/lib.sh"
|
||||||
|
|
||||||
|
NAME=check
|
||||||
|
|
||||||
|
clean_up() {
|
||||||
|
rm -f ${NAME}*
|
||||||
|
}
|
||||||
|
|
||||||
|
trap clean_up EXIT
|
||||||
|
|
||||||
|
# do not include the sbom, because the SBoM unique IDs per file/package are *not* deterministic,
|
||||||
|
# (currently based upon syft), and thus will make the file non-reproducible
|
||||||
|
linuxkit build --no-sbom --format tar --o "${NAME}-1.tar" ../test.yml
|
||||||
|
linuxkit build --no-sbom --format tar --input-tar "${NAME}-1.tar" --o "${NAME}-2.tar" ../test.yml
|
||||||
|
|
||||||
|
diff -q "${NAME}-1.tar" "${NAME}-2.tar" || exit 1
|
||||||
|
|
||||||
|
exit 0
|
18
test/cases/000_build/060_input_tar/README.md
Normal file
18
test/cases/000_build/060_input_tar/README.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# testing --input-tar
|
||||||
|
|
||||||
|
This test works by building two tar files, and checking logs.
|
||||||
|
This only works because we use verbose logs.
|
||||||
|
|
||||||
|
The two files - `test1.yml` and `test2.yml` are identical, except for some changed lines.
|
||||||
|
|
||||||
|
The test script - `test.sh` - builds an image from `test1.yml`, then uses its output
|
||||||
|
as `--input-tar` for building from `test2.yml`. It then checks the output logs to make sure
|
||||||
|
that expected sections are copied over, and unexpected ones are not.
|
||||||
|
|
||||||
|
**Note:** If you make any changes to either test file, mark here and in `test.sh` so we know what has changed.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- added one entry in `init`
|
||||||
|
- changed the command in `onboot[1]`
|
||||||
|
- removed `services[1]`, which causes `services[2]` to become `services[1]`, and thus should not be copied either, as order may matter.
|
49
test/cases/000_build/060_input_tar/test.sh
Normal file
49
test/cases/000_build/060_input_tar/test.sh
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SUMMARY: Check that tar output format build is reproducible after leveraging input tar
|
||||||
|
# LABELS:
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Source libraries. Uncomment if needed/defined
|
||||||
|
#. "${RT_LIB}"
|
||||||
|
. "${RT_PROJECT_ROOT}/_lib/lib.sh"
|
||||||
|
|
||||||
|
NAME=check
|
||||||
|
|
||||||
|
clean_up() {
|
||||||
|
rm -f ${NAME}-*.tar
|
||||||
|
}
|
||||||
|
|
||||||
|
trap clean_up EXIT
|
||||||
|
|
||||||
|
logfile=$(mktemp)
|
||||||
|
|
||||||
|
# do not include the sbom, because the SBoM unique IDs per file/package are *not* deterministic,
|
||||||
|
# (currently based upon syft), and thus will make the file non-reproducible
|
||||||
|
linuxkit build --no-sbom --format tar --o "${NAME}-1.tar" ./test1.yml
|
||||||
|
linuxkit build -v --no-sbom --format tar --input-tar "${NAME}-1.tar" --o "${NAME}-2.tar" ./test2.yml 2>&1 | tee ${logfile}
|
||||||
|
|
||||||
|
# the logfile should indicate which parts were copied and which not
|
||||||
|
# we only know this because we built the test2.yml manually
|
||||||
|
|
||||||
|
# should have 3 entries copied from init, but not a 4th
|
||||||
|
errors=""
|
||||||
|
grep -q "Copy init\[0\]" ${logfile} || errors="${errors}\nmissing Copy init[0]"
|
||||||
|
grep -q "Copy init\[1\]" ${logfile} || errors="${errors}\nmissing Copy init[1]"
|
||||||
|
grep -q "Copy init\[2\]" ${logfile} || errors="${errors}\nmissing Copy init[2]"
|
||||||
|
grep -q "Copy init\[3\]" ${logfile} && errors="${errors}\nunexpected Copy init[3]"
|
||||||
|
# should have one entry copied from onboot, but not a second
|
||||||
|
grep -q "Copy onboot\[0\]" ${logfile} || errors="${errors}\nmissing Copy onboot[0]"
|
||||||
|
grep -q "Copy onboot\[1\]" ${logfile} && errors="${errors}\nunexpected Copy onboot[1]"
|
||||||
|
# should have one entry copied from services, but not a second or third
|
||||||
|
grep -q "Copy services\[0\]" ${logfile} || errors="${errors}\nmissing Copy services[0]"
|
||||||
|
grep -q "Copy services\[1\]" ${logfile} && errors="${errors}\nunexpected Copy services[1]"
|
||||||
|
grep -q "Copy services\[2\]" ${logfile} && errors="${errors}\nunexpected Copy services[2]"
|
||||||
|
|
||||||
|
if [ -n "${errors}" ]; then
|
||||||
|
echo "Errors: ${errors}"
|
||||||
|
echo "logfile: ${logfile}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
37
test/cases/000_build/060_input_tar/test1.yml
Normal file
37
test/cases/000_build/060_input_tar/test1.yml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
kernel:
|
||||||
|
image: linuxkit/kernel:6.6.13
|
||||||
|
cmdline: "console=tty0 console=ttyS0 console=ttyAMA0"
|
||||||
|
init:
|
||||||
|
- linuxkit/init:45a1ad5919f0b6acf0f0cf730e9434abfae11fe6
|
||||||
|
- linuxkit/runc:6062483d748609d505f2bcde4e52ee64a3329f5f
|
||||||
|
- linuxkit/containerd:e7a92d9f3282039eac5fb1b07cac2b8664cbf0ad
|
||||||
|
onboot:
|
||||||
|
- name: sysctl
|
||||||
|
image: linuxkit/sysctl:5a374e4bf3e5a7deeacff6571d0f30f7ea8f56db
|
||||||
|
- name: dhcpcd
|
||||||
|
image: linuxkit/dhcpcd:e9e3580f2de00e73e7b316a007186d22fea056ee
|
||||||
|
command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"]
|
||||||
|
onshutdown:
|
||||||
|
- name: shutdown
|
||||||
|
image: busybox:latest
|
||||||
|
command: ["/bin/echo", "so long and thanks for all the fish"]
|
||||||
|
services:
|
||||||
|
- name: getty
|
||||||
|
image: linuxkit/getty:5d86a2ce2d890c14ab66b13638dcadf74f29218b
|
||||||
|
env:
|
||||||
|
- INSECURE=true
|
||||||
|
- name: rngd
|
||||||
|
image: linuxkit/rngd:cdb919e4aee49fed0bf6075f0a104037cba83c39
|
||||||
|
- name: nginx
|
||||||
|
image: nginx:1.19.5-alpine
|
||||||
|
capabilities:
|
||||||
|
- CAP_NET_BIND_SERVICE
|
||||||
|
- CAP_CHOWN
|
||||||
|
- CAP_SETUID
|
||||||
|
- CAP_SETGID
|
||||||
|
- CAP_DAC_OVERRIDE
|
||||||
|
binds:
|
||||||
|
- /etc/resolv.conf:/etc/resolv.conf
|
||||||
|
files:
|
||||||
|
- path: etc/linuxkit-config
|
||||||
|
metadata: yaml
|
36
test/cases/000_build/060_input_tar/test2.yml
Normal file
36
test/cases/000_build/060_input_tar/test2.yml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
kernel:
|
||||||
|
image: linuxkit/kernel:6.6.13
|
||||||
|
cmdline: "console=tty0 console=ttyS0 console=ttyAMA0"
|
||||||
|
init:
|
||||||
|
- linuxkit/init:45a1ad5919f0b6acf0f0cf730e9434abfae11fe6
|
||||||
|
- linuxkit/runc:6062483d748609d505f2bcde4e52ee64a3329f5f
|
||||||
|
- linuxkit/containerd:e7a92d9f3282039eac5fb1b07cac2b8664cbf0ad
|
||||||
|
- linuxkit/ca-certificates:5aaa343474e5ac3ac01f8b917e82efb1063d80ff
|
||||||
|
onboot:
|
||||||
|
- name: sysctl
|
||||||
|
image: linuxkit/sysctl:5a374e4bf3e5a7deeacff6571d0f30f7ea8f56db
|
||||||
|
- name: dhcpcd
|
||||||
|
image: linuxkit/dhcpcd:e9e3580f2de00e73e7b316a007186d22fea056ee
|
||||||
|
command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1", "change"]
|
||||||
|
onshutdown:
|
||||||
|
- name: shutdown
|
||||||
|
image: busybox:latest
|
||||||
|
command: ["/bin/echo", "so long and thanks for all the fish"]
|
||||||
|
services:
|
||||||
|
- name: getty
|
||||||
|
image: linuxkit/getty:5d86a2ce2d890c14ab66b13638dcadf74f29218b
|
||||||
|
env:
|
||||||
|
- INSECURE=true
|
||||||
|
- name: nginx
|
||||||
|
image: nginx:1.19.5-alpine
|
||||||
|
capabilities:
|
||||||
|
- CAP_NET_BIND_SERVICE
|
||||||
|
- CAP_CHOWN
|
||||||
|
- CAP_SETUID
|
||||||
|
- CAP_SETGID
|
||||||
|
- CAP_DAC_OVERRIDE
|
||||||
|
binds:
|
||||||
|
- /etc/resolv.conf:/etc/resolv.conf
|
||||||
|
files:
|
||||||
|
- path: etc/linuxkit-config
|
||||||
|
metadata: yaml
|
Loading…
Reference in New Issue
Block a user