Skip to content

Commit

Permalink
[#208] Add file flag to the CLI create command.
Browse files Browse the repository at this point in the history
* [#208] New Flag '-file' is added. as well as unit and integration tests for it.

---------

Signed-off-by: Daniel Milchev [email protected]
  • Loading branch information
daniel-milchev authored and k-gostev committed Apr 30, 2024
1 parent 7c7ab88 commit 898c6c8
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 45 deletions.
109 changes: 80 additions & 29 deletions containerm/cli/cli_command_ctrs_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ package main

import (
"context"
"encoding/json"
"fmt"
"os"
"time"

"github.com/eclipse-kanto/container-management/containerm/containers/types"
"github.com/eclipse-kanto/container-management/containerm/log"
"github.com/eclipse-kanto/container-management/containerm/util"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

type createCmd struct {
Expand All @@ -46,6 +49,7 @@ type createConfig struct {
interactive bool
privileged bool
network string
containerFile string
extraHosts []string
extraCapabilities []string
devices []string
Expand All @@ -68,10 +72,10 @@ type createConfig struct {
func (cc *createCmd) init(cli *cli) {
cc.cli = cli
cc.cmd = &cobra.Command{
Use: "create [option]... container-image-id [command] [command-arg]...",
Use: "create [option]... [container-image-id] [command] [command-arg]...",
Short: "Create a container.",
Long: "Create a container.",
Args: cobra.MinimumNArgs(1),
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
return cc.run(args)
},
Expand All @@ -81,37 +85,60 @@ func (cc *createCmd) init(cli *cli) {
cc.setupFlags()
}

func (cc *createCmd) run(args []string) error {
// parse parameters
imageID := args[0]
func initContainer(config createConfig, imageName string) *types.Container {
return &types.Container{
Name: config.name,
Image: types.Image{
Name: imageName,
},
HostConfig: &types.HostConfig{
Privileged: config.privileged,
ExtraHosts: config.extraHosts,
ExtraCapabilities: config.extraCapabilities,
NetworkMode: types.NetworkMode(config.network),
},
IOConfig: &types.IOConfig{
Tty: config.terminal,
OpenStdin: config.interactive,
},
}
}

func (cc *createCmd) containerFromFile() (*types.Container, error) {
var err error
cc.cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) {
if flag.Changed && flag.Name != "file" {
err = log.NewError("no other flags are expected when creating a container from file")
}
})
if err != nil {
return nil, err
}
byteValue, err := os.ReadFile(cc.config.containerFile)
if err != nil {
return nil, err
}
ctrToCreate := initContainer(cc.config, "")
if err = json.Unmarshal(byteValue, ctrToCreate); err != nil {
return nil, err
}
return ctrToCreate, nil
}

func (cc *createCmd) containerFromFlags(args []string) (*types.Container, error) {
ctrToCreate := initContainer(cc.config, args[0])

var command []string
if len(args) > 1 {
command = args[1:]
}

if cc.config.privileged && cc.config.devices != nil {
return log.NewError("cannot create the container as privileged and with specified devices at the same time - choose one of the options")
return nil, log.NewError("cannot create the container as privileged and with specified devices at the same time - choose one of the options")
}

if cc.config.privileged && cc.config.extraCapabilities != nil {
return log.NewError("cannot create the container as privileged and with extra capabilities at the same time - choose one of the options")
}

ctrToCreate := &types.Container{
Name: cc.config.name,
Image: types.Image{
Name: imageID,
},
HostConfig: &types.HostConfig{
Privileged: cc.config.privileged,
ExtraHosts: cc.config.extraHosts,
ExtraCapabilities: cc.config.extraCapabilities,
NetworkMode: types.NetworkMode(cc.config.network),
},
IOConfig: &types.IOConfig{
Tty: cc.config.terminal,
OpenStdin: cc.config.interactive,
},
return nil, log.NewError("cannot create the container as privileged and with extra capabilities at the same time - choose one of the options")
}

if cc.config.env != nil || command != nil {
Expand All @@ -124,23 +151,23 @@ func (cc *createCmd) run(args []string) error {
if cc.config.devices != nil {
devs, err := util.ParseDeviceMappings(cc.config.devices)
if err != nil {
return err
return nil, err
}
ctrToCreate.HostConfig.Devices = devs
}

if cc.config.mountPoints != nil {
mounts, err := util.ParseMountPoints(cc.config.mountPoints)
if err != nil {
return err
return nil, err
} else if mounts != nil {
ctrToCreate.Mounts = mounts
}
}
if cc.config.ports != nil {
mappings, err := util.ParsePortMappings(cc.config.ports)
if err != nil {
return err
return nil, err
}
ctrToCreate.HostConfig.PortMappings = mappings
}
Expand All @@ -155,7 +182,6 @@ func (cc *createCmd) run(args []string) error {
Type: types.No,
}
case string(types.UnlessStopped):

ctrToCreate.HostConfig.RestartPolicy = &types.RestartPolicy{
Type: types.UnlessStopped,
}
Expand Down Expand Up @@ -203,7 +229,31 @@ func (cc *createCmd) run(args []string) error {
ctrToCreate.HostConfig.Resources = getResourceLimits(cc.config.resources)
ctrToCreate.Image.DecryptConfig = getDecryptConfig(cc.config)

if err := util.ValidateContainer(ctrToCreate); err != nil {
return ctrToCreate, nil
}

func (cc *createCmd) run(args []string) error {
var (
ctrToCreate *types.Container
err error
)

if len(cc.config.containerFile) > 0 {
if len(args) > 0 {
return log.NewError("no arguments are expected when creating a container from file")
}
if ctrToCreate, err = cc.containerFromFile(); err != nil {
return err
}
} else if len(args) != 0 {
if ctrToCreate, err = cc.containerFromFlags(args); err != nil {
return err
}
} else {
return log.NewError("container image argument is expected")
}

if err = util.ValidateContainer(ctrToCreate); err != nil {
return err
}

Expand Down Expand Up @@ -314,4 +364,5 @@ func (cc *createCmd) setupFlags() {
flagSet.StringSliceVar(&cc.config.decRecipients, "dec-recipients", nil, "Sets a recipients certificates list of the image (used only for PKCS7 and must be an x509)")
//init extra capabilities
flagSet.StringSliceVar(&cc.config.extraCapabilities, "cap-add", nil, "Add Linux capabilities to the container")
flagSet.StringVarP(&cc.config.containerFile, "file", "f", "", "Creates a container with a predefined config given by the user.")
}
104 changes: 92 additions & 12 deletions containerm/cli/cli_command_ctrs_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ package main

import (
"context"
"encoding/json"
"os"
"strconv"
"strings"
"testing"
Expand All @@ -30,6 +32,7 @@ const (
createCmdFlagTerminal = "t"
createCmdFlagInteractive = "i"
createCmdFlagPrivileged = "privileged"
createCmdFlagContainerFile = "file"
createCmdFlagRestartPolicy = "rp"
createCmdFlagRestartPolicyMaxCount = "rp-cnt"
createCmdFlagRestartPolicyTimeout = "rp-to"
Expand Down Expand Up @@ -74,10 +77,11 @@ func TestCreateCmdSetupFlags(t *testing.T) {
createCliTest.init()

expectedCfg := createConfig{
name: "",
terminal: true,
interactive: true,
privileged: true,
name: "",
terminal: true,
interactive: true,
privileged: true,
containerFile: string("config.json"),
restartPolicy: restartPolicy{
kind: string(types.Always),
timeout: 10,
Expand Down Expand Up @@ -109,6 +113,7 @@ func TestCreateCmdSetupFlags(t *testing.T) {
createCmdFlagTerminal: strconv.FormatBool(expectedCfg.terminal),
createCmdFlagInteractive: strconv.FormatBool(expectedCfg.interactive),
createCmdFlagPrivileged: strconv.FormatBool(expectedCfg.privileged),
createCmdFlagContainerFile: expectedCfg.containerFile,
createCmdFlagRestartPolicy: expectedCfg.restartPolicy.kind,
createCmdFlagRestartPolicyMaxCount: strconv.Itoa(expectedCfg.restartPolicy.maxRetryCount),
createCmdFlagRestartPolicyTimeout: strconv.FormatInt(expectedCfg.restartPolicy.timeout, 10),
Expand Down Expand Up @@ -451,6 +456,14 @@ func (createTc *createCommandTest) generateRunExecutionConfigs() map[string]test
},
mockExecution: createTc.mockExecCreateWithExtraCapabilities,
},
"test_create_extra_capabilities_with_privileged": {
args: createCmdArgs,
flags: map[string]string{
createCmdFlagExtraCapabilities: "CAP_NET_ADMIN",
createCmdFlagPrivileged: "true",
},
mockExecution: createTc.mockExecCreateWithExtraCapabilitiesWithPrivileged,
},
// Test privileged
"test_create_privileged": {
args: createCmdArgs,
Expand All @@ -459,6 +472,35 @@ func (createTc *createCommandTest) generateRunExecutionConfigs() map[string]test
},
mockExecution: createTc.mockExecCreateWithPrivileged,
},
// Test container file
"test_create_no_args": {
mockExecution: createTc.mockExecCreateWithNoArgs,
},
"test_create_container_file": {
flags: map[string]string{
createCmdFlagContainerFile: "../pkg/testutil/config/container/valid.json",
},
mockExecution: createTc.mockExecCreateContainerFile,
},
"test_create_container_file_invalid_path": {
flags: map[string]string{
createCmdFlagContainerFile: "/test/test",
},
mockExecution: createTc.mockExecCreateContainerFileInvalidPath,
},
"test_create_container_file_invalid_json": {
flags: map[string]string{
createCmdFlagContainerFile: "../pkg/testutil/config/container/invalid.json",
},
mockExecution: createTc.mockExecCreateContainerFileInvalidJSON,
},
"test_create_container_file_with_args": {
args: createCmdArgs,
flags: map[string]string{
createCmdFlagContainerFile: "../pkg/testutil/config/container/valid.json",
},
mockExecution: createTc.mockExecCreateContainerFileWithArgs,
},
// Test terminal
"test_create_terminal": {
args: createCmdArgs,
Expand Down Expand Up @@ -884,6 +926,12 @@ func (createTc *createCommandTest) mockExecCreateWithPortsProto(args []string) e
createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Eq(container)).Times(1).Return(container, nil)
return nil
}

func (createTc *createCommandTest) mockExecCreateWithNoArgs(args []string) error {
createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0)
return log.NewError("container image argument is expected")
}

func (createTc *createCommandTest) mockExecCreateWithPortsIncorrectPortsConfig(args []string) error {
createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0)
return log.NewError("Incorrect port mapping configuration")
Expand Down Expand Up @@ -937,6 +985,11 @@ func (createTc *createCommandTest) mockExecCreateWithExtraCapabilities(args []st
return nil
}

func (createTc *createCommandTest) mockExecCreateWithExtraCapabilitiesWithPrivileged(args []string) error {
createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0)
return log.NewError("cannot create the container as privileged and with extra capabilities at the same time - choose one of the options")
}

func (createTc *createCommandTest) mockExecCreateWithPrivileged(args []string) error {
container := initExpectedCtr(&types.Container{
Image: types.Image{
Expand All @@ -950,6 +1003,39 @@ func (createTc *createCommandTest) mockExecCreateWithPrivileged(args []string) e
createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Eq(container)).Times(1).Return(container, nil)
return nil
}

func (createTc *createCommandTest) mockExecCreateContainerFile(_ []string) error {
byteValue, _ := os.ReadFile("../pkg/testutil/config/container/valid.json")
container := &types.Container{
HostConfig: &types.HostConfig{
NetworkMode: types.NetworkModeBridge,
},
IOConfig: &types.IOConfig{},
}
json.Unmarshal(byteValue, container)

createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Eq(container)).Times(1).Return(container, nil)
return nil
}

func (createTc *createCommandTest) mockExecCreateContainerFileInvalidPath(_ []string) error {
createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0)
_, err := os.ReadFile("/test/test")
return err
}

func (createTc *createCommandTest) mockExecCreateContainerFileInvalidJSON(_ []string) error {
createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0)
byteValue, _ := os.ReadFile("../pkg/testutil/config/container/invalid.json")
err := json.Unmarshal(byteValue, &types.Container{})
return err
}

func (createTc *createCommandTest) mockExecCreateContainerFileWithArgs(_ []string) error {
createTc.mockClient.EXPECT().Create(gomock.AssignableToTypeOf(context.Background()), gomock.Any()).Times(0)
return log.NewError("no arguments are expected when creating a container from file")
}

func (createTc *createCommandTest) mockExecCreateWithTerminal(args []string) error {
container := initExpectedCtr(&types.Container{
Image: types.Image{
Expand Down Expand Up @@ -1061,19 +1147,13 @@ func initExpectedCtr(ctr *types.Container) *types.Container {
//merge default and provided
if ctr.HostConfig == nil {
ctr.HostConfig = &types.HostConfig{
Privileged: false,
ExtraHosts: nil,
ExtraCapabilities: nil,
NetworkMode: types.NetworkModeBridge,
NetworkMode: types.NetworkModeBridge,
}
} else if ctr.HostConfig.NetworkMode == "" {
ctr.HostConfig.NetworkMode = types.NetworkModeBridge
}
if ctr.IOConfig == nil {
ctr.IOConfig = &types.IOConfig{
Tty: false,
OpenStdin: false,
}
ctr.IOConfig = &types.IOConfig{}
}
if ctr.HostConfig.LogConfig == nil {
ctr.HostConfig.LogConfig = &types.LogConfiguration{
Expand Down
4 changes: 3 additions & 1 deletion integration/ctr_management_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ import (
var testdataFS embed.FS

type cliTestConfiguration struct {
KantoHost string `env:"KANTO_HOST" envDefault:"/run/container-management/container-management.sock"`
KantoHost string `env:"KANTO_HOST" envDefault:"/run/container-management/container-management.sock"`
ContainerConfig string `env:"CONTAINER_CONFIG" envDefault:"./testdata/container.json"`
}

func init() {
Expand All @@ -51,6 +52,7 @@ func TestCtrMgrCLI(t *testing.T) {
cliTestConfiguration := &cliTestConfiguration{}
require.NoError(t, env.Parse(cliTestConfiguration, env.Options{RequiredIfNoDef: true}))
require.NoError(t, os.Setenv("KANTO_HOST", cliTestConfiguration.KantoHost))
require.NoError(t, os.Setenv("CONTAINER_CONFIG", cliTestConfiguration.ContainerConfig))

if exist, _ := util.IsDirectory(TestData); !exist {
require.NoError(t, dumpTestdata())
Expand Down
6 changes: 6 additions & 0 deletions integration/testdata/container.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"container_name": "create_container_from_file",
"image": {
"name": "docker.io/library/influxdb:1.8.4"
}
}
Loading

0 comments on commit 898c6c8

Please sign in to comment.