Skip to content

Commit

Permalink
new(cmd): add artifact config command
Browse files Browse the repository at this point in the history
Signed-off-by: Aldo Lacuku <[email protected]>
  • Loading branch information
alacuku committed Oct 31, 2023
1 parent 865d087 commit ef9832d
Show file tree
Hide file tree
Showing 5 changed files with 397 additions and 0 deletions.
2 changes: 2 additions & 0 deletions cmd/artifact/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/spf13/cobra"

artifactconfig "github.com/falcosecurity/falcoctl/cmd/artifact/config"
"github.com/falcosecurity/falcoctl/cmd/artifact/follow"
"github.com/falcosecurity/falcoctl/cmd/artifact/info"
"github.com/falcosecurity/falcoctl/cmd/artifact/install"
Expand Down Expand Up @@ -69,6 +70,7 @@ func NewArtifactCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Comma
cmd.AddCommand(list.NewArtifactListCmd(ctx, opt))
cmd.AddCommand(info.NewArtifactInfoCmd(ctx, opt))
cmd.AddCommand(follow.NewArtifactFollowCmd(ctx, opt))
cmd.AddCommand(artifactconfig.NewArtifactConfigCmd(ctx, opt))

return cmd
}
80 changes: 80 additions & 0 deletions cmd/artifact/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 config

import (
"context"

"github.com/spf13/cobra"

ocipuller "github.com/falcosecurity/falcoctl/pkg/oci/puller"
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
"github.com/falcosecurity/falcoctl/pkg/options"
)

type artifactConfigOptions struct {
*options.Common
*options.Registry
}

// NewArtifactConfigCmd returns the artifact config command.
func NewArtifactConfigCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := artifactConfigOptions{
Common: opt,
Registry: &options.Registry{},
}

cmd := &cobra.Command{
Use: "config [ref] [flags]",
Short: "Get the config layer of an artifact",
Long: "Get the config layer of an artifact",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunArtifactConfig(ctx, args)
},
}

o.Registry.AddFlags(cmd)

return cmd
}

func (o *artifactConfigOptions) RunArtifactConfig(ctx context.Context, args []string) error {
var (
puller *ocipuller.Puller
ref string
config []byte
err error
)

// Create puller with auto login enabled.
if puller, err = ociutils.Puller(o.PlainHTTP, o.Printer); err != nil {
return err
}

// Resolve the artifact reference.
if ref, err = o.IndexCache.ResolveReference(args[0]); err != nil {
return err
}

if config, err = puller.PullConfigLayer(ctx, ref); err != nil {
return err
}

o.Printer.DefaultText.Println(string(config))

return nil
}
132 changes: 132 additions & 0 deletions cmd/artifact/config/config_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 config_test

import (
"context"
"fmt"
"testing"

"github.com/distribution/distribution/v3/configuration"
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"

"github.com/falcosecurity/falcoctl/cmd"
"github.com/falcosecurity/falcoctl/pkg/oci"
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)

var (
localRegistryHost string
localRegistry *remote.Registry
testRuleTarball = "../../../pkg/test/data/rules.tar.gz"
testPluginTarball = "../../../pkg/test/data/plugin.tar.gz"
testPluginPlatform1 = "linux/amd64"
testPluginPlatform2 = "windows/amd64"
testPluginPlatform3 = "linux/arm64"
ctx = context.Background()
pluginMultiPlatformRef string
rulesRef string
artifactWithoutConfigRef string
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
)

func TestConfig(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Config Suite")
}

var _ = BeforeSuite(func() {
var err error
config := &configuration.Configuration{}
// Get a free port to be used by the registry.
port, err := testutils.FreePort()
Expect(err).ToNot(HaveOccurred())
// Create the registry address to which will bind.
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
localRegistryHost = config.HTTP.Addr

// Create the oras registry.
localRegistry, err = testutils.NewOrasRegistry(localRegistryHost, true)
Expect(err).ToNot(HaveOccurred())

// Start the local registry.
go func() {
err := testutils.StartRegistry(context.Background(), config)
Expect(err).ToNot(BeNil())
}()

// Initialize options for command.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))

// Push the artifacts to the registry.
// Same artifacts will be used to test the puller code.
pusher := ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), true, nil)

// Push plugin artifact with multiple architectures.
filePathsAndPlatforms := ocipusher.WithFilepathsAndPlatforms([]string{testPluginTarball, testPluginTarball, testPluginTarball},
[]string{testPluginPlatform1, testPluginPlatform2, testPluginPlatform3})
pluginMultiPlatformRef = localRegistryHost + "/plugins:multiplatform"
artConfig := oci.ArtifactConfig{}
Expect(artConfig.ParseDependencies("my-dep:1.2.3|my-alt-dep:1.4.5")).ToNot(HaveOccurred())
Expect(artConfig.ParseRequirements("my-req:7.8.9")).ToNot(HaveOccurred())
artifactConfig := ocipusher.WithArtifactConfig(artConfig)

// Build options slice.
options := []ocipusher.Option{filePathsAndPlatforms, artifactConfig}

// Push the plugin artifact.
_, err = pusher.Push(ctx, oci.Plugin, pluginMultiPlatformRef, options...)
Expect(err).ShouldNot(HaveOccurred())

// Prepare and push artifact without config layer.
filePaths := ocipusher.WithFilepaths([]string{testRuleTarball})
artConfig = oci.ArtifactConfig{}
Expect(artConfig.ParseDependencies("dep1:1.2.3", "dep2:2.3.1")).ToNot(HaveOccurred())
options = []ocipusher.Option{
filePaths,
ocipusher.WithTags("latest"),
}

// Push artifact without config layer.
// Push artifact without config layer.
artifactWithoutConfigRef = localRegistryHost + "/artifact:noconfig"
_, err = pusher.Push(ctx, oci.Rulesfile, artifactWithoutConfigRef, options...)
Expect(err).ShouldNot(HaveOccurred())

// Push a rulesfile artifact
options = append(options, ocipusher.WithArtifactConfig(artConfig))
rulesRef = localRegistryHost + "/rulesfiles:regular"
_, err = pusher.Push(ctx, oci.Rulesfile, rulesRef, options...)
Expect(err).ShouldNot(HaveOccurred())
})

func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}
166 changes: 166 additions & 0 deletions cmd/artifact/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 config_test

import (
"regexp"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"

"github.com/falcosecurity/falcoctl/cmd"
)

var usage = `Usage:
falcoctl artifact config [ref] [flags]
Flags:
-h, --help help for config
--plain-http allows interacting with remote registry via plain http requests
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
`

var help = `Get the config layer of an artifact
Usage:
falcoctl artifact config [ref] [flags]
Flags:
-h, --help help for config
--plain-http allows interacting with remote registry via plain http requests
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")`

var _ = Describe("Config", func() {
const (
artifactCmd = "artifact"
configCmd = "config"
plaingHTTP = "--plain-http"
)

var (
err error
args []string
)

var assertFailedBehavior = func(usage, specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}

JustBeforeEach(func() {
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})

JustAfterEach(func() {
err = nil
Expect(output.Clear()).ShouldNot(HaveOccurred())
args = nil
})

Context("help message", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, "--help"}
})

It("should match the saved one", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(string(output.Contents())).ShouldNot(Equal(help))
})
})

Context("wrong number of arguments", func() {
When("number of arguments equal to 0", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd}
})

assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 0 ")
})

When("number of arguments equal to 2", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, "arg1", "arg2"}
})

assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 2 ")
})
})

Context("failure", func() {
When("unreachable/non existing registry", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, "noregistry/noartifact", plaingHTTP}
})

assertFailedBehavior(usage, "ERROR unable to fetch reference \"noregistry/noartifact:latest\":"+
" Get \"http://noregistry/v2/noartifact/manifests/latest\": dial tcp: lookup noregistry: no such host")
})

When("non existing repository", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, localRegistryHost + "/noartifact", plaingHTTP}
})

assertFailedBehavior(usage, "noartifact:latest: not found")
})

When("non parsable reference", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, " ", plaingHTTP}
})

assertFailedBehavior(usage, "ERROR cannot find among the configured indexes, skipping ")
})
})

Context("success", func() {
When("empty config layer", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, artifactWithoutConfigRef, plaingHTTP}
})

It("should success", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta("{}")))
})
})

When("with valid config layer", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, rulesRef, plaingHTTP}
})

It("should success", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(`{"dependencies":[{"name":"dep1","version":"1.2.3"},{"name":"dep2","version":"2.3.1"}]}`)))
})
})
})

})
Loading

0 comments on commit ef9832d

Please sign in to comment.