working commit
This commit is contained in:
-56
@@ -1,56 +0,0 @@
|
||||
# `mutate`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate)
|
||||
|
||||
The `v1.Image`, `v1.ImageIndex`, and `v1.Layer` interfaces provide only
|
||||
accessor methods, so they are essentially immutable. If you want to change
|
||||
something about them, you need to produce a new instance of that interface.
|
||||
|
||||
A common use case for this library is to read an image from somewhere (a source),
|
||||
change something about it, and write the image somewhere else (a sink).
|
||||
|
||||
Graphically, this looks something like:
|
||||
|
||||
<p align="center">
|
||||
<img src="/images/mutate.dot.svg" />
|
||||
</p>
|
||||
|
||||
## Mutations
|
||||
|
||||
This is obviously not a comprehensive set of useful transformations (PRs welcome!),
|
||||
but a rough summary of what the `mutate` package currently does:
|
||||
|
||||
### `Config` and `ConfigFile`
|
||||
|
||||
These allow you to change the [image configuration](https://github.com/opencontainers/image-spec/blob/master/config.md#properties),
|
||||
e.g. to change the entrypoint, environment, author, etc.
|
||||
|
||||
### `Time`, `Canonical`, and `CreatedAt`
|
||||
|
||||
These are useful in the context of [reproducible builds](https://reproducible-builds.org/),
|
||||
where you may want to strip timestamps and other non-reproducible information.
|
||||
|
||||
### `Append`, `AppendLayers`, and `AppendManifests`
|
||||
|
||||
These functions allow the extension of a `v1.Image` or `v1.ImageIndex` with
|
||||
new layers or manifests.
|
||||
|
||||
For constructing an image `FROM scratch`, see the [`empty`](/pkg/v1/empty) package.
|
||||
|
||||
### `MediaType` and `IndexMediaType`
|
||||
|
||||
Sometimes, it is necessary to change the media type of an image or index,
|
||||
e.g. to appease a registry with strict validation of images (_looking at you, GCR_).
|
||||
|
||||
### `Rebase`
|
||||
|
||||
Rebase has [its own README](/cmd/crane/rebase.md).
|
||||
|
||||
This is the underlying implementation of [`crane rebase`](https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane_rebase.md).
|
||||
|
||||
### `Extract`
|
||||
|
||||
Extract will flatten an image filesystem into a single tar stream,
|
||||
respecting whiteout files.
|
||||
|
||||
This is the underlying implementation of [`crane export`](https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane_export.md).
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package mutate provides facilities for mutating v1.Images of any kind.
|
||||
package mutate
|
||||
-293
@@ -1,293 +0,0 @@
|
||||
// Copyright 2019 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mutate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/stream"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
type image struct {
|
||||
base v1.Image
|
||||
adds []Addendum
|
||||
|
||||
computed bool
|
||||
configFile *v1.ConfigFile
|
||||
manifest *v1.Manifest
|
||||
annotations map[string]string
|
||||
mediaType *types.MediaType
|
||||
configMediaType *types.MediaType
|
||||
diffIDMap map[v1.Hash]v1.Layer
|
||||
digestMap map[v1.Hash]v1.Layer
|
||||
subject *v1.Descriptor
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
var _ v1.Image = (*image)(nil)
|
||||
|
||||
func (i *image) MediaType() (types.MediaType, error) {
|
||||
if i.mediaType != nil {
|
||||
return *i.mediaType, nil
|
||||
}
|
||||
return i.base.MediaType()
|
||||
}
|
||||
|
||||
func (i *image) compute() error {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
// Don't re-compute if already computed.
|
||||
if i.computed {
|
||||
return nil
|
||||
}
|
||||
var configFile *v1.ConfigFile
|
||||
if i.configFile != nil {
|
||||
configFile = i.configFile
|
||||
} else {
|
||||
cf, err := i.base.ConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configFile = cf.DeepCopy()
|
||||
}
|
||||
diffIDs := configFile.RootFS.DiffIDs
|
||||
history := configFile.History
|
||||
|
||||
diffIDMap := make(map[v1.Hash]v1.Layer)
|
||||
digestMap := make(map[v1.Hash]v1.Layer)
|
||||
|
||||
for _, add := range i.adds {
|
||||
history = append(history, add.History)
|
||||
if add.Layer != nil {
|
||||
diffID, err := add.Layer.DiffID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
diffIDs = append(diffIDs, diffID)
|
||||
diffIDMap[diffID] = add.Layer
|
||||
}
|
||||
}
|
||||
|
||||
m, err := i.base.Manifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest := m.DeepCopy()
|
||||
manifestLayers := manifest.Layers
|
||||
for _, add := range i.adds {
|
||||
if add.Layer == nil {
|
||||
// Empty layers include only history in manifest.
|
||||
continue
|
||||
}
|
||||
|
||||
desc, err := partial.Descriptor(add.Layer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fields in the addendum override the original descriptor.
|
||||
if len(add.Annotations) != 0 {
|
||||
desc.Annotations = add.Annotations
|
||||
}
|
||||
if len(add.URLs) != 0 {
|
||||
desc.URLs = add.URLs
|
||||
}
|
||||
|
||||
if add.MediaType != "" {
|
||||
desc.MediaType = add.MediaType
|
||||
}
|
||||
|
||||
manifestLayers = append(manifestLayers, *desc)
|
||||
digestMap[desc.Digest] = add.Layer
|
||||
}
|
||||
|
||||
configFile.RootFS.DiffIDs = diffIDs
|
||||
configFile.History = history
|
||||
|
||||
manifest.Layers = manifestLayers
|
||||
|
||||
rcfg, err := json.Marshal(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, sz, err := v1.SHA256(bytes.NewBuffer(rcfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest.Config.Digest = d
|
||||
manifest.Config.Size = sz
|
||||
|
||||
// If Data was set in the base image, we need to update it in the mutated image.
|
||||
if m.Config.Data != nil {
|
||||
manifest.Config.Data = rcfg
|
||||
}
|
||||
|
||||
// If the user wants to mutate the media type of the config
|
||||
if i.configMediaType != nil {
|
||||
manifest.Config.MediaType = *i.configMediaType
|
||||
}
|
||||
|
||||
if i.mediaType != nil {
|
||||
manifest.MediaType = *i.mediaType
|
||||
}
|
||||
|
||||
if i.annotations != nil {
|
||||
if manifest.Annotations == nil {
|
||||
manifest.Annotations = map[string]string{}
|
||||
}
|
||||
|
||||
for k, v := range i.annotations {
|
||||
manifest.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
manifest.Subject = i.subject
|
||||
|
||||
i.configFile = configFile
|
||||
i.manifest = manifest
|
||||
i.diffIDMap = diffIDMap
|
||||
i.digestMap = digestMap
|
||||
i.computed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Layers returns the ordered collection of filesystem layers that comprise this image.
|
||||
// The order of the list is oldest/base layer first, and most-recent/top layer last.
|
||||
func (i *image) Layers() ([]v1.Layer, error) {
|
||||
if err := i.compute(); errors.Is(err, stream.ErrNotComputed) {
|
||||
// Image contains a streamable layer which has not yet been
|
||||
// consumed. Just return the layers we have in case the caller
|
||||
// is going to consume the layers.
|
||||
layers, err := i.base.Layers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, add := range i.adds {
|
||||
layers = append(layers, add.Layer)
|
||||
}
|
||||
return layers, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
diffIDs, err := partial.DiffIDs(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls := make([]v1.Layer, 0, len(diffIDs))
|
||||
for _, h := range diffIDs {
|
||||
l, err := i.LayerByDiffID(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls = append(ls, l)
|
||||
}
|
||||
return ls, nil
|
||||
}
|
||||
|
||||
// ConfigName returns the hash of the image's config file.
|
||||
func (i *image) ConfigName() (v1.Hash, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
return partial.ConfigName(i)
|
||||
}
|
||||
|
||||
// ConfigFile returns this image's config file.
|
||||
func (i *image) ConfigFile() (*v1.ConfigFile, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.configFile.DeepCopy(), nil
|
||||
}
|
||||
|
||||
// RawConfigFile returns the serialized bytes of ConfigFile()
|
||||
func (i *image) RawConfigFile() ([]byte, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(i.configFile)
|
||||
}
|
||||
|
||||
// Digest returns the sha256 of this image's manifest.
|
||||
func (i *image) Digest() (v1.Hash, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
return partial.Digest(i)
|
||||
}
|
||||
|
||||
// Size implements v1.Image.
|
||||
func (i *image) Size() (int64, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return partial.Size(i)
|
||||
}
|
||||
|
||||
// Manifest returns this image's Manifest object.
|
||||
func (i *image) Manifest() (*v1.Manifest, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.manifest.DeepCopy(), nil
|
||||
}
|
||||
|
||||
// RawManifest returns the serialized bytes of Manifest()
|
||||
func (i *image) RawManifest() ([]byte, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(i.manifest)
|
||||
}
|
||||
|
||||
// LayerByDigest returns a Layer for interacting with a particular layer of
|
||||
// the image, looking it up by "digest" (the compressed hash).
|
||||
func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) {
|
||||
if cn, err := i.ConfigName(); err != nil {
|
||||
return nil, err
|
||||
} else if h == cn {
|
||||
return partial.ConfigLayer(i)
|
||||
}
|
||||
if layer, ok := i.digestMap[h]; ok {
|
||||
return layer, nil
|
||||
}
|
||||
return i.base.LayerByDigest(h)
|
||||
}
|
||||
|
||||
// LayerByDiffID is an analog to LayerByDigest, looking up by "diff id"
|
||||
// (the uncompressed hash).
|
||||
func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) {
|
||||
if layer, ok := i.diffIDMap[h]; ok {
|
||||
return layer, nil
|
||||
}
|
||||
return i.base.LayerByDiffID(h)
|
||||
}
|
||||
|
||||
func validate(adds []Addendum) error {
|
||||
for _, add := range adds {
|
||||
if add.Layer == nil && !add.History.EmptyLayer {
|
||||
return errors.New("unable to add a nil layer to the image")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-232
@@ -1,232 +0,0 @@
|
||||
// Copyright 2019 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mutate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/logs"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/match"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/stream"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
func computeDescriptor(ia IndexAddendum) (*v1.Descriptor, error) {
|
||||
desc, err := partial.Descriptor(ia.Add)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The IndexAddendum allows overriding Descriptor values.
|
||||
if ia.Size != 0 {
|
||||
desc.Size = ia.Size
|
||||
}
|
||||
if string(ia.MediaType) != "" {
|
||||
desc.MediaType = ia.MediaType
|
||||
}
|
||||
if ia.Digest != (v1.Hash{}) {
|
||||
desc.Digest = ia.Digest
|
||||
}
|
||||
if ia.Platform != nil {
|
||||
desc.Platform = ia.Platform
|
||||
}
|
||||
if len(ia.URLs) != 0 {
|
||||
desc.URLs = ia.URLs
|
||||
}
|
||||
if len(ia.Annotations) != 0 {
|
||||
desc.Annotations = ia.Annotations
|
||||
}
|
||||
if ia.Data != nil {
|
||||
desc.Data = ia.Data
|
||||
}
|
||||
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
type index struct {
|
||||
base v1.ImageIndex
|
||||
adds []IndexAddendum
|
||||
// remove is removed before adds
|
||||
remove match.Matcher
|
||||
|
||||
computed bool
|
||||
manifest *v1.IndexManifest
|
||||
annotations map[string]string
|
||||
mediaType *types.MediaType
|
||||
imageMap map[v1.Hash]v1.Image
|
||||
indexMap map[v1.Hash]v1.ImageIndex
|
||||
layerMap map[v1.Hash]v1.Layer
|
||||
subject *v1.Descriptor
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
var _ v1.ImageIndex = (*index)(nil)
|
||||
|
||||
func (i *index) MediaType() (types.MediaType, error) {
|
||||
if i.mediaType != nil {
|
||||
return *i.mediaType, nil
|
||||
}
|
||||
return i.base.MediaType()
|
||||
}
|
||||
|
||||
func (i *index) Size() (int64, error) { return partial.Size(i) }
|
||||
|
||||
func (i *index) compute() error {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
// Don't re-compute if already computed.
|
||||
if i.computed {
|
||||
return nil
|
||||
}
|
||||
|
||||
i.imageMap = make(map[v1.Hash]v1.Image)
|
||||
i.indexMap = make(map[v1.Hash]v1.ImageIndex)
|
||||
i.layerMap = make(map[v1.Hash]v1.Layer)
|
||||
|
||||
m, err := i.base.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest := m.DeepCopy()
|
||||
manifests := manifest.Manifests
|
||||
|
||||
if i.remove != nil {
|
||||
var cleanedManifests []v1.Descriptor
|
||||
for _, m := range manifests {
|
||||
if !i.remove(m) {
|
||||
cleanedManifests = append(cleanedManifests, m)
|
||||
}
|
||||
}
|
||||
manifests = cleanedManifests
|
||||
}
|
||||
|
||||
for _, add := range i.adds {
|
||||
desc, err := computeDescriptor(add)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifests = append(manifests, *desc)
|
||||
if idx, ok := add.Add.(v1.ImageIndex); ok {
|
||||
i.indexMap[desc.Digest] = idx
|
||||
} else if img, ok := add.Add.(v1.Image); ok {
|
||||
i.imageMap[desc.Digest] = img
|
||||
} else if l, ok := add.Add.(v1.Layer); ok {
|
||||
i.layerMap[desc.Digest] = l
|
||||
} else {
|
||||
logs.Warn.Printf("Unexpected index addendum: %T", add.Add)
|
||||
}
|
||||
}
|
||||
|
||||
manifest.Manifests = manifests
|
||||
|
||||
if i.mediaType != nil {
|
||||
manifest.MediaType = *i.mediaType
|
||||
}
|
||||
|
||||
if i.annotations != nil {
|
||||
if manifest.Annotations == nil {
|
||||
manifest.Annotations = map[string]string{}
|
||||
}
|
||||
for k, v := range i.annotations {
|
||||
manifest.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
manifest.Subject = i.subject
|
||||
|
||||
i.manifest = manifest
|
||||
i.computed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *index) Image(h v1.Hash) (v1.Image, error) {
|
||||
if img, ok := i.imageMap[h]; ok {
|
||||
return img, nil
|
||||
}
|
||||
return i.base.Image(h)
|
||||
}
|
||||
|
||||
func (i *index) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
|
||||
if idx, ok := i.indexMap[h]; ok {
|
||||
return idx, nil
|
||||
}
|
||||
return i.base.ImageIndex(h)
|
||||
}
|
||||
|
||||
type withLayer interface {
|
||||
Layer(v1.Hash) (v1.Layer, error)
|
||||
}
|
||||
|
||||
// Workaround for #819.
|
||||
func (i *index) Layer(h v1.Hash) (v1.Layer, error) {
|
||||
if layer, ok := i.layerMap[h]; ok {
|
||||
return layer, nil
|
||||
}
|
||||
if wl, ok := i.base.(withLayer); ok {
|
||||
return wl.Layer(h)
|
||||
}
|
||||
return nil, fmt.Errorf("layer not found: %s", h)
|
||||
}
|
||||
|
||||
// Digest returns the sha256 of this image's manifest.
|
||||
func (i *index) Digest() (v1.Hash, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
return partial.Digest(i)
|
||||
}
|
||||
|
||||
// Manifest returns this image's Manifest object.
|
||||
func (i *index) IndexManifest() (*v1.IndexManifest, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.manifest.DeepCopy(), nil
|
||||
}
|
||||
|
||||
// RawManifest returns the serialized bytes of Manifest()
|
||||
func (i *index) RawManifest() ([]byte, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(i.manifest)
|
||||
}
|
||||
|
||||
func (i *index) Manifests() ([]partial.Describable, error) {
|
||||
if err := i.compute(); errors.Is(err, stream.ErrNotComputed) {
|
||||
// Index contains a streamable layer which has not yet been
|
||||
// consumed. Just return the manifests we have in case the caller
|
||||
// is going to consume the streamable layers.
|
||||
manifests, err := partial.Manifests(i.base)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, add := range i.adds {
|
||||
manifests = append(manifests, add.Add)
|
||||
}
|
||||
return manifests, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return partial.ComputeManifests(i)
|
||||
}
|
||||
-546
@@ -1,546 +0,0 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mutate
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/gzip"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/match"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
const whiteoutPrefix = ".wh."
|
||||
|
||||
// Addendum contains layers and history to be appended
|
||||
// to a base image
|
||||
type Addendum struct {
|
||||
Layer v1.Layer
|
||||
History v1.History
|
||||
URLs []string
|
||||
Annotations map[string]string
|
||||
MediaType types.MediaType
|
||||
}
|
||||
|
||||
// AppendLayers applies layers to a base image.
|
||||
func AppendLayers(base v1.Image, layers ...v1.Layer) (v1.Image, error) {
|
||||
additions := make([]Addendum, 0, len(layers))
|
||||
for _, layer := range layers {
|
||||
additions = append(additions, Addendum{Layer: layer})
|
||||
}
|
||||
|
||||
return Append(base, additions...)
|
||||
}
|
||||
|
||||
// Append will apply the list of addendums to the base image
|
||||
func Append(base v1.Image, adds ...Addendum) (v1.Image, error) {
|
||||
if len(adds) == 0 {
|
||||
return base, nil
|
||||
}
|
||||
if err := validate(adds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &image{
|
||||
base: base,
|
||||
adds: adds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Appendable is an interface that represents something that can be appended
|
||||
// to an ImageIndex. We need to be able to construct a v1.Descriptor in order
|
||||
// to append something, and this is the minimum required information for that.
|
||||
type Appendable interface {
|
||||
MediaType() (types.MediaType, error)
|
||||
Digest() (v1.Hash, error)
|
||||
Size() (int64, error)
|
||||
}
|
||||
|
||||
// IndexAddendum represents an appendable thing and all the properties that
|
||||
// we may want to override in the resulting v1.Descriptor.
|
||||
type IndexAddendum struct {
|
||||
Add Appendable
|
||||
v1.Descriptor
|
||||
}
|
||||
|
||||
// AppendManifests appends a manifest to the ImageIndex.
|
||||
func AppendManifests(base v1.ImageIndex, adds ...IndexAddendum) v1.ImageIndex {
|
||||
return &index{
|
||||
base: base,
|
||||
adds: adds,
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveManifests removes any descriptors that match the match.Matcher.
|
||||
func RemoveManifests(base v1.ImageIndex, matcher match.Matcher) v1.ImageIndex {
|
||||
return &index{
|
||||
base: base,
|
||||
remove: matcher,
|
||||
}
|
||||
}
|
||||
|
||||
// Config mutates the provided v1.Image to have the provided v1.Config
|
||||
func Config(base v1.Image, cfg v1.Config) (v1.Image, error) {
|
||||
cf, err := base.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cf.Config = cfg
|
||||
|
||||
return ConfigFile(base, cf)
|
||||
}
|
||||
|
||||
// Subject mutates the subject on an image or index manifest.
|
||||
//
|
||||
// The input is expected to be a v1.Image or v1.ImageIndex, and
|
||||
// returns the same type. You can type-assert the result like so:
|
||||
//
|
||||
// img := Subject(empty.Image, subj).(v1.Image)
|
||||
//
|
||||
// Or for an index:
|
||||
//
|
||||
// idx := Subject(empty.Index, subj).(v1.ImageIndex)
|
||||
//
|
||||
// If the input is not an Image or ImageIndex, the result will
|
||||
// attempt to lazily annotate the raw manifest.
|
||||
func Subject(f partial.WithRawManifest, subject v1.Descriptor) partial.WithRawManifest {
|
||||
if img, ok := f.(v1.Image); ok {
|
||||
return &image{
|
||||
base: img,
|
||||
subject: &subject,
|
||||
}
|
||||
}
|
||||
if idx, ok := f.(v1.ImageIndex); ok {
|
||||
return &index{
|
||||
base: idx,
|
||||
subject: &subject,
|
||||
}
|
||||
}
|
||||
return arbitraryRawManifest{a: f, subject: &subject}
|
||||
}
|
||||
|
||||
// Annotations mutates the annotations on an annotatable image or index manifest.
|
||||
//
|
||||
// The annotatable input is expected to be a v1.Image or v1.ImageIndex, and
|
||||
// returns the same type. You can type-assert the result like so:
|
||||
//
|
||||
// img := Annotations(empty.Image, map[string]string{
|
||||
// "foo": "bar",
|
||||
// }).(v1.Image)
|
||||
//
|
||||
// Or for an index:
|
||||
//
|
||||
// idx := Annotations(empty.Index, map[string]string{
|
||||
// "foo": "bar",
|
||||
// }).(v1.ImageIndex)
|
||||
//
|
||||
// If the input Annotatable is not an Image or ImageIndex, the result will
|
||||
// attempt to lazily annotate the raw manifest.
|
||||
func Annotations(f partial.WithRawManifest, anns map[string]string) partial.WithRawManifest {
|
||||
if img, ok := f.(v1.Image); ok {
|
||||
return &image{
|
||||
base: img,
|
||||
annotations: maps.Clone(anns),
|
||||
}
|
||||
}
|
||||
if idx, ok := f.(v1.ImageIndex); ok {
|
||||
return &index{
|
||||
base: idx,
|
||||
annotations: maps.Clone(anns),
|
||||
}
|
||||
}
|
||||
return arbitraryRawManifest{a: f, anns: maps.Clone(anns)}
|
||||
}
|
||||
|
||||
type arbitraryRawManifest struct {
|
||||
a partial.WithRawManifest
|
||||
anns map[string]string
|
||||
subject *v1.Descriptor
|
||||
}
|
||||
|
||||
func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
|
||||
b, err := a.a.RawManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ann, ok := m["annotations"]; ok {
|
||||
if annm, ok := ann.(map[string]string); ok {
|
||||
for k, v := range a.anns {
|
||||
annm[k] = v
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf(".annotations is not a map: %T", ann)
|
||||
}
|
||||
} else {
|
||||
m["annotations"] = a.anns
|
||||
}
|
||||
if a.subject != nil {
|
||||
m["subject"] = a.subject
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// ConfigFile mutates the provided v1.Image to have the provided v1.ConfigFile
|
||||
func ConfigFile(base v1.Image, cfg *v1.ConfigFile) (v1.Image, error) {
|
||||
m, err := base.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
image := &image{
|
||||
base: base,
|
||||
manifest: m.DeepCopy(),
|
||||
configFile: cfg,
|
||||
}
|
||||
|
||||
return image, nil
|
||||
}
|
||||
|
||||
// CreatedAt mutates the provided v1.Image to have the provided v1.Time
|
||||
func CreatedAt(base v1.Image, created v1.Time) (v1.Image, error) {
|
||||
cf, err := base.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := cf.DeepCopy()
|
||||
cfg.Created = created
|
||||
|
||||
return ConfigFile(base, cfg)
|
||||
}
|
||||
|
||||
// Extract takes an image and returns an io.ReadCloser containing the image's
|
||||
// flattened filesystem.
|
||||
//
|
||||
// Callers can read the filesystem contents by passing the reader to
|
||||
// tar.NewReader, or io.Copy it directly to some output.
|
||||
//
|
||||
// If a caller doesn't read the full contents, they should Close it to free up
|
||||
// resources used during extraction.
|
||||
func Extract(img v1.Image) io.ReadCloser {
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
go func() {
|
||||
// Close the writer with any errors encountered during
|
||||
// extraction. These errors will be returned by the reader end
|
||||
// on subsequent reads. If err == nil, the reader will return
|
||||
// EOF.
|
||||
pw.CloseWithError(extract(img, pw))
|
||||
}()
|
||||
|
||||
return pr
|
||||
}
|
||||
|
||||
// Adapted from https://github.com/google/containerregistry/blob/da03b395ccdc4e149e34fbb540483efce962dc64/client/v2_2/docker_image_.py#L816
|
||||
func extract(img v1.Image, w io.Writer) error {
|
||||
tarWriter := tar.NewWriter(w)
|
||||
defer tarWriter.Close()
|
||||
|
||||
fileMap := map[string]bool{}
|
||||
|
||||
layers, err := img.Layers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving image layers: %w", err)
|
||||
}
|
||||
|
||||
// we iterate through the layers in reverse order because it makes handling
|
||||
// whiteout layers more efficient, since we can just keep track of the removed
|
||||
// files as we see .wh. layers and ignore those in previous layers.
|
||||
for i := len(layers) - 1; i >= 0; i-- {
|
||||
layer := layers[i]
|
||||
layerReader, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading layer contents: %w", err)
|
||||
}
|
||||
defer layerReader.Close()
|
||||
tarReader := tar.NewReader(layerReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading tar: %w", err)
|
||||
}
|
||||
|
||||
// Some tools prepend everything with "./", so if we don't Clean the
|
||||
// name, we may have duplicate entries, which angers tar-split.
|
||||
header.Name = filepath.Clean(header.Name)
|
||||
// force PAX format to remove Name/Linkname length limit of 100 characters
|
||||
// required by USTAR and to not depend on internal tar package guess which
|
||||
// prefers USTAR over PAX
|
||||
header.Format = tar.FormatPAX
|
||||
|
||||
basename := filepath.Base(header.Name)
|
||||
dirname := filepath.Dir(header.Name)
|
||||
tombstone := strings.HasPrefix(basename, whiteoutPrefix)
|
||||
if tombstone {
|
||||
basename = basename[len(whiteoutPrefix):]
|
||||
}
|
||||
|
||||
// check if we have seen value before
|
||||
// if we're checking a directory, don't filepath.Join names
|
||||
var name string
|
||||
if header.Typeflag == tar.TypeDir {
|
||||
name = header.Name
|
||||
} else {
|
||||
name = filepath.Join(dirname, basename)
|
||||
}
|
||||
|
||||
if _, ok := fileMap[name]; ok && !tombstone {
|
||||
continue
|
||||
}
|
||||
|
||||
// check for a whited out parent directory
|
||||
if inWhiteoutDir(fileMap, name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// mark file as handled. non-directory implicitly tombstones
|
||||
// any entries with a matching (or child) name
|
||||
fileMap[name] = tombstone || (header.Typeflag != tar.TypeDir)
|
||||
if !tombstone {
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
if header.Size > 0 {
|
||||
if _, err := io.CopyN(tarWriter, tarReader, header.Size); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func inWhiteoutDir(fileMap map[string]bool, file string) bool {
|
||||
for file != "" {
|
||||
dirname := filepath.Dir(file)
|
||||
if file == dirname {
|
||||
break
|
||||
}
|
||||
if val, ok := fileMap[dirname]; ok && val {
|
||||
return true
|
||||
}
|
||||
file = dirname
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Time sets all timestamps in an image to the given timestamp.
|
||||
func Time(img v1.Image, t time.Time) (v1.Image, error) {
|
||||
newImage := empty.Image
|
||||
|
||||
layers, err := img.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting image layers: %w", err)
|
||||
}
|
||||
|
||||
ocf, err := img.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting original config file: %w", err)
|
||||
}
|
||||
|
||||
addendums := make([]Addendum, max(len(ocf.History), len(layers)))
|
||||
var historyIdx, addendumIdx int
|
||||
for layerIdx := 0; layerIdx < len(layers); addendumIdx, layerIdx = addendumIdx+1, layerIdx+1 {
|
||||
newLayer, err := layerTime(layers[layerIdx], t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting layer times: %w", err)
|
||||
}
|
||||
|
||||
// try to search for the history entry that corresponds to this layer
|
||||
for ; historyIdx < len(ocf.History); historyIdx++ {
|
||||
addendums[addendumIdx].History = ocf.History[historyIdx]
|
||||
// if it's an EmptyLayer, do not set the Layer and have the Addendum with just the History
|
||||
// and move on to the next History entry
|
||||
if ocf.History[historyIdx].EmptyLayer {
|
||||
addendumIdx++
|
||||
continue
|
||||
}
|
||||
// otherwise, we can exit from the cycle
|
||||
historyIdx++
|
||||
break
|
||||
}
|
||||
if addendumIdx < len(addendums) {
|
||||
addendums[addendumIdx].Layer = newLayer
|
||||
}
|
||||
}
|
||||
|
||||
// add all leftover History entries
|
||||
for ; historyIdx < len(ocf.History); historyIdx, addendumIdx = historyIdx+1, addendumIdx+1 {
|
||||
addendums[addendumIdx].History = ocf.History[historyIdx]
|
||||
}
|
||||
|
||||
newImage, err = Append(newImage, addendums...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("appending layers: %w", err)
|
||||
}
|
||||
|
||||
cf, err := newImage.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting config file: %w", err)
|
||||
}
|
||||
|
||||
cfg := cf.DeepCopy()
|
||||
|
||||
// Copy basic config over
|
||||
cfg.Architecture = ocf.Architecture
|
||||
cfg.OS = ocf.OS
|
||||
cfg.OSVersion = ocf.OSVersion
|
||||
cfg.Config = ocf.Config
|
||||
|
||||
// Strip away timestamps from the config file
|
||||
cfg.Created = v1.Time{Time: t}
|
||||
|
||||
for i, h := range cfg.History {
|
||||
h.Created = v1.Time{Time: t}
|
||||
h.CreatedBy = ocf.History[i].CreatedBy
|
||||
h.Comment = ocf.History[i].Comment
|
||||
h.EmptyLayer = ocf.History[i].EmptyLayer
|
||||
// Explicitly ignore Author field; which hinders reproducibility
|
||||
h.Author = ""
|
||||
cfg.History[i] = h
|
||||
}
|
||||
|
||||
return ConfigFile(newImage, cfg)
|
||||
}
|
||||
|
||||
func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
|
||||
layerReader, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting layer: %w", err)
|
||||
}
|
||||
defer layerReader.Close()
|
||||
w := new(bytes.Buffer)
|
||||
tarWriter := tar.NewWriter(w)
|
||||
defer tarWriter.Close()
|
||||
|
||||
tarReader := tar.NewReader(layerReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading layer: %w", err)
|
||||
}
|
||||
|
||||
header.ModTime = t
|
||||
|
||||
//PAX and GNU Format support additional timestamps in the header
|
||||
if header.Format == tar.FormatPAX || header.Format == tar.FormatGNU {
|
||||
header.AccessTime = t
|
||||
header.ChangeTime = t
|
||||
}
|
||||
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return nil, fmt.Errorf("writing tar header: %w", err)
|
||||
}
|
||||
|
||||
if header.Typeflag == tar.TypeReg {
|
||||
// TODO(#1168): This should be lazy, and not buffer the entire layer contents.
|
||||
if _, err = io.CopyN(tarWriter, tarReader, header.Size); err != nil {
|
||||
return nil, fmt.Errorf("writing layer file: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tarWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := w.Bytes()
|
||||
// gzip the contents, then create the layer
|
||||
opener := func() (io.ReadCloser, error) {
|
||||
return gzip.ReadCloser(io.NopCloser(bytes.NewReader(b))), nil
|
||||
}
|
||||
layer, err = tarball.LayerFromOpener(opener)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating layer: %w", err)
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
}
|
||||
|
||||
// Canonical is a helper function to combine Time and configFile
|
||||
// to remove any randomness during a docker build.
|
||||
func Canonical(img v1.Image) (v1.Image, error) {
|
||||
// Set all timestamps to 0
|
||||
created := time.Time{}
|
||||
img, err := Time(img, created)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cf, err := img.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get rid of host-dependent random config
|
||||
cfg := cf.DeepCopy()
|
||||
|
||||
cfg.Container = ""
|
||||
cfg.Config.Hostname = ""
|
||||
cfg.DockerVersion = "" //nolint:staticcheck // Field will be removed in next release
|
||||
|
||||
return ConfigFile(img, cfg)
|
||||
}
|
||||
|
||||
// MediaType modifies the MediaType() of the given image.
|
||||
func MediaType(img v1.Image, mt types.MediaType) v1.Image {
|
||||
return &image{
|
||||
base: img,
|
||||
mediaType: &mt,
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigMediaType modifies the MediaType() of the given image's Config.
|
||||
//
|
||||
// If !mt.IsConfig(), this will be the image's artifactType in any indexes it's a part of.
|
||||
func ConfigMediaType(img v1.Image, mt types.MediaType) v1.Image {
|
||||
return &image{
|
||||
base: img,
|
||||
configMediaType: &mt,
|
||||
}
|
||||
}
|
||||
|
||||
// IndexMediaType modifies the MediaType() of the given index.
|
||||
func IndexMediaType(idx v1.ImageIndex, mt types.MediaType) v1.ImageIndex {
|
||||
return &index{
|
||||
base: idx,
|
||||
mediaType: &mt,
|
||||
}
|
||||
}
|
||||
-144
@@ -1,144 +0,0 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mutate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
)
|
||||
|
||||
// Rebase returns a new v1.Image where the oldBase in orig is replaced by newBase.
|
||||
func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
|
||||
// Verify that oldBase's layers are present in orig, otherwise orig is
|
||||
// not based on oldBase at all.
|
||||
origLayers, err := orig.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layers for original: %w", err)
|
||||
}
|
||||
oldBaseLayers, err := oldBase.Layers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(oldBaseLayers) > len(origLayers) {
|
||||
return nil, fmt.Errorf("image %q is not based on %q (too few layers)", orig, oldBase)
|
||||
}
|
||||
for i, l := range oldBaseLayers {
|
||||
oldLayerDigest, err := l.Digest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, oldBase, err)
|
||||
}
|
||||
origLayerDigest, err := origLayers[i].Digest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, orig, err)
|
||||
}
|
||||
if oldLayerDigest != origLayerDigest {
|
||||
return nil, fmt.Errorf("image %q is not based on %q (layer %d mismatch)", orig, oldBase, i)
|
||||
}
|
||||
}
|
||||
|
||||
oldConfig, err := oldBase.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config for old base: %w", err)
|
||||
}
|
||||
|
||||
origConfig, err := orig.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config for original: %w", err)
|
||||
}
|
||||
|
||||
newConfig, err := newBase.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get config for new base: %w", err)
|
||||
}
|
||||
|
||||
// Stitch together an image that contains:
|
||||
// - original image's config
|
||||
// - new base image's os/arch properties
|
||||
// - new base image's layers + top of original image's layers
|
||||
// - new base image's history + top of original image's history
|
||||
rebasedImage, err := Config(empty.Image, *origConfig.Config.DeepCopy())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create empty image with original config: %w", err)
|
||||
}
|
||||
|
||||
// Add new config properties from existing images.
|
||||
rebasedConfig, err := rebasedImage.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get config for rebased image: %w", err)
|
||||
}
|
||||
// OS/Arch properties from new base
|
||||
rebasedConfig.Architecture = newConfig.Architecture
|
||||
rebasedConfig.OS = newConfig.OS
|
||||
rebasedConfig.OSVersion = newConfig.OSVersion
|
||||
|
||||
// Apply config properties to rebased.
|
||||
rebasedImage, err = ConfigFile(rebasedImage, rebasedConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to replace config for rebased image: %w", err)
|
||||
}
|
||||
|
||||
// Get new base layers and config for history.
|
||||
newBaseLayers, err := newBase.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get new base layers for new base: %w", err)
|
||||
}
|
||||
// Add new base layers.
|
||||
rebasedImage, err = Append(rebasedImage, createAddendums(0, 0, newConfig.History, newBaseLayers)...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append new base image: %w", err)
|
||||
}
|
||||
|
||||
// Add original layers above the old base.
|
||||
rebasedImage, err = Append(rebasedImage, createAddendums(len(oldConfig.History), len(oldBaseLayers)+1, origConfig.History, origLayers)...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append original image: %w", err)
|
||||
}
|
||||
|
||||
return rebasedImage, nil
|
||||
}
|
||||
|
||||
// createAddendums makes a list of addendums from a history and layers starting from a specific history and layer
|
||||
// indexes.
|
||||
func createAddendums(startHistory, startLayer int, history []v1.History, layers []v1.Layer) []Addendum {
|
||||
var adds []Addendum
|
||||
// History should be a superset of layers; empty layers (e.g. ENV statements) only exist in history.
|
||||
// They cannot be iterated identically but must be walked independently, only advancing the iterator for layers
|
||||
// when a history entry for a non-empty layer is seen.
|
||||
layerIndex := 0
|
||||
for historyIndex := range history {
|
||||
var layer v1.Layer
|
||||
emptyLayer := history[historyIndex].EmptyLayer
|
||||
if !emptyLayer {
|
||||
layer = layers[layerIndex]
|
||||
layerIndex++
|
||||
}
|
||||
if historyIndex >= startHistory || layerIndex >= startLayer {
|
||||
adds = append(adds, Addendum{
|
||||
Layer: layer,
|
||||
History: history[historyIndex],
|
||||
})
|
||||
}
|
||||
}
|
||||
// In the event history was malformed or non-existent, append the remaining layers.
|
||||
for i := layerIndex; i < len(layers); i++ {
|
||||
if i >= startLayer {
|
||||
adds = append(adds, Addendum{Layer: layers[layerIndex]})
|
||||
}
|
||||
}
|
||||
|
||||
return adds
|
||||
}
|
||||
Reference in New Issue
Block a user