kairos-sdk/sysext/sysext.go
Itxaka e7f1026df1
Add a method to extract files from a docker image last layer (#362)
Co-authored-by: Mauro Morales <mauro.morales@spectrocloud.com>
2024-09-10 10:04:57 +02:00

98 lines
2.9 KiB
Go

package sysext
import (
"archive/tar"
"errors"
"fmt"
"github.com/kairos-io/kairos-sdk/types"
"io"
"os"
"path/filepath"
"regexp"
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
)
var ErrorImageNoLayers = errors.New("image")
// DefaultAllowListRegex provided for easy use of defaults for confext and sysext
var DefaultAllowListRegex = regexp.MustCompile(`^usr/*|^/usr/*|^etc/*|^/etc/*`)
// ExtractFilesFromLastLayer will get an image and a destination and extract the files from the last layer in the image
// into that destination.
// It will skip anything that doesn't start with /usr or /etc as its purpose is to get the files for creating a
// sysextension or a confextension
// Accepts an allowList in form of regexp.Regexp that will match the files and allow copying
func ExtractFilesFromLastLayer(image v1.Image, dst string, log types.KairosLogger, allowList *regexp.Regexp) error {
layers, _ := image.Layers()
numLayers := len(layers)
if len(layers) <= 0 {
return ErrorImageNoLayers
}
return extractFilesFromLayer(image, dst, log, allowList, numLayers-1)
}
func extractFilesFromLayer(image v1.Image, dst string, log types.KairosLogger, allowList *regexp.Regexp, layerNumber int) error {
layers, _ := image.Layers()
layerToExtract := layers[layerNumber]
layerReader, _ := layerToExtract.Uncompressed()
defer func(layerReader io.ReadCloser) {
_ = layerReader.Close()
}(layerReader)
tr := tar.NewReader(layerReader)
// TODO: Support whiteout? https://github.com/opencontainers/image-spec/blob/79b036d80240ae530a8de15e1d21c7ab9292c693/layer.md#whiteouts
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("tar read: %w", err)
}
header.Name = filepath.Clean(header.Name)
path := filepath.Join(dst, header.Name)
fi := header.FileInfo()
mask := fi.Mode()
if !allowList.MatchString(header.Name) {
log.Debug("Skipping ", header.Name)
continue
}
switch header.Typeflag {
case tar.TypeDir:
log.Debugf("%s is a directory", header.Name)
if fi, err := os.Lstat(path); !(err == nil && fi.IsDir()) {
if err := os.MkdirAll(path, mask); err != nil {
return fmt.Errorf("mkdir: %w", err)
}
}
case tar.TypeReg:
log.Debugf("%s is a file", header.Name)
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, mask)
if err != nil {
return fmt.Errorf("open: %w", err)
}
if _, err := io.Copy(file, tr); err != nil {
file.Close()
return fmt.Errorf("copy: %w", err)
}
file.Close()
case tar.TypeSymlink:
log.Debugf("%s is a symlink", header.Name)
targetPath := filepath.Join(filepath.Dir(path), header.Linkname)
if !strings.HasPrefix(targetPath, dst) {
return fmt.Errorf("symlink: %w", err)
}
if err := os.Symlink(header.Linkname, path); err != nil {
return fmt.Errorf("symlink: %w", err)
}
default:
return fmt.Errorf("unsupported type: %d", header.Typeflag)
}
}
return nil
}