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

Add file flag to the CLI create command #209

Merged
merged 4 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
48 changes: 35 additions & 13 deletions containerm/cli/cli_command_ctrs_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ package main

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

"github.com/eclipse-kanto/container-management/containerm/containers/types"
Expand Down Expand Up @@ -46,6 +48,7 @@ type createConfig struct {
interactive bool
privileged bool
network string
containerFile string
extraHosts []string
extraCapabilities []string
devices []string
Expand All @@ -68,10 +71,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 @@ -82,19 +85,20 @@ func (cc *createCmd) init(cli *cli) {
}

func (cc *createCmd) run(args []string) error {
// parse parameters
imageID := 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")
imageID := ""
if len(cc.config.containerFile) == 0 {
if len(args) == 0 {
return log.NewError("container image argument is expected")
}
imageID = args[0]
} else if len(args) > 0 {
return log.NewError("no arguments are expected when container is created from a file")
}

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")
var command []string
if len(args) > 1 {
command = args[1:]
}

ctrToCreate := &types.Container{
Expand All @@ -114,6 +118,14 @@ func (cc *createCmd) run(args []string) error {
},
}

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")
}

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")
}

if cc.config.env != nil || command != nil {
ctrToCreate.Config = &types.ContainerConfiguration{
Env: cc.config.env,
Expand Down Expand Up @@ -155,7 +167,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,6 +214,16 @@ func (cc *createCmd) run(args []string) error {
ctrToCreate.HostConfig.Resources = getResourceLimits(cc.config.resources)
ctrToCreate.Image.DecryptConfig = getDecryptConfig(cc.config)

if cc.config.containerFile != "" {
byteValue, err := os.ReadFile(cc.config.containerFile)
if err != nil {
return err
}
if err = json.Unmarshal(byteValue, ctrToCreate); err != nil {
return err
}
}

if err := util.ValidateContainer(ctrToCreate); err != nil {
return err
}
Expand Down Expand Up @@ -314,4 +335,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.")
}
89 changes: 85 additions & 4 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,34 @@ 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 := initExpectedCtr(&types.Container{})
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 container is created from a file")
}

func (createTc *createCommandTest) mockExecCreateWithTerminal(args []string) error {
container := initExpectedCtr(&types.Container{
Image: types.Image{
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"
}
}
3 changes: 2 additions & 1 deletion integration/testdata/create-help.golden
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:<IP1>, hostname2:<IP2>.."
Expand Down
17 changes: 15 additions & 2 deletions integration/testdata/create-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -33,4 +33,17 @@ command:
args: ["create", "--host", "$KANTO_HOST", "invalid"]
expected:
exitCode: 1
err: "Error: rpc error: code = Unknown desc = failed to resolve reference \"invalid\": object required"
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"]
Loading