Skip to content

Commit

Permalink
Implement new interface for legacy intermediate format layouts (bazel…
Browse files Browse the repository at this point in the history
…build#954)

* Implement new interface for MM format images

* Add documentation and reformat code

* Added copyright headers

* Add header in the BUILD file

* Fix BUILD file header

* Restyle with go tools

* Refactor files paths usages

* Add lock protection for raw manifest

* Implement pusher to incorporate new legacy format

* Add more comments

* Add comments for legacy reader

* Reformat comments

* Refactor code after review

* Fixed typo in code

* Refactor code after comments

* Refactor input file directory trimming

* Refactor src path checking
  • Loading branch information
xiaohegong authored and k8s-ci-robot committed Jul 8, 2019
1 parent a84ff53 commit b190dfc
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 12 deletions.
1 change: 1 addition & 0 deletions container/go/cmd/pusher/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ go_library(
importpath = "github.com/bazelbuild/rules_docker",
visibility = ["//visibility:private"],
deps = [
"//container/go/pkg/compat:go_default_library",
"//container/go/pkg/oci:go_default_library",
"@com_github_google_go_containerregistry//pkg/authn:go_default_library",
"@com_github_google_go_containerregistry//pkg/name:go_default_library",
Expand Down
41 changes: 30 additions & 11 deletions container/go/cmd/pusher/pusher.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"os"
"path/filepath"

"github.com/bazelbuild/rules_docker/container/go/pkg/compat"
"github.com/bazelbuild/rules_docker/container/go/pkg/oci"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
Expand All @@ -32,8 +33,8 @@ import (

var (
dst = flag.String("dst", "", "The destination location including repo and digest/tag of the docker image to push. Supports fully-qualified tag or digest references.")
src = flag.String("src", "", "Path to the directory which has the image index that will be pushed.")
format = flag.String("format", "", "The format of the source image, oci|docker. The docker format should be a tarball of the image as generated by docker save.")
src = flag.String("src", "", "Path to the manifest.json when -format is legacy, path to the index.json when -format is oci or path to the image .tar file when -format is docker.")
format = flag.String("format", "", "The format of the source image, (oci, legacy, or docker). The docker format should be a tarball of the image as generated by docker save.")
clientConfigDir = flag.String("client-config-dir", "", "The path to the directory where the client configuration files are located. Overiddes the value from DOCKER_CONFIG.")
)

Expand All @@ -57,26 +58,40 @@ func main() {
os.Setenv("DOCKER_CONFIG", *clientConfigDir)
}

// Ensure src is a directory, trim basename index.json if not.
if filepath.Base(*src) == "index.json" {
*src = filepath.Dir(*src)
// Validates provided format and src path. Check if src is a tarball when pushing a docker image. Trim basename index.json or manifest.json if src is a directory, since we are pushing a OCI/legacy index.
var imgSrc string
if *format == "docker" && filepath.Ext(*src) != ".tar" {
log.Fatalf("Invalid value for argument -src for -format=docker, got %q, want path to tarball file with extension .tar.", src)
}
if *format == "legacy" && filepath.Base(*src) != "manifest.json" {
log.Fatalf("Invalid value for argument -src for -format=legacy, got %q, want path to manifest.json", *src)
}
if *format == "oci" && filepath.Base(*src) != "index.json" {
log.Fatalf("Invalid value for argument -src for -format=oci, got %q, want path to index.json", *src)
}
if *format == "oci" || *format == "legacy" {
imgSrc = filepath.Dir(*src)
}
if *format == "docker" {
imgSrc = *src
}
log.Printf("Determined image source path to be %q based on -format=%q, -src=%q.", imgSrc, *format, *src)

img, err := readImage(*src, *format)
img, err := readImage(imgSrc, *format)
if err != nil {
log.Fatalf("Error reading from %s: %v", *src, err)
log.Fatalf("error reading from %s: %v", imgSrc, err)
}

if err := push(*dst, img); err != nil {
log.Fatalf("Error pushing image to %s: %v", *dst, err)
log.Fatalf("error pushing image to %s: %v", *dst, err)
}

log.Printf("Successfully pushed %s image from %s to %s", *format, *src, *dst)
log.Printf("Successfully pushed %s image from %s to %s", *format, imgSrc, *dst)
}

// push pushes the given image to the given destination.
// NOTE: This function is adapted from https://github.com/google/go-containerregistry/blob/master/pkg/crane/push.go
// with modification for option to push OCI layout or Docker tarball format .
// with modification for option to push OCI layout, legacy layout or Docker tarball format.
// Push the given image to destination <dst>.
func push(dst string, img v1.Image) error {
// Push the image to dst.
Expand All @@ -92,13 +107,17 @@ func push(dst string, img v1.Image) error {
return nil
}

// readImage returns a v1.Image after reading an OCI index or a Docker tarball from src.
// readImage returns a v1.Image after reading an legacy layout, an OCI layout or a Docker tarball from src.
func readImage(src, format string) (v1.Image, error) {
if format == "oci" {
return oci.Read(src)
}
if format == "legacy" {
return compat.Read(src)
}
if format == "docker" {
return tarball.ImageFromPath(src, nil)
}

return nil, errors.Errorf("unknown image format %q", format)
}
10 changes: 9 additions & 1 deletion container/go/pkg/compat/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,19 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
name = "go_default_library",
srcs = ["ociToLegacy.go"],
srcs = [
"image.go",
"ociToLegacy.go",
"path.go",
"reader.go",
],
importpath = "github.com/bazelbuild/rules_docker/container/go/pkg/compat",
visibility = ["//visibility:public"],
deps = [
"@com_github_google_go_containerregistry//pkg/v1:go_default_library",
"@com_github_google_go_containerregistry//pkg/v1/partial:go_default_library",
"@com_github_google_go_containerregistry//pkg/v1/types:go_default_library",
"@com_github_google_go_containerregistry//pkg/v1/validate:go_default_library",
"@com_github_pkg_errors//:go_default_library",
],
)
155 changes: 155 additions & 0 deletions container/go/pkg/compat/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/// Copyright 2015 The Bazel Authors. 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.
//////////////////////////////////////////////////////////////////////
// Image for intermediate format used in python containerregistry.
// Adopted from go-containerregistry's layout.image implementation with modification to understand rules_docker's legacy intermediate format.
// Uses the go-containerregistry API as backend.

package compat

import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sync"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/types"
)

// legacyImage is the image in legacy intermediate format. Implements v1.Image, and its implementation is very similar to layout.layoutImage.
type legacyImage struct {
// path is the path to the directory containing the legacy image.
path string
// digest is the sha256 hash for this image.
digest v1.Hash
// manifestLock protects rawManifest.
manifestLock sync.Mutex
// rawManifest is the raw bytes of manifest.json file.
rawManifest []byte
}

var _ partial.CompressedImageCore = (*legacyImage)(nil)

// MediaType of this image's manifest from manifest.json.
func (li *legacyImage) MediaType() (types.MediaType, error) {
manifest, err := li.Manifest()
if err != nil {
return "", err
}

if manifest.MediaType != types.OCIManifestSchema1 && manifest.MediaType != types.DockerManifestSchema2 {
return "", fmt.Errorf("unexpected media type for %v: %s", li.digest, manifest.MediaType)
}

return manifest.MediaType, nil
}

// Parses manifest.json into Manifest object. Implements WithManifest for partial.Blobset.
func (li *legacyImage) Manifest() (*v1.Manifest, error) {
return partial.Manifest(li)
}

// RawManifest returns the serialized bytes of manifest.json metadata.
func (li *legacyImage) RawManifest() ([]byte, error) {
li.manifestLock.Lock()
defer li.manifestLock.Unlock()

if li.rawManifest != nil {
return li.rawManifest, nil
}

// Read and store raw manifest.json file from src directory.
b, err := ioutil.ReadFile(filepath.Join(li.path, manifestFile))
if err != nil {
return nil, err
}

li.rawManifest = b
return li.rawManifest, nil
}

// RawConfigFile returns the serialized bytes of config.json metadata.
func (li *legacyImage) RawConfigFile() ([]byte, error) {
return ioutil.ReadFile(filepath.Join(li.path, configFile))
}

// LayerByDigest returns a Layer for interacting with a particular layer of the image, looking it up by "digest" (the compressed hash).
// We assume the layer files are named in the format of e.g., 000.tar.gz in this path, following the order they appear in manifest.json.
func (li *legacyImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
manifest, err := li.Manifest()
if err != nil {
return nil, err
}

// The config is a layer in some cases.
if h == manifest.Config.Digest {
return partial.CompressedLayer(&compressedBlob{
path: li.path,
desc: manifest.Config,
filename: "config.json",
}), nil
}

for i, desc := range manifest.Layers {
if h == desc.Digest {
switch desc.MediaType {
case types.OCILayer, types.DockerLayer:
return partial.CompressedToLayer(&compressedBlob{
path: li.path,
desc: desc,
filename: layerFilename(i),
})
default:
// TODO: We assume everything is a compressed blob, but that might not be true.
// TODO: Handle foreign layers.
return nil, fmt.Errorf("unexpected media type: %v for layer: %v", desc.MediaType, desc.Digest)
}
}
}

return nil, fmt.Errorf("could not find layer in image: %s", h)
}

type compressedBlob struct {
// path of this compressed blob.
path string
// desc is the descriptor of this compressed blob.
desc v1.Descriptor
// filename is the filename of this blob at the directory.
filename string
}

// The digest of this compressedBlob.
func (b *compressedBlob) Digest() (v1.Hash, error) {
return b.desc.Digest, nil
}

// Return and open a the layer file at path.
func (b *compressedBlob) Compressed() (io.ReadCloser, error) {
return os.Open(filepath.Join(b.path, b.filename))
}

// The size of this compressedBlob.
func (b *compressedBlob) Size() (int64, error) {
return b.desc.Size, nil
}

// The media type of this compressedBlob.
func (b *compressedBlob) MediaType() (types.MediaType, error) {
return b.desc.MediaType, nil
}
54 changes: 54 additions & 0 deletions container/go/pkg/compat/path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/// Copyright 2015 The Bazel Authors. 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.
//////////////////////////////////////////////////////////////////////
// Path utils used for legacy image layout outputted by python containerregistry.
// Uses the go-containerregistry API as backend.

package compat

import (
"fmt"
"os"
"path/filepath"
)

// Expected metadata files in legacy layout.
const (
manifestFile = "manifest.json"
configFile = "config.json"
digestFile = "digest"
)

// Return the filename for layer at index i in the layers array in manifest.json.
// Assume the layers are padded to three digits, e.g., the first layer is named 000.tar.gz.
func layerFilename(i int) string {
return fmt.Sprintf("%03d.tar.gz", i)
}

// Naively validates a legacy intermediate layout at <path> by checking if digest, config.json, and manifest.json all exist.
func isValidLegacylayout(path string) (bool, error) {
if _, err := os.Stat(filepath.Join(path, manifestFile)); err != nil {
return false, err
}

if _, err := os.Stat(filepath.Join(path, configFile)); err != nil {
return false, err
}

if _, err := os.Stat(filepath.Join(path, digestFile)); err != nil {
return false, err
}

return true, nil
}
69 changes: 69 additions & 0 deletions container/go/pkg/compat/reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2015 The Bazel Authors. 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.
//////////////////////////////////////////////////////////////////////
// Reads an legacy image layout on disk.
package compat

import (
"fmt"
"io/ioutil"
"path/filepath"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/validate"
"github.com/pkg/errors"
)

// Read returns a docker image referenced by the legacy intermediate layout at src. The image index should have been outputted by container_pull.
// NOTE: this only reads index with a single image.
func Read(src string) (v1.Image, error) {
_, err := isValidLegacylayout(src)
if err != nil {
return nil, errors.Wrapf(err, "invalid legacy layout at %s, requires manifest.json, config.json and digest files", src)
}

digest, err := getManifestDigest(src)
if err != nil {
return nil, errors.Wrapf(err, "unable to get manifest digest from %s", src)
}

// Constructs and validates a v1.Image object.
legacyImg := &legacyImage{
path: src,
digest: digest,
}

img, err := partial.CompressedToImage(legacyImg)
if err != nil {
return nil, errors.Wrapf(err, "unable to load image with digest %s obtained from the manifest at %s", digest, src)
}

if err := validate.Image(img); err != nil {
return nil, errors.Wrapf(err, "unable to load image with digest %s due to invalid legacy layout format from %s", digest, src)
}

return img, nil
}

// Get the hash of the image to read at <path> from digest file.
func getManifestDigest(path string) (v1.Hash, error) {
// We expect a file named digest that stores the manifest's hash formatted as sha256:{Hash} in this directory.
digest, err := ioutil.ReadFile(filepath.Join(path, digestFile))
if err != nil {
return v1.Hash{}, fmt.Errorf("failed to locate SHA256 digest file for image manifest: %v", err)
}

return v1.NewHash(string(digest))
}
Loading

0 comments on commit b190dfc

Please sign in to comment.