diff --git a/config.go b/config.go index 7b46087..a5eb8d9 100644 --- a/config.go +++ b/config.go @@ -95,6 +95,7 @@ var ( "devices": hclspec.NewAttr("devices", "list(string)", false), "entrypoint": hclspec.NewAttr("entrypoint", "any", false), // any for compat "working_dir": hclspec.NewAttr("working_dir", "string", false), + "group_add": hclspec.NewAttr("group_add", "list(string)", false), "hostname": hclspec.NewAttr("hostname", "string", false), "image": hclspec.NewAttr("image", "string", true), "image_pull_timeout": hclspec.NewDefault( @@ -225,6 +226,7 @@ type TaskConfig struct { Devices []string `codec:"devices"` Entrypoint any `codec:"entrypoint"` // any for compat WorkingDir string `codec:"working_dir"` + GroupAdd []string `codec:"group_add"` Hostname string `codec:"hostname"` Image string `codec:"image"` ImagePullTimeout string `codec:"image_pull_timeout"` diff --git a/config_test.go b/config_test.go index 852c5a9..9934be2 100644 --- a/config_test.go +++ b/config_test.go @@ -168,3 +168,20 @@ func TestConfig_PodmanOOMScoreAdj(t *testing.T) { parser.ParseHCL(t, validHCL, &tc) must.Eq(t, "default", tc.Socket) } + +func TestConfig_GroupAdd(t *testing.T) { + ci.Parallel(t) + + parser := hclutils.NewConfigParser(taskConfigSpec) + expectedGroups := []string{"audio", "video"} + validHCL := ` + config { + image = "docker://redis" + group_add = ["audio", "video"] + } +` + + var tc *TaskConfig + parser.ParseHCL(t, validHCL, &tc) + must.SliceContainsAll(t, expectedGroups, tc.GroupAdd) +} diff --git a/driver.go b/driver.go index a57ab51..4b9367d 100644 --- a/driver.go +++ b/driver.go @@ -731,6 +731,11 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive createOpts.ContainerSecurityConfig.ReadOnlyFilesystem = podmanTaskConfig.ReadOnlyRootfs createOpts.ContainerSecurityConfig.ApparmorProfile = podmanTaskConfig.ApparmorProfile + // add group_add if configured + if groupAddErr := parseGroupAdd(podmanTaskConfig.GroupAdd, &createOpts); groupAddErr != nil { + return nil, nil, fmt.Errorf("failed to parse group_add configuration: %w", groupAddErr) + } + // add security_opt if configured if securiyOptsErr := parseSecurityOpt(podmanTaskConfig.SecurityOpt, &createOpts); securiyOptsErr != nil { return nil, nil, fmt.Errorf("failed to parse security_opt configuration: %w", securiyOptsErr) @@ -1744,8 +1749,37 @@ func setExtraHosts(hosts []string, createOpts *api.SpecGenerator) error { return nil } +// ensureAnnotations initializes the Annotations map if it's nil +func ensureAnnotations(createOpts *api.SpecGenerator) { + if createOpts.Annotations == nil { + createOpts.Annotations = make(map[string]string) + } +} + +// parseGroupAdd parses group-add options and sets them in the container creation specification. +func parseGroupAdd(groupAdd []string, createOpts *api.SpecGenerator) error { + ensureAnnotations(createOpts) + + if slices.Contains(groupAdd, "keep-groups") { + // "keep-groups" is a special value that is interpreted by crun via annotations + // to retain the original user's supplementary groups. + // This is mutually exclusive with any other group-add options. + if len(groupAdd) > 1 { + return fmt.Errorf("the '--group-add keep-groups' option is not allowed with any other --group-add options") + } + createOpts.Annotations["run.oci.keep_original_groups"] = "1" + } else { + // Regular group additions as a list of group names. + createOpts.ContainerSecurityConfig.Groups = groupAdd + } + + return nil +} + + func parseSecurityOpt(securityOpt []string, createOpts *api.SpecGenerator) error { - createOpts.Annotations = make(map[string]string) + ensureAnnotations(createOpts) + for _, opt := range securityOpt { con := strings.SplitN(opt, "=", 2) if len(con) == 1 && con[0] != "no-new-privileges" { diff --git a/driver_test.go b/driver_test.go index 04b9a56..b14268e 100644 --- a/driver_test.go +++ b/driver_test.go @@ -1661,6 +1661,55 @@ func TestPodmanDriver_Caps(t *testing.T) { } } +// check group_add option +func TestPodmanDriver_GroupAdd(t *testing.T) { + taskCfg := newTaskConfig("", busyboxLongRunningCmd) + // add a group_add + taskCfg.GroupAdd = []string{"audio", "video"} + inspectData := startDestroyInspect(t, taskCfg, "groupadd") + // and compare it + must.SliceContains(t, inspectData.HostConfig.GroupAdd, "audio") + must.SliceContains(t, inspectData.HostConfig.GroupAdd, "video") +} + +// check group_add option with keep-groups special case +func TestPodmanDriver_GroupAdd_KeepGroups(t *testing.T) { + taskCfg := newTaskConfig("", busyboxLongRunningCmd) + // add a group_add + taskCfg.GroupAdd = []string{"keep-groups"} + inspectData := startDestroyInspect(t, taskCfg, "groupadd-keep-groups") + // and compare it + must.NotNil(t, inspectData.Config.Annotations["run.oci.keep_original_groups"]) + must.Eq(t, inspectData.Config.Annotations["run.oci.keep_original_groups"], "1") +} + +// check group_add option with keep-groups special case +// with other groups - should error +func TestPodmanDriver_GroupAdd_KeepGroupsWithOthersError(t *testing.T) { + ci.Parallel(t) + + taskCfg := newTaskConfig("", busyboxLongRunningCmd) + // try to combine keep-groups with other groups - should error + taskCfg.GroupAdd = []string{"keep-groups", "audio"} + + task := &drivers.TaskConfig{ + ID: uuid.Generate(), + Name: "groupadd-error", + AllocID: uuid.Generate(), + Resources: createBasicResources(), + } + must.NoError(t, task.EncodeConcreteDriverConfig(&taskCfg)) + + d := podmanDriverHarness(t, nil) + cleanup := d.MkAllocDir(task, false) + defer cleanup() + + _, _, err := d.StartTask(task) + // should fail with error about keep-groups being mutually exclusive + must.Error(t, err) + must.StrContains(t, err.Error(), "keep-groups") +} + // check security_opt option func TestPodmanDriver_SecurityOpt(t *testing.T) { taskCfg := newTaskConfig("", busyboxLongRunningCmd)