Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(artifact/config): fetch config layer for a specific platform #349

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cmd/artifact/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ package config

import (
"context"
"fmt"
"runtime"
"strings"

"github.com/spf13/cobra"

Expand All @@ -28,6 +31,7 @@ import (
type artifactConfigOptions struct {
*options.Common
*options.Registry
platform string
}

// NewArtifactConfigCmd returns the artifact config command.
Expand All @@ -48,6 +52,8 @@ func NewArtifactConfigCmd(ctx context.Context, opt *options.Common) *cobra.Comma
}

o.Registry.AddFlags(cmd)
cmd.Flags().StringVar(&o.platform, "platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
"os and architecture of the artifact in OS/ARCH format")

return cmd
}
Expand All @@ -70,7 +76,14 @@ func (o *artifactConfigOptions) RunArtifactConfig(ctx context.Context, args []st
return err
}

if config, err = puller.PullConfigLayer(ctx, ref); err != nil {
// TODO: implement two new flags (platforms, platform) based on the oci platform struct.
// Split the platform.
tokens := strings.Split(o.platform, "/")
if len(tokens) != 2 {
return fmt.Errorf("invalid platform format: %s", o.platform)
}

if config, err = puller.RawConfigLayer(ctx, ref, tokens[0], tokens[1]); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/artifact/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ var _ = Describe("Config", func() {
args = []string{artifactCmd, configCmd, "noregistry/noartifact", plaingHTTP, configFlag, configDir}
})

assertFailedBehavior(usage, "ERROR unable to fetch reference")
assertFailedBehavior(usage, "ERROR unable to get manifest: unable to fetch reference")
})

When("non existing repository", func() {
Expand Down
4 changes: 2 additions & 2 deletions cmd/artifact/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
return nil, err
}

artifactConfig, err := puller.GetArtifactConfig(ctx, ref)
artifactConfig, err := puller.ArtifactConfig(ctx, ref, runtime.GOOS, runtime.GOARCH)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -265,7 +265,7 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []

logger.Info("Preparing to pull artifact", logger.Args("ref", ref))

if err := puller.CheckAllowedType(ctx, ref, o.allowedTypes.Types); err != nil {
if err := puller.CheckAllowedType(ctx, ref, runtime.GOOS, runtime.GOARCH, o.allowedTypes.Types); err != nil {
return err
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/artifact/install/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ var artifactInstallTests = Describe("install", func() {
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, "noregistry/testrules", "--plain-http", "--config", configFile}
})
installAssertFailedBehavior(artifactInstallUsage, `ERROR unable to fetch reference`)
installAssertFailedBehavior(artifactInstallUsage, `ERROR unable to get manifest: unable to fetch reference`)
})

When("invalid repository", func() {
Expand All @@ -193,7 +193,7 @@ var artifactInstallTests = Describe("install", func() {
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, newReg, "--plain-http", "--config", configFile}
})
installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR unable to fetch reference %q", newReg))
installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR unable to get manifest: unable to fetch reference %q", newReg))
})

When("with disallowed types (rulesfile)", func() {
Expand Down
4 changes: 2 additions & 2 deletions internal/follower/follower.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func (f *Follower) follow(ctx context.Context) {
f.logger.Info("Found new artifact version", f.logger.Args("followerName", f.ref, "tag", f.tag))

// Pull config layer to check falco versions
artifactConfig, err := f.GetArtifactConfig(ctx, f.ref)
artifactConfig, err := f.ArtifactConfig(ctx, f.ref, runtime.GOOS, runtime.GOARCH)
if err != nil {
f.logger.Error("Unable to pull config layer", f.logger.Args("followerName", f.ref, "reason", err.Error()))
return
Expand Down Expand Up @@ -252,7 +252,7 @@ func (f *Follower) follow(ctx context.Context) {
// pull downloads, extracts, and installs the artifact.
func (f *Follower) pull(ctx context.Context) (filePaths []string, res *oci.RegistryResult, err error) {
f.logger.Debug("Check if pulling an allowed type of artifact", f.logger.Args("followerName", f.ref))
if err := f.Puller.CheckAllowedType(ctx, f.ref, f.Config.AllowedTypes.Types); err != nil {
if err := f.Puller.CheckAllowedType(ctx, f.ref, runtime.GOOS, runtime.GOARCH, f.Config.AllowedTypes.Types); err != nil {
return nil, nil, err
}

Expand Down
59 changes: 38 additions & 21 deletions pkg/oci/puller/puller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"encoding/json"
"fmt"
"io"
"runtime"

v1 "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
Expand Down Expand Up @@ -161,8 +160,28 @@ func manifestFromDesc(ctx context.Context, target oras.Target, desc *v1.Descript
return &manifest, nil
}

// manifestFromRef retieves the manifest of an artifact, also taking care of resolving to it walking through indexes.
func (p *Puller) manifestFromRef(ctx context.Context, ref string) (*v1.Manifest, error) {
// manifest retieves the manifest of an artifact, also taking care of resolving to it walking through indexes.
// If the artifact has a v1.MediaTypeImageIndex descriptor then it fetches the manifest for the
// specified platform.
func (p *Puller) manifest(ctx context.Context, ref, os, arch string) (*v1.Manifest, error) {
var manifest v1.Manifest

manifestBytes, err := p.RawManifest(ctx, ref, os, arch)
if err != nil {
return nil, fmt.Errorf("unable to get manifest: %w", err)
}

if err = json.Unmarshal(manifestBytes, &manifest); err != nil {
return nil, fmt.Errorf("unable to unmarshal manifest: %w", err)
}

return &manifest, nil
}

// RawManifest fetches the manifest layer from a given reference.
// If the artifact has a v1.MediaTypeImageIndex descriptor then it fetches the manifest for the
// specified platform.
func (p *Puller) RawManifest(ctx context.Context, ref, os, arch string) ([]byte, error) {
repo, err := repository.NewRepository(ref, repository.WithClient(p.Client), repository.WithPlainHTTP(p.plainHTTP))
if err != nil {
return nil, err
Expand All @@ -188,19 +207,18 @@ func (p *Puller) manifestFromRef(ctx context.Context, ref string) (*v1.Manifest,
return nil, fmt.Errorf("unable to unmarshal manifest: %w", err)
}

// todo: decide if goos or arch should be passed to this function
found := false
for _, manifest := range index.Manifests {
if manifest.Platform.OS == runtime.GOOS &&
manifest.Platform.Architecture == runtime.GOARCH {
if manifest.Platform.OS == os &&
manifest.Platform.Architecture == arch {
desc = manifest
found = true
break
}
}

if !found {
return nil, fmt.Errorf("unable to find a manifest matching the given platform: %s %s", runtime.GOOS, runtime.GOARCH)
return nil, fmt.Errorf("unable to find a manifest matching the given platform: %s/%s", os, arch)
}

manifestReader, err = repo.Fetch(ctx, desc)
Expand All @@ -209,22 +227,19 @@ func (p *Puller) manifestFromRef(ctx context.Context, ref string) (*v1.Manifest,
}
}

var manifest v1.Manifest
manifestBytes, err := io.ReadAll(manifestReader)
if err != nil {
return nil, fmt.Errorf("unable to read bytes from manifest reader for ref %q: %w", ref, err)
}

if err = json.Unmarshal(manifestBytes, &manifest); err != nil {
return nil, fmt.Errorf("unable to unmarshal manifest: %w", err)
}

return &manifest, nil
return manifestBytes, nil
}

// GetArtifactConfig fetches only the config layer from a given ref.
func (p *Puller) GetArtifactConfig(ctx context.Context, ref string) (*oci.ArtifactConfig, error) {
configBytes, err := p.PullConfigLayer(ctx, ref)
// ArtifactConfig fetches only the config layer from a given ref.
// If the artifact has a v1.MediaTypeImageIndex descriptor then it fetches the config layer for the
// specified platform.
func (p *Puller) ArtifactConfig(ctx context.Context, ref, os, arch string) (*oci.ArtifactConfig, error) {
configBytes, err := p.RawConfigLayer(ctx, ref, os, arch)
if err != nil {
return nil, err
}
Expand All @@ -237,14 +252,16 @@ func (p *Puller) GetArtifactConfig(ctx context.Context, ref string) (*oci.Artifa
return &artifactConfig, nil
}

// PullConfigLayer fetches only the config layer from a given ref.
func (p *Puller) PullConfigLayer(ctx context.Context, ref string) ([]byte, error) {
// RawConfigLayer fetches only the config layer from a given ref.
// If the artifact has a v1.MediaTypeImageIndex descriptor then it fetches the config layer for the
// specified platform.
func (p *Puller) RawConfigLayer(ctx context.Context, ref, os, arch string) ([]byte, error) {
repo, err := repository.NewRepository(ref, repository.WithClient(p.Client), repository.WithPlainHTTP(p.plainHTTP))
if err != nil {
return nil, err
}

manifest, err := p.manifestFromRef(ctx, ref)
manifest, err := p.manifest(ctx, ref, os, arch)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -276,12 +293,12 @@ func (p *Puller) PullConfigLayer(ctx context.Context, ref string) ([]byte, error
// CheckAllowedType does a preliminary check on the manifest to state whether we are allowed
// or not to download this type of artifact. If allowedTypes is empty, everything is allowed,
// else it is used to perform the check.
func (p *Puller) CheckAllowedType(ctx context.Context, ref string, allowedTypes []oci.ArtifactType) error {
func (p *Puller) CheckAllowedType(ctx context.Context, ref, os, arch string, allowedTypes []oci.ArtifactType) error {
if len(allowedTypes) == 0 {
return nil
}

manifest, err := p.manifestFromRef(ctx, ref)
manifest, err := p.manifest(ctx, ref, os, arch)
if err != nil {
return err
}
Expand Down
56 changes: 53 additions & 3 deletions pkg/oci/puller/puller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -143,20 +144,24 @@ var _ = Describe("Puller", func() {
})
})

Context("PullConfigLayer func", func() {
Context("RawConfigLayer func", func() {
var (
ref string
os string
arch string
cfgLayer []byte
err error
)
JustBeforeEach(func() {
puller = ocipuller.NewPuller(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
cfgLayer, err = puller.PullConfigLayer(ctx, ref)
cfgLayer, err = puller.RawConfigLayer(ctx, ref, os, arch)
})

JustAfterEach(func() {
cfgLayer = nil
err = nil
os = ""
arch = ""
})

When("Artifact does not exist", func() {
Expand All @@ -173,6 +178,9 @@ var _ = Describe("Puller", func() {
When("config layer is set", func() {
BeforeEach(func() {
ref = pluginMultiPlatformRef
tokens := strings.Split(testPluginPlatform1, "/")
os = tokens[0]
arch = tokens[1]
})

It("should get config layer", func() {
Expand All @@ -191,6 +199,48 @@ var _ = Describe("Puller", func() {
Expect(string(cfgLayer)).Should(Equal("{}"))
})
})

When("config layer for linux/arm64", func() {
BeforeEach(func() {
ref = pluginMultiPlatformRef
tokens := strings.Split(testPluginPlatform1, "/")
os = tokens[0]
arch = tokens[1]
})

It("should get config layer", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(cfgLayer).ShouldNot(BeNil())
})
})

When("config layer artifact without platform", func() {
BeforeEach(func() {
ref = rulesRef
tokens := strings.Split(testPluginPlatform1, "/")
os = tokens[0]
arch = tokens[1]
})

It("should get config layer", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(cfgLayer).ShouldNot(BeNil())
})
})

When("config layer for non existing platform", func() {
BeforeEach(func() {
ref = pluginMultiPlatformRef
os = "linux"
arch = "non-existing"
})

It("should error", func() {
Expect(err).Should(HaveOccurred())
Expect(cfgLayer).Should(BeNil())
})
})

})

Context("Descriptor func", func() {
Expand Down Expand Up @@ -251,7 +301,7 @@ var _ = Describe("Puller", func() {
)
JustBeforeEach(func() {
puller = ocipuller.NewPuller(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
err = puller.CheckAllowedType(ctx, ref, allowedTypes)
err = puller.CheckAllowedType(ctx, ref, runtime.GOOS, runtime.GOARCH, allowedTypes)
})

JustAfterEach(func() {
Expand Down