| package winlayers |
| |
| import ( |
| "archive/tar" |
| "context" |
| "crypto/rand" |
| "encoding/base64" |
| "fmt" |
| "io" |
| "time" |
| |
| "github.com/containerd/containerd/archive" |
| "github.com/containerd/containerd/archive/compression" |
| "github.com/containerd/containerd/content" |
| "github.com/containerd/containerd/diff" |
| "github.com/containerd/containerd/errdefs" |
| "github.com/containerd/containerd/log" |
| "github.com/containerd/containerd/mount" |
| digest "github.com/opencontainers/go-digest" |
| ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| "github.com/pkg/errors" |
| "github.com/sirupsen/logrus" |
| ) |
| |
| const ( |
| keyFileAttr = "MSWINDOWS.fileattr" |
| keySDRaw = "MSWINDOWS.rawsd" |
| keyCreationTime = "LIBARCHIVE.creationtime" |
| ) |
| |
| func NewWalkingDiffWithWindows(store content.Store, d diff.Comparer) diff.Comparer { |
| return &winDiffer{ |
| store: store, |
| d: d, |
| } |
| } |
| |
| var emptyDesc = ocispec.Descriptor{} |
| |
| type winDiffer struct { |
| store content.Store |
| d diff.Comparer |
| } |
| |
| // Compare creates a diff between the given mounts and uploads the result |
| // to the content store. |
| func (s *winDiffer) Compare(ctx context.Context, lower, upper []mount.Mount, opts ...diff.Opt) (d ocispec.Descriptor, err error) { |
| if !hasWindowsLayerMode(ctx) { |
| return s.d.Compare(ctx, lower, upper, opts...) |
| } |
| |
| var config diff.Config |
| for _, opt := range opts { |
| if err := opt(&config); err != nil { |
| return emptyDesc, err |
| } |
| } |
| |
| if config.MediaType == "" { |
| config.MediaType = ocispec.MediaTypeImageLayerGzip |
| } |
| |
| var isCompressed bool |
| switch config.MediaType { |
| case ocispec.MediaTypeImageLayer: |
| case ocispec.MediaTypeImageLayerGzip: |
| isCompressed = true |
| default: |
| return emptyDesc, errors.Wrapf(errdefs.ErrNotImplemented, "unsupported diff media type: %v", config.MediaType) |
| } |
| |
| var ocidesc ocispec.Descriptor |
| if err := mount.WithTempMount(ctx, lower, func(lowerRoot string) error { |
| return mount.WithTempMount(ctx, upper, func(upperRoot string) error { |
| var newReference bool |
| if config.Reference == "" { |
| newReference = true |
| config.Reference = uniqueRef() |
| } |
| |
| cw, err := s.store.Writer(ctx, |
| content.WithRef(config.Reference), |
| content.WithDescriptor(ocispec.Descriptor{ |
| MediaType: config.MediaType, // most contentstore implementations just ignore this |
| })) |
| if err != nil { |
| return errors.Wrap(err, "failed to open writer") |
| } |
| defer func() { |
| if err != nil { |
| cw.Close() |
| if newReference { |
| if err := s.store.Abort(ctx, config.Reference); err != nil { |
| log.G(ctx).WithField("ref", config.Reference).Warnf("failed to delete diff upload") |
| } |
| } |
| } |
| }() |
| if !newReference { |
| if err := cw.Truncate(0); err != nil { |
| return err |
| } |
| } |
| |
| if isCompressed { |
| dgstr := digest.SHA256.Digester() |
| compressed, err := compression.CompressStream(cw, compression.Gzip) |
| if err != nil { |
| return errors.Wrap(err, "failed to get compressed stream") |
| } |
| var w io.Writer = io.MultiWriter(compressed, dgstr.Hash()) |
| w, discard, done := makeWindowsLayer(w) |
| err = archive.WriteDiff(ctx, w, lowerRoot, upperRoot) |
| if err != nil { |
| discard(err) |
| } |
| <-done |
| compressed.Close() |
| if err != nil { |
| return errors.Wrap(err, "failed to write compressed diff") |
| } |
| |
| if config.Labels == nil { |
| config.Labels = map[string]string{} |
| } |
| config.Labels["containerd.io/uncompressed"] = dgstr.Digest().String() |
| } else { |
| w, discard, done := makeWindowsLayer(cw) |
| if err = archive.WriteDiff(ctx, w, lowerRoot, upperRoot); err != nil { |
| discard(err) |
| return errors.Wrap(err, "failed to write diff") |
| } |
| <-done |
| } |
| |
| var commitopts []content.Opt |
| if config.Labels != nil { |
| commitopts = append(commitopts, content.WithLabels(config.Labels)) |
| } |
| |
| dgst := cw.Digest() |
| if err := cw.Commit(ctx, 0, dgst, commitopts...); err != nil { |
| return errors.Wrap(err, "failed to commit") |
| } |
| |
| info, err := s.store.Info(ctx, dgst) |
| if err != nil { |
| return errors.Wrap(err, "failed to get info from content store") |
| } |
| |
| ocidesc = ocispec.Descriptor{ |
| MediaType: config.MediaType, |
| Size: info.Size, |
| Digest: info.Digest, |
| } |
| return nil |
| }) |
| }); err != nil { |
| return emptyDesc, err |
| } |
| |
| return ocidesc, nil |
| } |
| |
| func uniqueRef() string { |
| t := time.Now() |
| var b [3]byte |
| // Ignore read failures, just decreases uniqueness |
| rand.Read(b[:]) |
| return fmt.Sprintf("%d-%s", t.UnixNano(), base64.URLEncoding.EncodeToString(b[:])) |
| } |
| |
| func prepareWinHeader(h *tar.Header) { |
| if h.PAXRecords == nil { |
| h.PAXRecords = map[string]string{} |
| } |
| if h.Typeflag == tar.TypeDir { |
| h.Mode |= 1 << 14 |
| h.PAXRecords[keyFileAttr] = "16" |
| } |
| |
| if h.Typeflag == tar.TypeReg { |
| h.Mode |= 1 << 15 |
| h.PAXRecords[keyFileAttr] = "32" |
| } |
| |
| if !h.ModTime.IsZero() { |
| h.PAXRecords[keyCreationTime] = fmt.Sprintf("%d.%d", h.ModTime.Unix(), h.ModTime.Nanosecond()) |
| } |
| |
| h.Format = tar.FormatPAX |
| } |
| |
| func addSecurityDescriptor(h *tar.Header) { |
| if h.Typeflag == tar.TypeDir { |
| // O:BAG:SYD:(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)(A;;FA;;;BA)(A;OICIIO;GA;;;CO)(A;OICI;0x1200a9;;;BU)(A;CI;LC;;;BU)(A;CI;DC;;;BU) |
| h.PAXRecords[keySDRaw] = "AQAEgBQAAAAkAAAAAAAAADAAAAABAgAAAAAABSAAAAAgAgAAAQEAAAAAAAUSAAAAAgCoAAcAAAAAAxgA/wEfAAECAAAAAAAFIAAAACACAAAAAxQA/wEfAAEBAAAAAAAFEgAAAAAAGAD/AR8AAQIAAAAAAAUgAAAAIAIAAAALFAAAAAAQAQEAAAAAAAMAAAAAAAMYAKkAEgABAgAAAAAABSAAAAAhAgAAAAIYAAQAAAABAgAAAAAABSAAAAAhAgAAAAIYAAIAAAABAgAAAAAABSAAAAAhAgAA" |
| } |
| |
| if h.Typeflag == tar.TypeReg { |
| // O:BAG:SYD:(A;;FA;;;BA)(A;;FA;;;SY)(A;;0x1200a9;;;BU) |
| h.PAXRecords[keySDRaw] = "AQAEgBQAAAAkAAAAAAAAADAAAAABAgAAAAAABSAAAAAgAgAAAQEAAAAAAAUSAAAAAgBMAAMAAAAAABgA/wEfAAECAAAAAAAFIAAAACACAAAAABQA/wEfAAEBAAAAAAAFEgAAAAAAGACpABIAAQIAAAAAAAUgAAAAIQIAAA==" |
| } |
| } |
| |
| func makeWindowsLayer(w io.Writer) (io.Writer, func(error), chan error) { |
| pr, pw := io.Pipe() |
| done := make(chan error) |
| |
| go func() { |
| tarReader := tar.NewReader(pr) |
| tarWriter := tar.NewWriter(w) |
| |
| err := func() error { |
| |
| h := &tar.Header{ |
| Name: "Hives", |
| Typeflag: tar.TypeDir, |
| ModTime: time.Now(), |
| } |
| prepareWinHeader(h) |
| if err := tarWriter.WriteHeader(h); err != nil { |
| return err |
| } |
| |
| h = &tar.Header{ |
| Name: "Files", |
| Typeflag: tar.TypeDir, |
| ModTime: time.Now(), |
| } |
| prepareWinHeader(h) |
| if err := tarWriter.WriteHeader(h); err != nil { |
| return err |
| } |
| |
| for { |
| h, err := tarReader.Next() |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| return err |
| } |
| h.Name = "Files/" + h.Name |
| if h.Linkname != "" { |
| h.Linkname = "Files/" + h.Linkname |
| } |
| prepareWinHeader(h) |
| addSecurityDescriptor(h) |
| if err := tarWriter.WriteHeader(h); err != nil { |
| return err |
| } |
| if h.Size > 0 { |
| if _, err := io.Copy(tarWriter, tarReader); err != nil { |
| return err |
| } |
| } |
| } |
| return tarWriter.Close() |
| }() |
| if err != nil { |
| logrus.Errorf("makeWindowsLayer %+v", err) |
| } |
| pw.CloseWithError(err) |
| done <- err |
| return |
| }() |
| |
| discard := func(err error) { |
| pw.CloseWithError(err) |
| } |
| |
| return pw, discard, done |
| } |