Skip to content

Commit

Permalink
Merge pull request #421 from hpidcock/rootless
Browse files Browse the repository at this point in the history
#421

Adds new metadata fields required to enable rootless charms on Kubernetes.
- add the `uid` and `gid` fields to each container in the `containers` section which can be omitted.
- limit the uid and gid values to less than 1000 and greater than 9999 as per the spec.
- add the `charm-user` field which can be omitted, with values of `root`, `sudoer` and `non-root`.

### QA

Run unit tests.

### Links

Specs: [JU074](https://docs.google.com/document/d/1Y6OhDVWDadPYSlZVnPpH_upA_RNNRbM0luYLJJRrbUw/edit) [JU075](https://docs.google.com/document/d/1wSTB6R96B031j91ygK0hlfWdwaRPWCZ0ENQnjSDlEH4/edit)
Jira: JUJU-5123 JUJU-5127
  • Loading branch information
jujubot committed Dec 5, 2023
2 parents 74f4d8b + 1815c9f commit cbe04c8
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 8 deletions.
76 changes: 68 additions & 8 deletions meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,16 @@ func (r Relation) IsImplicit() bool {
r.Role == RoleProvider)
}

// RunAs defines which user to run a certain process as.
type RunAs string

const (
RunAsDefault RunAs = ""
RunAsRoot RunAs = "root"
RunAsSudoer RunAs = "sudoer"
RunAsNonRoot RunAs = "non-root"
)

// Meta represents all the known content that may be defined
// within a charm's metadata.yaml file.
// Note: Series is serialised for backward compatibility
Expand Down Expand Up @@ -280,12 +290,15 @@ type Meta struct {
// v2
Containers map[string]Container `bson:"containers,omitempty" json:"containers,omitempty" yaml:"containers,omitempty"`
Assumes *assumes.ExpressionTree `bson:"assumes,omitempty" json:"assumes,omitempty" yaml:"assumes,omitempty"`
CharmUser RunAs `bson:"charm-user,omitempty" json:"charm-user,omitempty" yaml:"charm-user,omitempty"`
}

// Container specifies the possible systems it supports and mounts it wants.
type Container struct {
Resource string `bson:"resource,omitempty" json:"resource,omitempty" yaml:"resource,omitempty"`
Mounts []Mount `bson:"mounts,omitempty" json:"mounts,omitempty" yaml:"mounts,omitempty"`
Uid int `bson:"uid,omitempty" json:"uid,omitempty" yaml:"uid,omitempty"`
Gid int `bson:"gid,omitempty" json:"gid,omitempty" yaml:"gid,omitempty"`
}

// Mount allows a container to mount a storage filesystem from the storage top-level directive.
Expand Down Expand Up @@ -561,13 +574,9 @@ func parseMeta(m map[string]interface{}) (*Meta, error) {
return nil, err
}
meta.PayloadClasses = parsePayloadClasses(m["payloads"])

if ver := m["min-juju-version"]; ver != nil {
minver, err := version.Parse(ver.(string))
if err != nil {
return &meta, errors.Annotate(err, "invalid min-juju-version")
}
meta.MinJujuVersion = minver
meta.MinJujuVersion, err = parseMinJujuVersion(m["min-juju-version"])
if err != nil {
return nil, err
}
meta.Terms = parseStringList(m["terms"])

Expand All @@ -581,6 +590,10 @@ func parseMeta(m map[string]interface{}) (*Meta, error) {
if err != nil {
return nil, errors.Annotatef(err, "parsing containers")
}
meta.CharmUser, err = parseCharmUser(m["charm-user"])
if err != nil {
return nil, errors.Annotatef(err, "parsing charm-user")
}
return &meta, nil
}

Expand Down Expand Up @@ -1170,6 +1183,22 @@ func parseContainers(input interface{}, resources map[string]resource.Meta, stor
if err != nil {
return nil, errors.Annotatef(err, "container %q", name)
}

if value, ok := containerMap["uid"]; ok {
container.Uid = int(value.(int64))
if container.Uid >= 1000 && container.Uid < 10000 {
return nil, errors.Errorf("container %q has invalid uid %d: uid cannot be in reserved range 1000-9999",
name, container.Uid)
}
}
if value, ok := containerMap["gid"]; ok {
container.Gid = int(value.(int64))
if container.Gid >= 1000 && container.Gid < 10000 {
return nil, errors.Errorf("container %q has invalid gid %d: gid cannot be in reserved range 1000-9999",
name, container.Gid)
}
}

containers[name] = container
}
if len(containers) == 0 {
Expand Down Expand Up @@ -1206,6 +1235,31 @@ func parseMounts(input interface{}, storage map[string]Storage) ([]Mount, error)
return mounts, nil
}

func parseMinJujuVersion(value any) (version.Number, error) {
if value == nil {
return version.Zero, nil
}
ver, err := version.Parse(value.(string))
if err != nil {
return version.Zero, errors.Annotate(err, "invalid min-juju-version")
}
return ver, nil
}

func parseCharmUser(value any) (RunAs, error) {
if value == nil {
return RunAsDefault, nil
}
v := RunAs(value.(string))
switch v {
case RunAsRoot, RunAsSudoer, RunAsNonRoot:
return v, nil
default:
return RunAsDefault, errors.Errorf("invalid charm-user %q expected one of %s, %s or %s", v,
RunAsRoot, RunAsSudoer, RunAsNonRoot)
}
}

var storageSchema = schema.FieldMap(
schema.Fields{
"type": schema.OneOf(schema.Const(string(StorageBlock)), schema.Const(string(StorageFilesystem))),
Expand Down Expand Up @@ -1345,9 +1399,13 @@ var containerSchema = schema.FieldMap(
schema.Fields{
"resource": schema.String(),
"mounts": schema.List(mountSchema),
"uid": schema.Int(),
"gid": schema.Int(),
}, schema.Defaults{
"resource": schema.Omit,
"mounts": schema.Omit,
"uid": schema.Omit,
"gid": schema.Omit,
})

var mountSchema = schema.FieldMap(
Expand Down Expand Up @@ -1383,6 +1441,7 @@ var charmSchema = schema.FieldMap(
"min-juju-version": schema.String(),
"assumes": schema.List(schema.Any()),
"containers": schema.StringMap(containerSchema),
"charm-user": schema.String(),
},
schema.Defaults{
"provides": schema.Omit,
Expand All @@ -1404,6 +1463,7 @@ var charmSchema = schema.FieldMap(
"min-juju-version": schema.Omit,
"assumes": schema.Omit,
"containers": schema.Omit,
"charm-user": schema.Omit,
},
)

Expand All @@ -1427,7 +1487,7 @@ func ensureUnambiguousFormat(raw map[interface{}]interface{}) error {
for _, key := range keys {
detected := FormatUnknown
switch key {
case "containers", "assumes":
case "containers", "assumes", "charm-user":
detected = FormatV2
case "series", "deployment", "min-juju-version":
detected = FormatV1
Expand Down
81 changes: 81 additions & 0 deletions meta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1733,6 +1733,8 @@ containers:
mounts:
- storage: a
location: /b/
uid: 10
gid: 10
resources:
test-os:
type: oci-image
Expand All @@ -1748,10 +1750,44 @@ storage:
Storage: "a",
Location: "/b/",
}},
Uid: 10,
Gid: 10,
},
})
}

func (s *MetaSuite) TestInvalidUid(c *gc.C) {
_, err := charm.ReadMeta(strings.NewReader(`
name: a
summary: b
description: c
containers:
foo:
resource: test-os
uid: 1000
resources:
test-os:
type: oci-image
`))
c.Assert(err, gc.ErrorMatches, `parsing containers: container "foo" has invalid uid 1000: uid cannot be in reserved range 1000-9999`)
}

func (s *MetaSuite) TestInvalidGid(c *gc.C) {
_, err := charm.ReadMeta(strings.NewReader(`
name: a
summary: b
description: c
containers:
foo:
resource: test-os
gid: 1000
resources:
test-os:
type: oci-image
`))
c.Assert(err, gc.ErrorMatches, `parsing containers: container "foo" has invalid gid 1000: gid cannot be in reserved range 1000-9999`)
}

func (s *MetaSuite) TestSystemReferencesFileResource(c *gc.C) {
_, err := charm.ReadMeta(strings.NewReader(`
name: a
Expand Down Expand Up @@ -2014,3 +2050,48 @@ func (FormatMetaSuite) TestCheckV2WithDeployment(c *gc.C) {
err := meta.Check(charm.FormatV2, charm.SelectionManifest, charm.SelectionBases)
c.Assert(err, gc.ErrorMatches, `deployment in metadata v2 not valid`)
}

func (s *MetaSuite) TestCharmUser(c *gc.C) {
meta, err := charm.ReadMeta(strings.NewReader(`
name: a
summary: b
description: c
charm-user: root
`))
c.Assert(err, gc.IsNil)
c.Assert(meta.CharmUser, gc.Equals, charm.RunAsRoot)

meta, err = charm.ReadMeta(strings.NewReader(`
name: a
summary: b
description: c
charm-user: sudoer
`))
c.Assert(err, gc.IsNil)
c.Assert(meta.CharmUser, gc.Equals, charm.RunAsSudoer)

meta, err = charm.ReadMeta(strings.NewReader(`
name: a
summary: b
description: c
charm-user: non-root
`))
c.Assert(err, gc.IsNil)
c.Assert(meta.CharmUser, gc.Equals, charm.RunAsNonRoot)

meta, err = charm.ReadMeta(strings.NewReader(`
name: a
summary: b
description: c
`))
c.Assert(err, gc.IsNil)
c.Assert(meta.CharmUser, gc.Equals, charm.RunAsDefault)

_, err = charm.ReadMeta(strings.NewReader(`
name: a
summary: b
description: c
charm-user: barry
`))
c.Assert(err, gc.ErrorMatches, `parsing charm-user: invalid charm-user "barry" expected one of root, sudoer or non-root`)
}

0 comments on commit cbe04c8

Please sign in to comment.