Use OCI "History" type instead of inventing our own copy
The most notable change here is that the OCI's type uses a pointer for `Created`, which we probably should've been too, so most of these changes are accounting for that (and embedding our `Equal` implementation in the one single place it was used).

I made this while reviewing/chatting about #45322 (and #45501), so I definitely give the credit for it to @laurazard :joy:

This _just_ uses a type alias for now; I think we should probably update to use the native OCI type everywhere, but that was going to be a slightly larger diff so I didn't go that far yet. :sweat_smile:
image/image.go
package image // import "github.com/docker/docker/image"

import (
	"encoding/json"
	"errors"
	"io"
	"reflect"
	"runtime"
	"strings"
	"time"

	"github.com/docker/distribution/reference"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/dockerversion"
	"github.com/docker/docker/layer"
	"github.com/opencontainers/go-digest"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

// ID is the content-addressable ID of an image.
type ID digest.Digest

func (id ID) String() string {
	return id.Digest().String()
}

// Digest converts ID into a digest
func (id ID) Digest() digest.Digest {
	return digest.Digest(id)
}

// IDFromDigest creates an ID from a digest
//
// Deprecated: cast to an ID using ID(digest).
func IDFromDigest(digest digest.Digest) ID {
	return ID(digest)
}

// V1Image stores the V1 image configuration.
type V1Image struct {
	// ID is a unique 64 character identifier of the image
	ID string `json:"id,omitempty"`

	// Parent is the ID of the parent image.
	//
	// Depending on how the image was created, this field may be empty and
	// is only set for images that were built/created locally. This field
	// is empty if the image was pulled from an image registry.
	Parent string `json:"parent,omitempty"`

	// Comment is an optional message that can be set when committing or
	// importing the image.
	Comment string `json:"comment,omitempty"`

	// Created is the timestamp at which the image was created
	Created time.Time `json:"created"`

	// Container is the ID of the container that was used to create the image.
	//
	// Depending on how the image was created, this field may be empty.
	Container string `json:"container,omitempty"`

	// ContainerConfig is the configuration of the container that was committed
	// into the image.
	ContainerConfig container.Config `json:"container_config,omitempty"`

	// DockerVersion is the version of Docker that was used to build the image.
	//
	// Depending on how the image was created, this field may be empty.
	DockerVersion string `json:"docker_version,omitempty"`

	// Author is the name of the author that was specified when committing the
	// image, or as specified through MAINTAINER (deprecated) in the Dockerfile.
	Author string `json:"author,omitempty"`

	// Config is the configuration of the container received from the client.
	Config *container.Config `json:"config,omitempty"`

	// Architecture is the hardware CPU architecture that the image runs on.
	Architecture string `json:"architecture,omitempty"`

	// Variant is the CPU architecture variant (presently ARM-only).
	Variant string `json:"variant,omitempty"`

	// OS is the Operating System the image is built to run on.
	OS string `json:"os,omitempty"`

	// Size is the total size of the image including all layers it is composed of.
	Size int64 `json:",omitempty"`
}

// Image stores the image configuration
type Image struct {
	V1Image

	// Parent is the ID of the parent image.
	//
	// Depending on how the image was created, this field may be empty and
	// is only set for images that were built/created locally. This field
	// is empty if the image was pulled from an image registry.
	Parent ID `json:"parent,omitempty"` //nolint:govet

	// RootFS contains information about the image's RootFS, including the
	// layer IDs.
	RootFS  *RootFS   `json:"rootfs,omitempty"`
	History []History `json:"history,omitempty"`

	// OsVersion is the version of the Operating System the image is built to
	// run on (especially for Windows).
	OSVersion  string   `json:"os.version,omitempty"`
	OSFeatures []string `json:"os.features,omitempty"`

	// rawJSON caches the immutable JSON associated with this image.
	rawJSON []byte

	// computedID is the ID computed from the hash of the image config.
	// Not to be confused with the legacy V1 ID in V1Image.
	computedID ID

	// Details holds additional details about image
	Details *Details `json:"-"`
}

// Details provides additional image data
type Details struct {
	References  []reference.Named
	Size        int64
	Metadata    map[string]string
	Driver      string
	LastUpdated time.Time
}

// RawJSON returns the immutable JSON associated with the image.
func (img *Image) RawJSON() []byte {
	return img.rawJSON
}

// ID returns the image's content-addressable ID.
func (img *Image) ID() ID {
	return img.computedID
}

// ImageID stringifies ID.
func (img *Image) ImageID() string {
	return img.ID().String()
}

// RunConfig returns the image's container config.
func (img *Image) RunConfig() *container.Config {
	return img.Config
}

// BaseImgArch returns the image's architecture. If not populated, defaults to the host runtime arch.
func (img *Image) BaseImgArch() string {
	arch := img.Architecture
	if arch == "" {
		arch = runtime.GOARCH
	}
	return arch
}

// BaseImgVariant returns the image's variant, whether populated or not.
// This avoids creating an inconsistency where the stored image variant
// is "greater than" (i.e. v8 vs v6) the actual image variant.
func (img *Image) BaseImgVariant() string {
	return img.Variant
}

// OperatingSystem returns the image's operating system. If not populated, defaults to the host runtime OS.
func (img *Image) OperatingSystem() string {
	os := img.OS
	if os == "" {
		os = runtime.GOOS
	}
	return os
}

// Platform generates an OCI platform from the image
func (img *Image) Platform() ocispec.Platform {
	return ocispec.Platform{
		Architecture: img.Architecture,
		OS:           img.OS,
		OSVersion:    img.OSVersion,
		OSFeatures:   img.OSFeatures,
		Variant:      img.Variant,
	}
}

// MarshalJSON serializes the image to JSON. It sorts the top-level keys so
// that JSON that's been manipulated by a push/pull cycle with a legacy
// registry won't end up with a different key order.
func (img *Image) MarshalJSON() ([]byte, error) {
	type MarshalImage Image

	pass1, err := json.Marshal(MarshalImage(*img))
	if err != nil {
		return nil, err
	}

	var c map[string]*json.RawMessage
	if err := json.Unmarshal(pass1, &c); err != nil {
		return nil, err
	}
	return json.Marshal(c)
}

// ChildConfig is the configuration to apply to an Image to create a new
// Child image. Other properties of the image are copied from the parent.
type ChildConfig struct {
	ContainerID     string
	Author          string
	Comment         string
	DiffID          layer.DiffID
	ContainerConfig *container.Config
	Config          *container.Config
}

// NewImage creates a new image with the given ID
func NewImage(id ID) *Image {
	return &Image{
		computedID: id,
	}
}

// NewChildImage creates a new Image as a child of this image.
func NewChildImage(img *Image, child ChildConfig, os string) *Image {
	isEmptyLayer := layer.IsEmpty(child.DiffID)
	var rootFS *RootFS
	if img.RootFS != nil {
		rootFS = img.RootFS.Clone()
	} else {
		rootFS = NewRootFS()
	}

	if !isEmptyLayer {
		rootFS.Append(child.DiffID)
	}
	imgHistory := NewHistory(
		child.Author,
		child.Comment,
		strings.Join(child.ContainerConfig.Cmd, " "),
		isEmptyLayer)

	return &Image{
		V1Image: V1Image{
			DockerVersion:   dockerversion.Version,
			Config:          child.Config,
			Architecture:    img.BaseImgArch(),
			Variant:         img.BaseImgVariant(),
			OS:              os,
			Container:       child.ContainerID,
			ContainerConfig: *child.ContainerConfig,
			Author:          child.Author,
			Created:         imgHistory.Created,
		},
		RootFS:     rootFS,
		History:    append(img.History, imgHistory),
		OSFeatures: img.OSFeatures,
		OSVersion:  img.OSVersion,
	}
}

// History stores build commands that were used to create an image
type History struct {
	// Created is the timestamp at which the image was created
	Created time.Time `json:"created"`
	// Author is the name of the author that was specified when committing the
	// image, or as specified through MAINTAINER (deprecated) in the Dockerfile.
	Author string `json:"author,omitempty"`
	// CreatedBy keeps the Dockerfile command used while building the image
	CreatedBy string `json:"created_by,omitempty"`
	// Comment is the commit message that was set when committing the image
	Comment string `json:"comment,omitempty"`
	// EmptyLayer is set to true if this history item did not generate a
	// layer. Otherwise, the history item is associated with the next
	// layer in the RootFS section.
	EmptyLayer bool `json:"empty_layer,omitempty"`
}

// NewHistory creates a new history struct from arguments, and sets the created
// time to the current time in UTC
func NewHistory(author, comment, createdBy string, isEmptyLayer bool) History {
	return History{
		Author:     author,
		Created:    time.Now().UTC(),
		CreatedBy:  createdBy,
		Comment:    comment,
		EmptyLayer: isEmptyLayer,
	}
}

// Equal compares two history structs for equality
func (h History) Equal(i History) bool {
	if !h.Created.Equal(i.Created) {
		return false
	}
	i.Created = h.Created

	return reflect.DeepEqual(h, i)
}

// Exporter provides interface for loading and saving images
type Exporter interface {
	Load(io.ReadCloser, io.Writer, bool) error
	// TODO: Load(net.Context, io.ReadCloser, <- chan StatusMessage) error
	Save([]string, io.Writer) error
}

// NewFromJSON creates an Image configuration from json.
func NewFromJSON(src []byte) (*Image, error) {
	img := &Image{}

	if err := json.Unmarshal(src, img); err != nil {
		return nil, err
	}
	if img.RootFS == nil {
		return nil, errors.New("invalid image JSON, no RootFS key")
	}

	img.rawJSON = src

	return img, nil
}

package image // import "github.com/docker/docker/image"

import (
	"encoding/json"
	"errors"
	"io"
	"runtime"
	"strings"
	"time"

	"github.com/docker/distribution/reference"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/dockerversion"
	"github.com/docker/docker/layer"
	"github.com/opencontainers/go-digest"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

// ID is the content-addressable ID of an image.
type ID digest.Digest

func (id ID) String() string {
	return id.Digest().String()
}

// Digest converts ID into a digest
func (id ID) Digest() digest.Digest {
	return digest.Digest(id)
}

// IDFromDigest creates an ID from a digest
//
// Deprecated: cast to an ID using ID(digest).
func IDFromDigest(digest digest.Digest) ID {
	return ID(digest)
}

// V1Image stores the V1 image configuration.
type V1Image struct {
	// ID is a unique 64 character identifier of the image
	ID string `json:"id,omitempty"`

	// Parent is the ID of the parent image.
	//
	// Depending on how the image was created, this field may be empty and
	// is only set for images that were built/created locally. This field
	// is empty if the image was pulled from an image registry.
	Parent string `json:"parent,omitempty"`

	// Comment is an optional message that can be set when committing or
	// importing the image.
	Comment string `json:"comment,omitempty"`

	// Created is the timestamp at which the image was created
	Created *time.Time `json:"created"`

	// Container is the ID of the container that was used to create the image.
	//
	// Depending on how the image was created, this field may be empty.
	Container string `json:"container,omitempty"`

	// ContainerConfig is the configuration of the container that was committed
	// into the image.
	ContainerConfig container.Config `json:"container_config,omitempty"`

	// DockerVersion is the version of Docker that was used to build the image.
	//
	// Depending on how the image was created, this field may be empty.
	DockerVersion string `json:"docker_version,omitempty"`

	// Author is the name of the author that was specified when committing the
	// image, or as specified through MAINTAINER (deprecated) in the Dockerfile.
	Author string `json:"author,omitempty"`

	// Config is the configuration of the container received from the client.
	Config *container.Config `json:"config,omitempty"`

	// Architecture is the hardware CPU architecture that the image runs on.
	Architecture string `json:"architecture,omitempty"`

	// Variant is the CPU architecture variant (presently ARM-only).
	Variant string `json:"variant,omitempty"`

	// OS is the Operating System the image is built to run on.
	OS string `json:"os,omitempty"`

	// Size is the total size of the image including all layers it is composed of.
	Size int64 `json:",omitempty"`
}

// Image stores the image configuration
type Image struct {
	V1Image

	// Parent is the ID of the parent image.
	//
	// Depending on how the image was created, this field may be empty and
	// is only set for images that were built/created locally. This field
	// is empty if the image was pulled from an image registry.
	Parent ID `json:"parent,omitempty"` //nolint:govet

	// RootFS contains information about the image's RootFS, including the
	// layer IDs.
	RootFS  *RootFS   `json:"rootfs,omitempty"`
	History []History `json:"history,omitempty"`

	// OsVersion is the version of the Operating System the image is built to
	// run on (especially for Windows).
	OSVersion  string   `json:"os.version,omitempty"`
	OSFeatures []string `json:"os.features,omitempty"`

	// rawJSON caches the immutable JSON associated with this image.
	rawJSON []byte

	// computedID is the ID computed from the hash of the image config.
	// Not to be confused with the legacy V1 ID in V1Image.
	computedID ID

	// Details holds additional details about image
	Details *Details `json:"-"`
}

// Details provides additional image data
type Details struct {
	References  []reference.Named
	Size        int64
	Metadata    map[string]string
	Driver      string
	LastUpdated time.Time
}

// RawJSON returns the immutable JSON associated with the image.
func (img *Image) RawJSON() []byte {
	return img.rawJSON
}

// ID returns the image's content-addressable ID.
func (img *Image) ID() ID {
	return img.computedID
}

// ImageID stringifies ID.
func (img *Image) ImageID() string {
	return img.ID().String()
}

// RunConfig returns the image's container config.
func (img *Image) RunConfig() *container.Config {
	return img.Config
}

// BaseImgArch returns the image's architecture. If not populated, defaults to the host runtime arch.
func (img *Image) BaseImgArch() string {
	arch := img.Architecture
	if arch == "" {
		arch = runtime.GOARCH
	}
	return arch
}

// BaseImgVariant returns the image's variant, whether populated or not.
// This avoids creating an inconsistency where the stored image variant
// is "greater than" (i.e. v8 vs v6) the actual image variant.
func (img *Image) BaseImgVariant() string {
	return img.Variant
}

// OperatingSystem returns the image's operating system. If not populated, defaults to the host runtime OS.
func (img *Image) OperatingSystem() string {
	os := img.OS
	if os == "" {
		os = runtime.GOOS
	}
	return os
}

// Platform generates an OCI platform from the image
func (img *Image) Platform() ocispec.Platform {
	return ocispec.Platform{
		Architecture: img.Architecture,
		OS:           img.OS,
		OSVersion:    img.OSVersion,
		OSFeatures:   img.OSFeatures,
		Variant:      img.Variant,
	}
}

// MarshalJSON serializes the image to JSON. It sorts the top-level keys so
// that JSON that's been manipulated by a push/pull cycle with a legacy
// registry won't end up with a different key order.
func (img *Image) MarshalJSON() ([]byte, error) {
	type MarshalImage Image

	pass1, err := json.Marshal(MarshalImage(*img))
	if err != nil {
		return nil, err
	}

	var c map[string]*json.RawMessage
	if err := json.Unmarshal(pass1, &c); err != nil {
		return nil, err
	}
	return json.Marshal(c)
}

// ChildConfig is the configuration to apply to an Image to create a new
// Child image. Other properties of the image are copied from the parent.
type ChildConfig struct {
	ContainerID     string
	Author          string
	Comment         string
	DiffID          layer.DiffID
	ContainerConfig *container.Config
	Config          *container.Config
}

// NewImage creates a new image with the given ID
func NewImage(id ID) *Image {
	return &Image{
		computedID: id,
	}
}

// NewChildImage creates a new Image as a child of this image.
func NewChildImage(img *Image, child ChildConfig, os string) *Image {
	isEmptyLayer := layer.IsEmpty(child.DiffID)
	var rootFS *RootFS
	if img.RootFS != nil {
		rootFS = img.RootFS.Clone()
	} else {
		rootFS = NewRootFS()
	}

	if !isEmptyLayer {
		rootFS.Append(child.DiffID)
	}
	imgHistory := NewHistory(
		child.Author,
		child.Comment,
		strings.Join(child.ContainerConfig.Cmd, " "),
		isEmptyLayer)

	return &Image{
		V1Image: V1Image{
			DockerVersion:   dockerversion.Version,
			Config:          child.Config,
			Architecture:    img.BaseImgArch(),
			Variant:         img.BaseImgVariant(),
			OS:              os,
			Container:       child.ContainerID,
			ContainerConfig: *child.ContainerConfig,
			Author:          child.Author,
			Created:         imgHistory.Created,
		},
		RootFS:     rootFS,
		History:    append(img.History, imgHistory),
		OSFeatures: img.OSFeatures,
		OSVersion:  img.OSVersion,
	}
}

// History stores build commands that were used to create an image
type History = ocispec.History

// NewHistory creates a new history struct from arguments, and sets the created
// time to the current time in UTC
func NewHistory(author, comment, createdBy string, isEmptyLayer bool) History {
	now := time.Now().UTC()
	return History{
		Author:     author,
		Created:    &now,
		CreatedBy:  createdBy,
		Comment:    comment,
		EmptyLayer: isEmptyLayer,
	}
}

// Exporter provides interface for loading and saving images
type Exporter interface {
	Load(io.ReadCloser, io.Writer, bool) error
	// TODO: Load(net.Context, io.ReadCloser, <- chan StatusMessage) error
	Save([]string, io.Writer) error
}

// NewFromJSON creates an Image configuration from json.
func NewFromJSON(src []byte) (*Image, error) {
	img := &Image{}

	if err := json.Unmarshal(src, img); err != nil {
		return nil, err
	}
	if img.RootFS == nil {
		return nil, errors.New("invalid image JSON, no RootFS key")
	}

	img.rawJSON = src

	return img, nil
}

We should prepare a PR that deprecates this one, and 🍒 ⛏️ it into the release branch to give potential users a heads-up.