diff --git a/containerm/cli/cli_command_ctrs_create.go b/containerm/cli/cli_command_ctrs_create.go index 00985a5..914120a 100644 --- a/containerm/cli/cli_command_ctrs_create.go +++ b/containerm/cli/cli_command_ctrs_create.go @@ -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 { @@ -46,6 +49,7 @@ type createConfig struct { interactive bool privileged bool network string + containerFile string extraHosts []string extraCapabilities []string devices []string @@ -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) }, @@ -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 { @@ -124,7 +151,7 @@ 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 } @@ -132,7 +159,7 @@ func (cc *createCmd) run(args []string) error { 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 } @@ -140,7 +167,7 @@ func (cc *createCmd) run(args []string) error { if cc.config.ports != nil { mappings, err := util.ParsePortMappings(cc.config.ports) if err != nil { - return err + return nil, err } ctrToCreate.HostConfig.PortMappings = mappings } @@ -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, } @@ -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 } @@ -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.") } diff --git a/containerm/cli/cli_command_ctrs_create_test.go b/containerm/cli/cli_command_ctrs_create_test.go index 2242b29..d361606 100644 --- a/containerm/cli/cli_command_ctrs_create_test.go +++ b/containerm/cli/cli_command_ctrs_create_test.go @@ -14,6 +14,8 @@ package main import ( "context" + "encoding/json" + "os" "strconv" "strings" "testing" @@ -30,6 +32,7 @@ const ( createCmdFlagTerminal = "t" createCmdFlagInteractive = "i" createCmdFlagPrivileged = "privileged" + createCmdFlagContainerFile = "file" createCmdFlagRestartPolicy = "rp" createCmdFlagRestartPolicyMaxCount = "rp-cnt" createCmdFlagRestartPolicyTimeout = "rp-to" @@ -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, @@ -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), @@ -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, @@ -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, @@ -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") @@ -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{ @@ -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{ @@ -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{ diff --git a/integration/ctr_management_cli_test.go b/integration/ctr_management_cli_test.go index dac4f81..59477ff 100644 --- a/integration/ctr_management_cli_test.go +++ b/integration/ctr_management_cli_test.go @@ -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() { @@ -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()) diff --git a/integration/testdata/container.json b/integration/testdata/container.json new file mode 100644 index 0000000..5d7a656 --- /dev/null +++ b/integration/testdata/container.json @@ -0,0 +1,6 @@ +{ + "container_name": "create_container_from_file", + "image": { + "name": "docker.io/library/influxdb:1.8.4" + } +} diff --git a/integration/testdata/create-help.golden b/integration/testdata/create-help.golden index 1267beb..e2e2a93 100644 --- a/integration/testdata/create-help.golden +++ b/integration/testdata/create-help.golden @@ -1,7 +1,7 @@ Create a container. Usage: - kanto-cm create [option]... container-image-id [command] [command-arg]... + kanto-cm create [option]... [container-image-id] [command] [command-arg]... Examples: create container-image-id @@ -16,6 +16,7 @@ Flags: --e=VAR1=2 --e=VAR2="a bc" If --e=VAR1= is used, the environment variable would be set to empty. If --e=VAR1 is used, the environment variable would be removed from the container environment inherited from the image. + -f, --file string Creates a container with a predefined config given by the user. -h, --help help for create --hosts strings Extra hosts to be added in the current container's /etc/hosts file. Example: --hosts="hostname1:, hostname2:.." diff --git a/integration/testdata/create-test.yaml b/integration/testdata/create-test.yaml index 5d4fa06..8840a65 100644 --- a/integration/testdata/create-test.yaml +++ b/integration/testdata/create-test.yaml @@ -12,7 +12,7 @@ command: args: ["create", "--host", "$KANTO_HOST"] expected: exitCode: 1 - err: "Error: requires at least 1 arg(s), only received 0" + err: "Error: container image argument is expected" --- name: create_influxdb_container command: @@ -33,4 +33,25 @@ command: args: ["create", "--host", "$KANTO_HOST", "invalid"] expected: exitCode: 1 - err: "Error: rpc error: code = Unknown desc = failed to resolve reference \"invalid\": object required" \ No newline at end of file + err: "Error: rpc error: code = Unknown desc = failed to resolve reference \"invalid\": object required" +--- +name: create_container_from_file +command: + binary: kanto-cm + args: ["create", "--file", "$CONTAINER_CONFIG", "--host", "$KANTO_HOST"] +expected: + exitCode: 0 +customResult: + type: REGEX + args: ["([A-Za-z0-9]+(-[A-Za-z0-9]+)+)"] +onExit: + - binary: "kanto-cm" + args: ["remove", "--host", "$KANTO_HOST", "-n", "create_container_from_file", "-f"] +--- +name: create_container_from_file_with_flags +command: + binary: kanto-cm + args: ["create", "--file", "$CONTAINER_CONFIG", "--privileged", "true", "--host", "$KANTO_HOST"] +expected: + exitCode: 1 + err: "Error: no arguments are expected when creating a container from file" \ No newline at end of file