Skip to content

Commit

Permalink
Implement index manifest collection for the containerd hostfs image t…
Browse files Browse the repository at this point in the history
…ype (#133)

* Implement index manifest collection for the containerd hostfs image type
  • Loading branch information
domust committed May 10, 2023
1 parent 2206444 commit e3cbe6d
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 58 deletions.
5 changes: 5 additions & 0 deletions Dockerfile.imgcollector.tilt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM alpine:3.17

COPY ./bin/castai-imgcollector /usr/local/bin/castai-imgcollector

CMD ["/usr/local/bin/castai-imgcollector"]
4 changes: 3 additions & 1 deletion Dockerfile.mockapi
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
FROM gcr.io/distroless/static-debian11
FROM alpine:3.17

COPY ./bin/mockapi /usr/local/bin/mockapi

CMD ["/usr/local/bin/mockapi"]
4 changes: 2 additions & 2 deletions Tiltfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
if config.tilt_subcommand == "down":
fail("consider using `kubectl delete ns castai-sec")
fail("consider using `kubectl delete ns kvisor")

load('ext://restart_process', 'docker_build_with_restart')
load('ext://namespace', 'namespace_create')
Expand Down Expand Up @@ -44,7 +44,7 @@ local_resource(

local_resource(
'imgcollector-docker-build',
'docker build -t localhost:5000/kvisor-imgcollector . -f Dockerfile.imgcollector && docker push localhost:5000/kvisor-imgcollector',
'docker build -t localhost:5000/kvisor-imgcollector . -f Dockerfile.imgcollector.tilt && docker push localhost:5000/kvisor-imgcollector',
deps=[
'./bin/castai-imgcollector'
],
Expand Down
7 changes: 5 additions & 2 deletions castai/imagemeta_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ type ImageMetadata struct {
ResourceIDs []string `json:"resourceIDs,omitempty"`
BlobsInfo []types.BlobInfo `json:"blobsInfo,omitempty"`
ConfigFile *v1.ConfigFile `json:"configFile,omitempty"`
Manifest *v1.Manifest `json:"manifest,omitempty"`
OsInfo *OsInfo `json:"osInfo,omitempty"`
// Manifest specification can be found here: https://github.com/opencontainers/image-spec/blob/main/manifest.md
Manifest *v1.Manifest `json:"manifest,omitempty"`
// Index specification can be found here: https://github.com/opencontainers/image-spec/blob/main/image-index.md
Index *v1.IndexManifest `json:"index,omitempty"`
OsInfo *OsInfo `json:"osInfo,omitempty"`
}

// nolint:musttag
Expand Down
14 changes: 10 additions & 4 deletions cmd/imgcollector/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@ func (c *Collector) Collect(ctx context.Context) error {
if err != nil {
return err
}

manifest, err := img.Manifest()
if err != nil {
return fmt.Errorf("extract manifest: %w", err)
}

if err := c.client.SendImageMetadata(ctx, &castai.ImageMetadata{
metadata := &castai.ImageMetadata{
ImageName: c.cfg.ImageName,
ImageID: c.cfg.ImageID,
ResourceIDs: strings.Split(c.cfg.ResourceIDs, ","),
Expand All @@ -121,14 +121,20 @@ func (c *Collector) Collect(ctx context.Context) error {
ArtifactInfo: arRef.ArtifactInfo,
OS: arRef.OsInfo,
},
}); err != nil {
}

if index := img.Index(); index != nil {
metadata.Index = index
}

if err := c.client.SendImageMetadata(ctx, metadata); err != nil {
return err
}

return nil
}

func (c *Collector) getImage(ctx context.Context) (image.Image, func(), error) {
func (c *Collector) getImage(ctx context.Context) (image.ImageWithIndex, func(), error) {
imgRef, err := name.ParseReference(c.cfg.ImageName)
if err != nil {
return nil, nil, err
Expand Down
3 changes: 1 addition & 2 deletions cmd/imgcollector/image/blob.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package image

import (
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/castai/kvisor/cmd/imgcollector/image/hostfs"
)

func NewFromContainerdHostFS(imageID string, config hostfs.ContainerdHostFSConfig) (types.Image, func(), error) {
func NewFromContainerdHostFS(imageID string, config hostfs.ContainerdHostFSConfig) (ImageWithIndex, func(), error) {
hash, err := hostfs.NewImageHash(imageID)
if err != nil {
return nil, nil, err
Expand Down
9 changes: 3 additions & 6 deletions cmd/imgcollector/image/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@ package image
import (
"context"

"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/google/go-containerregistry/pkg/name"

"github.com/castai/kvisor/cmd/imgcollector/image/daemon"
)

type Image = types.Image

func NewFromContainerdDaemon(ctx context.Context, imageName string) (types.Image, func(), error) {
func NewFromContainerdDaemon(ctx context.Context, imageName string) (ImageWithIndex, func(), error) {
img, cleanup, err := daemon.ContainerdImage(ctx, imageName)
if err != nil {
return nil, nil, err
Expand All @@ -22,7 +19,7 @@ func NewFromContainerdDaemon(ctx context.Context, imageName string) (types.Image
}, cleanup, nil
}

func NewFromDockerDaemon(imageName string, ref name.Reference) (types.Image, func(), error) {
func NewFromDockerDaemon(imageName string, ref name.Reference) (ImageWithIndex, func(), error) {
img, cleanup, err := daemon.DockerImage(ref)
if err != nil {
return nil, nil, err
Expand All @@ -33,7 +30,7 @@ func NewFromDockerDaemon(imageName string, ref name.Reference) (types.Image, fun
}, cleanup, nil
}

func NewFromDockerDaemonTarFile(imageName, localTarPath string, ref name.Reference) (types.Image, func(), error) {
func NewFromDockerDaemonTarFile(imageName, localTarPath string, ref name.Reference) (ImageWithIndex, func(), error) {
img, cleanup, err := daemon.DockerTarImage(ref, localTarPath)
if err != nil {
return nil, nil, err
Expand Down
5 changes: 5 additions & 0 deletions cmd/imgcollector/image/daemon/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Image interface {
v1.Image
RepoTags() []string
RepoDigests() []string
Index() *v1.IndexManifest
}

var mu sync.Mutex
Expand Down Expand Up @@ -99,6 +100,10 @@ func (img *image) Manifest() (*v1.Manifest, error) {
return img.Image.Manifest()
}

func (img *image) Index() *v1.IndexManifest {
return nil
}

func (img *image) ConfigFile() (*v1.ConfigFile, error) {
if len(img.inspect.RootFS.Layers) == 0 {
// Podman doesn't return RootFS...
Expand Down
107 changes: 68 additions & 39 deletions cmd/imgcollector/image/hostfs/containerd_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,34 +25,36 @@ const (
)

func NewContainerdImage(hash v1.Hash, cfg ContainerdHostFSConfig) (Image, error) {
manifestReader := newContainerdManifestReader(hash, cfg)
manifest, err := manifestReader.resolveManifest()
metadataReader := newContainerdMetadataReader(hash, cfg)
metadata, err := metadataReader.readMetadata()
if err != nil {
return nil, fmt.Errorf("resolving manifest: %w", err)
}

config, configBytes, err := manifestReader.readConfig(manifest.Config.Digest.String())
config, configBytes, err := metadataReader.readConfig(metadata.Manifest.Config.Digest.String())
if err != nil {
return nil, fmt.Errorf("reading config file: %w", err)
}

img := &containerdBlobImage{
manifest: manifest,
return &containerdBlobImage{
manifest: metadata.Manifest,
index: metadata.Index,
config: config,
configBytes: configBytes,
contentDir: cfg.ContentDir,
}
return img, nil
}, nil
}

func newContainerdManifestReader(hash v1.Hash, cfg ContainerdHostFSConfig) *containerdManifestReader {
return &containerdManifestReader{
func newContainerdMetadataReader(hash v1.Hash, cfg ContainerdHostFSConfig) *containerdMetadataReader {
return &containerdMetadataReader{
imgHash: hash,
cfg: cfg,
}
}

type containerdManifestReader struct {
// containerdMetadataReader is used to follow image references as described here:
// https://github.com/google/go-containerregistry/blob/main/images/ociimage.jpeg
type containerdMetadataReader struct {
cfg ContainerdHostFSConfig
imgHash v1.Hash
}
Expand All @@ -62,6 +64,11 @@ type ContainerdHostFSConfig struct {
ContentDir string
}

type containerdMetadata struct {
Index *v1.IndexManifest
Manifest *v1.Manifest
}

type manifestOrIndex struct {
SchemaVersion int64 `json:"schemaVersion"`
MediaType types.MediaType `json:"mediaType,omitempty"`
Expand All @@ -74,6 +81,16 @@ type manifestOrIndex struct {
Manifests []v1.Descriptor `json:"manifests"`
}

func readManifest(atPath string, into *manifestOrIndex) error {
fileBytes, err := os.ReadFile(atPath)
if err != nil {
return err
}

return json.Unmarshal(fileBytes, into)
}

// manifest part of the sum type
func (mi *manifestOrIndex) manifest() *v1.Manifest {
return &v1.Manifest{
SchemaVersion: mi.SchemaVersion,
Expand All @@ -84,62 +101,69 @@ func (mi *manifestOrIndex) manifest() *v1.Manifest {
}
}

func (h *containerdManifestReader) resolveManifest() (*v1.Manifest, error) {
// Try to find manifest file. In most cases image id digest will point to manifest or index.
var mi manifestOrIndex
readManifest := func(manifestPath string) error {
var err error
fileBytes, err := os.ReadFile(manifestPath)
if err != nil {
return err
}
err = json.Unmarshal(fileBytes, &mi)
if err != nil {
return err
}
return nil
// index part of the sum type
func (mi *manifestOrIndex) index() *v1.IndexManifest {
return &v1.IndexManifest{
SchemaVersion: mi.SchemaVersion,
MediaType: mi.MediaType,
Manifests: mi.Manifests,
Annotations: mi.Annotations,
}
if err := readManifest(path.Join(h.cfg.ContentDir, blobs, h.imgHash.Algorithm, h.imgHash.Hex)); err != nil {
}

func (h *containerdMetadataReader) readMetadata() (*containerdMetadata, error) {
var (
metadata containerdMetadata
manOrIdx manifestOrIndex
)
if err := readManifest(
path.Join(h.cfg.ContentDir, blobs, h.imgHash.Algorithm, h.imgHash.Hex), &manOrIdx,
); err != nil {
return nil, err
}

// This case indicates that image id digest points to config file.
// In such case we need to find manifest by iterating all files and searching for
// config file digest hash inside files content.
if len(mi.Layers) == 0 && len(mi.Manifests) == 0 {
if len(manOrIdx.Layers) == 0 && len(manOrIdx.Manifests) == 0 {
manifestPath, err := h.searchManifestPath()
if err != nil {
return nil, fmt.Errorf("searching manifest path: %w", err)
}
if err := readManifest(manifestPath); err != nil {
if err := readManifest(manifestPath, &manOrIdx); err != nil {
return nil, err
}
}

if len(mi.Layers) > 0 {
return mi.manifest(), nil
if len(manOrIdx.Layers) > 0 {
metadata.Manifest = manOrIdx.manifest()
return &metadata, nil
}

// Search manifest from index manifest.
if len(mi.Manifests) > 0 {
for _, m := range mi.Manifests {
if matchingPlatform(h.cfg.Platform, *m.Platform) {
if err := readManifest(path.Join(h.cfg.ContentDir, blobs, m.Digest.Algorithm, m.Digest.Hex)); err != nil {
if len(manOrIdx.Manifests) > 0 {
metadata.Index = manOrIdx.index()
for _, manifest := range manOrIdx.Manifests {
if matchingPlatform(h.cfg.Platform, *manifest.Platform) {
if err := readManifest(
path.Join(h.cfg.ContentDir, blobs, manifest.Digest.Algorithm, manifest.Digest.Hex), &manOrIdx,
); err != nil {
return nil, err
}
if len(mi.Layers) == 0 {
if len(manOrIdx.Layers) == 0 {
return nil, errors.New("invalid manifest, no layers")
}
return mi.manifest(), nil
metadata.Manifest = manOrIdx.manifest()
return &metadata, nil
}
}
return nil, fmt.Errorf("manifest not found for platform: %s %s", h.cfg.Platform.Architecture, h.cfg.Platform.OS)
}

return nil, fmt.Errorf("unrecognised manifest mediatype %q", string(mi.MediaType))
return nil, fmt.Errorf("unrecognised manifest mediatype %q", string(manOrIdx.MediaType))
}

func (h *containerdManifestReader) readConfig(configID string) (*v1.ConfigFile, []byte, error) {
func (h *containerdMetadataReader) readConfig(configID string) (*v1.ConfigFile, []byte, error) {
p := strings.Split(configID, ":")
if len(p) < 2 {
return nil, nil, fmt.Errorf("invalid configID: %s", configID)
Expand All @@ -160,7 +184,7 @@ func (h *containerdManifestReader) readConfig(configID string) (*v1.ConfigFile,
return &cfg, configBytes, nil
}

func (h *containerdManifestReader) searchManifestPath() (string, error) {
func (h *containerdMetadataReader) searchManifestPath() (string, error) {
root := path.Join(h.cfg.ContentDir, blobs, h.imgHash.Algorithm)
var manifestPath string
digestBytes := []byte(h.imgHash.Hex)
Expand All @@ -184,13 +208,14 @@ func (h *containerdManifestReader) searchManifestPath() (string, error) {
return "", err
}
if manifestPath == "" {
return "", errors.New("manifest not find by searching in blobs content")
return "", errors.New("manifest not found by searching in blobs content")
}
return manifestPath, nil
}

type containerdBlobImage struct {
manifest *v1.Manifest
index *v1.IndexManifest
config *v1.ConfigFile
configBytes []byte
imgHash v1.Hash
Expand Down Expand Up @@ -223,6 +248,10 @@ func (b *containerdBlobImage) Manifest() (*v1.Manifest, error) {
return b.manifest, nil
}

func (b *containerdBlobImage) Index() *v1.IndexManifest {
return b.index
}

func (b *containerdBlobImage) RawConfigFile() ([]byte, error) {
return b.configBytes, nil
}
Expand Down
26 changes: 26 additions & 0 deletions cmd/imgcollector/image/hostfs/containerd_image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,29 @@ func TestContainerdImage(t *testing.T) {
})
}
}

func TestContainerdImageWithIndex(t *testing.T) {
r := require.New(t)
img, err := NewContainerdImage(v1.Hash{
Algorithm: "sha256",
Hex: "211a3be9e15e1e4ccd75220aa776d92e06235552351464db2daf043bd30a0ac0",
},
ContainerdHostFSConfig{
Platform: v1.Platform{
Architecture: "amd64",
OS: "linux",
},
ContentDir: "./testdata/containerd_content",
},
)
r.NoError(err)

index := img.Index()
r.NotNil(index)
r.Len(index.Manifests, 2)

manifest, err := img.Manifest()
r.NoError(err)
r.NotNil(manifest)
r.Len(manifest.Layers, 2)
}
1 change: 1 addition & 0 deletions cmd/imgcollector/image/hostfs/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Image interface {
v1.Image
RepoTags() []string
RepoDigests() []string
Index() *v1.IndexManifest
}

// NewImageHash returns image hash from string in format:
Expand Down
Loading

0 comments on commit e3cbe6d

Please sign in to comment.