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 support for adding users and groups in spec #514

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
17 changes: 16 additions & 1 deletion artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ type Artifacts struct {
// On linux this would typically be installed to /usr/include/.
Headers map[string]ArtifactConfig `yaml:"headers,omitempty" json:"headers,omitempty"`

// TODO: other types of artifacts (libexec, etc)
// Users is a list of users to add to the system when the package is installed.
Users []AddUserConfig `yaml:"users,omitempty" json:"users,omitempty"`
// Groups is a list of groups to add to the system when the package is installed.
Groups []AddGroupConfig `yaml:"groups,omitempty" json:"groups,omitempty"`
}

type ArtifactSymlinkConfig struct {
Expand Down Expand Up @@ -106,6 +109,18 @@ func (a *ArtifactConfig) ResolveName(path string) string {
return filepath.Base(path)
}

// AddUserConfig is the configuration for adding a user to the system.
type AddUserConfig struct {
// Name is the name of the user to add to the system.
Name string `yaml:"name" json:"name"`
}

// AddGroupConfig is the configuration for adding a group to the system.
type AddGroupConfig struct {
// Name is the name of the group to add to the system.
Name string `yaml:"name" json:"name"`
}

// IsEmpty is used to determine if there are any artifacts to include in the package.
func (a *Artifacts) IsEmpty() bool {
if len(a.Binaries) > 0 {
Expand Down
42 changes: 42 additions & 0 deletions docs/spec.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,34 @@
"$id": "https://github.com/Azure/dalec/spec",
"$ref": "#/$defs/Spec",
"$defs": {
"AddGroupConfig": {
"properties": {
"name": {
"type": "string",
"description": "Name is the name of the group to add to the system."
}
},
"additionalProperties": false,
"type": "object",
"required": [
"name"
],
"description": "AddGroupConfig is the configuration for adding a group to the system."
},
"AddUserConfig": {
"properties": {
"name": {
"type": "string",
"description": "Name is the name of the user to add to the system."
}
},
"additionalProperties": false,
"type": "object",
"required": [
"name"
],
"description": "AddUserConfig is the configuration for adding a user to the system."
},
"ArtifactBuild": {
"properties": {
"steps": {
Expand Down Expand Up @@ -154,6 +182,20 @@
},
"type": "object",
"description": "Headers is a list of header files and/or folders to be installed.\nOn linux this would typically be installed to /usr/include/."
},
"users": {
"items": {
"$ref": "#/$defs/AddUserConfig"
},
"type": "array",
"description": "Users is a list of users to add to the system when the package is installed."
},
"groups": {
"items": {
"$ref": "#/$defs/AddGroupConfig"
},
"type": "array",
"description": "Groups is a list of groups to add to the system when the package is installed."
}
},
"additionalProperties": false,
Expand Down
23 changes: 23 additions & 0 deletions frontend/deb/debroot.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,17 @@ func Debroot(ctx context.Context, sOpt dalec.SourceOpts, spec *dalec.Spec, worke
states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "dalec/"+customSystemdPostinstFile), 0o600, customEnable), opts...))
}

postinst := bytes.NewBuffer(nil)
writeUsersPostInst(postinst, spec.Artifacts.Users)
writeGroupsPostInst(postinst, spec.Artifacts.Groups)

if postinst.Len() > 0 {
dt := []byte("#!/usr/bin/env sh\nset -e\n")
dt = append(dt, postinst.Bytes()...)

states = append(states, dalecDir.File(llb.Mkfile(filepath.Join(dir, "postinst"), 0o700, dt), opts...))
}

patchDir := dalecDir.File(llb.Mkdir(filepath.Join(dir, "dalec/patches"), 0o755), opts...)
sorted := dalec.SortMapKeys(spec.Patches)
for _, name := range sorted {
Expand Down Expand Up @@ -596,3 +607,15 @@ func unquote(v string) string {
}
return v
}

func writeUsersPostInst(w io.Writer, users []dalec.AddUserConfig) {
for _, u := range users {
fmt.Fprintf(w, "getent passwd %s >/dev/null || useradd %s\n", u.Name, u.Name)
}
}

func writeGroupsPostInst(w io.Writer, groups []dalec.AddGroupConfig) {
for _, g := range groups {
fmt.Fprintf(w, "getent group %s >/dev/null || groupadd --system %s\n", g.Name, g.Name)
}
}
76 changes: 68 additions & 8 deletions frontend/rpm/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,28 @@ OrderWithRequires(postun): systemd
return requires + orderRequires
}

func getUserPostRequires(users []dalec.AddUserConfig, groups []dalec.AddGroupConfig) string {
var out string

if len(users) > 0 {
out += "Requires(post): /usr/sbin/adduser, /usr/bin/getent\n"
}
if len(groups) > 0 {
out += "Requires(post): /usr/sbin/groupadd, /usr/bin/getent\n"
}

return out
}

func (w *specWrapper) Requires() fmt.Stringer {
b := &strings.Builder{}

// first write systemd requires if they exist,
// first write post requires for systemd and user/group creation
// as these do not come from dependencies in the spec
// NOTE: This is a bit janky since different distributions may have different
// package names... something to consider as we expand functionality.
b.WriteString(getSystemdRequires(w.Artifacts.Systemd))
b.WriteString(getUserPostRequires(w.Artifacts.Users, w.Artifacts.Groups))

deps := w.Spec.Targets[w.Target].Dependencies
if deps == nil {
Expand Down Expand Up @@ -385,21 +401,66 @@ fi
func (w *specWrapper) Post() fmt.Stringer {
b := &strings.Builder{}

if w.Artifacts.Systemd.IsEmpty() {
systemd := w.postSystemd()
users := w.postUsers()
groups := w.postGroups()

if systemd == "" && users == "" && groups == "" {
return b
}

b.WriteString("%post\n")
if systemd != "" {
b.WriteString(systemd)
}
if users != "" {
b.WriteString(users)
}
if groups != "" {
b.WriteString(groups)
}

b.WriteString("\n")
return b
}

func (w *specWrapper) postUsers() string {
if len(w.Artifacts.Users) == 0 {
return ""
}

b := &strings.Builder{}
for _, user := range w.Artifacts.Users {
fmt.Fprintf(b, "getent passwd %s >/dev/null || adduser %s\n", user.Name, user.Name)
}
return b.String()
}

func (w *specWrapper) postGroups() string {
if len(w.Artifacts.Groups) == 0 {
return ""
}

b := &strings.Builder{}
for _, group := range w.Artifacts.Groups {
fmt.Fprintf(b, "getent group %s >/dev/null || groupadd --system %s\n", group.Name, group.Name)
}
return b.String()
}

func (w *specWrapper) postSystemd() string {
if w.Artifacts.Systemd.IsEmpty() {
return ""
}
enabledUnits := w.Artifacts.Systemd.EnabledUnits()
if len(enabledUnits) == 0 {
// if we have no enabled units, we don't need to do anything systemd related
// in the post script. In this case, we shouldn't emit '%post'
// as this eliminates the need for extra dependencies in the target container
return b
return ""
}

b.WriteString("%post\n")
// TODO: can inject other post install steps here in the future

b := &strings.Builder{}
keys := dalec.SortMapKeys(enabledUnits)
for _, servicePath := range keys {
unitConf := w.Spec.Artifacts.Systemd.Units[servicePath]
Expand All @@ -409,8 +470,7 @@ func (w *specWrapper) Post() fmt.Stringer {
)
}

b.WriteString("\n")
return b
return b.String()
}

func (w *specWrapper) PostUn() fmt.Stringer {
Expand Down
111 changes: 90 additions & 21 deletions frontend/rpm/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,41 @@ cp -r src/simple.service %{buildroot}/%{_unitdir}/phony.service
`)
})

t.Run("test user", func(t *testing.T) {
w := &specWrapper{Spec: &dalec.Spec{
Artifacts: dalec.Artifacts{
Users: []dalec.AddUserConfig{
{Name: "testuser"},
},
},
}}

got := w.Post().String()
want := `%post
getent passwd testuser >/dev/null || adduser testuser

`

assert.Equal(t, want, got)
})

t.Run("test group", func(t *testing.T) {
w := &specWrapper{Spec: &dalec.Spec{
Artifacts: dalec.Artifacts{
Groups: []dalec.AddGroupConfig{
{Name: "testgroup"},
},
},
}}

got := w.Post().String()
want := `%post
getent group testgroup >/dev/null || groupadd --system testgroup

`

assert.Equal(t, want, got)
})
}

func TestTemplate_Requires(t *testing.T) {
Expand Down Expand Up @@ -731,43 +766,77 @@ A helpful tool
}

func TestTemplate_ImplicitRequires(t *testing.T) {
spec := &dalec.Spec{
Artifacts: dalec.Artifacts{
Systemd: &dalec.SystemdConfiguration{
Units: map[string]dalec.SystemdUnitConfig{
"test.service": {
Enable: true,
t.Run("systemd", func(t *testing.T) {
spec := &dalec.Spec{
Artifacts: dalec.Artifacts{
Systemd: &dalec.SystemdConfiguration{
Units: map[string]dalec.SystemdUnitConfig{
"test.service": {
Enable: true,
},
},
},
},
},
}
}

w := specWrapper{Spec: spec}
w := specWrapper{Spec: spec}

got := w.Requires().String()
assert.Equal(t, got,
`Requires(post): systemd
got := w.Requires().String()
assert.Equal(t, got,
`Requires(post): systemd
Requires(preun): systemd
Requires(postun): systemd
OrderWithRequires(post): systemd
OrderWithRequires(preun): systemd
OrderWithRequires(postun): systemd
`,
)
)

spec.Artifacts.Systemd.Units = map[string]dalec.SystemdUnitConfig{
"test.service": {
Enable: false,
},
}
spec.Artifacts.Systemd.Units = map[string]dalec.SystemdUnitConfig{
"test.service": {
Enable: false,
},
}

got = w.Requires().String()
assert.Equal(t, got,
`Requires(preun): systemd
got = w.Requires().String()
assert.Equal(t, got,
`Requires(preun): systemd
Requires(postun): systemd
OrderWithRequires(preun): systemd
OrderWithRequires(postun): systemd
`)
})

t.Run("user", func(t *testing.T) {
spec := &dalec.Spec{
Artifacts: dalec.Artifacts{
Users: []dalec.AddUserConfig{
{Name: "testuser"},
},
},
}

w := specWrapper{Spec: spec}

got := w.Requires().String()
want := "Requires(post): /usr/sbin/adduser, /usr/bin/getent\n"
assert.Equal(t, got, want)
})

t.Run("group", func(t *testing.T) {
spec := &dalec.Spec{
Artifacts: dalec.Artifacts{
Groups: []dalec.AddGroupConfig{
{Name: "testgroup"},
},
},
}

w := specWrapper{Spec: spec}

got := w.Requires().String()
want := "Requires(post): /usr/sbin/groupadd, /usr/bin/getent\n"
assert.Equal(t, got, want)
})

}
Loading
Loading