Skip to content
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
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