Skip to content

Commit

Permalink
Quality of life features for ContainerConfig (#165)
Browse files Browse the repository at this point in the history
Shamelessly copied TagsSlice and Tag helper functions from VirtualMachine implementation
Add Indexed fields helper maps
  • Loading branch information
xortim authored Oct 3, 2024
1 parent b137704 commit 824e06b
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 68 deletions.
66 changes: 66 additions & 0 deletions container_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package proxmox

import (
"reflect"
"strconv"
"strings"
)

// mergeIndexedString uses reflection to merge the ordinal/indexed fields
// returned by the Proxmox API. Such as Mp0..9, and Net0..9
func (cc *ContainerConfig) mergeIndexedFields(prefix string) map[string]string {
stringMap := make(map[string]string)
t := reflect.TypeOf(*cc)
v := reflect.ValueOf(*cc)
count := v.NumField()

for i := 0; i < count; i++ {
fn := t.Field(i).Name
fv := v.Field(i).String()
if fv == "" {
continue
}
if strings.HasPrefix(fn, prefix) {
// Ignore non-numeric suffixes like SCSIHW
suffix := strings.TrimPrefix(fn, prefix)
if _, err := strconv.Atoi(suffix); err != nil {
continue
}
stringMap[strings.ToLower(fn)] = fv
}
}

return stringMap
}

// MergeDevs merges and assigns the indexed Dev0..9 fields to a string map
func (cc *ContainerConfig) MergeDevs() map[string]string {
if cc.Devs == nil {
cc.Devs = cc.mergeIndexedFields("Dev")
}
return cc.Devs
}

// MergeDevs merges and assigns the indexed Mp0..9 fields to a string map
func (cc *ContainerConfig) MergeMps() map[string]string {
if cc.Mps == nil {
cc.Mps = cc.mergeIndexedFields("Mp")
}
return cc.Mps
}

// MergeDevs merges and assigns the indexed Net0..9 fields to a string map
func (cc *ContainerConfig) MergeNets() map[string]string {
if cc.Nets == nil {
cc.Nets = cc.mergeIndexedFields("Net")
}
return cc.Nets
}

// MergeDevs merges and assigns the indexed Unused0..9 fields to a string map
func (cc *ContainerConfig) MergeUnuseds() map[string]string {
if cc.Unuseds == nil {
cc.Unuseds = cc.mergeIndexedFields("Unused")
}
return cc.Unuseds
}
82 changes: 82 additions & 0 deletions containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/url"
"strings"
)

func (c *Container) Clone(ctx context.Context, params *ContainerCloneOptions) (newid int, task *Task, err error) {
Expand Down Expand Up @@ -288,3 +289,84 @@ func (c *Container) GetFirewallOptions(ctx context.Context) (options *FirewallVi
func (c *Container) UpdateFirewallOptions(ctx context.Context, options *FirewallVirtualMachineOption) error {
return c.client.Put(ctx, fmt.Sprintf("/nodes/%s/lxc/%d/firewall/options", c.Node, c.VMID), options, nil)
}

// Tag Management Helpers

// HasTag returns if a Tag is present in TagSlice
func (c *Container) HasTag(value string) bool {
if c.ContainerConfig == nil {
return false
}

if c.ContainerConfig.Tags == "" {
return false
}

if c.ContainerConfig.TagsSlice == nil {
c.SplitTags()
}

for _, tag := range c.ContainerConfig.TagsSlice {
if tag == value {
return true
}
}

return false
}

// AddTag appends the passed value to TagsSlice and updates Tags via c.Config
// If accurate state is important, then reassign the value of Container after the task
// has completed.
func (c *Container) AddTag(ctx context.Context, value string) (*Task, error) {
if c.HasTag(value) {
return nil, ErrNoop
}

if c.ContainerConfig.TagsSlice == nil {
c.SplitTags()
}

c.ContainerConfig.TagsSlice = append(c.ContainerConfig.TagsSlice, value)
c.ContainerConfig.Tags = strings.Join(c.ContainerConfig.TagsSlice, TagSeperator)
c.Tags = c.ContainerConfig.Tags // Keep the parent object up to date

return c.Config(ctx, ContainerOption{
Name: "tags",
Value: c.ContainerConfig.Tags,
})
}

// RemoveTag removes the passed value from TagsSlice and updates Tags via c.Config
// If accurate state is important, then reassign the value of Container after the task
// has completed.
func (c *Container) RemoveTag(ctx context.Context, value string) (*Task, error) {
if !c.HasTag(value) {
return nil, ErrNoop
}

if c.ContainerConfig.TagsSlice == nil {
c.SplitTags()
}

for i, tag := range c.ContainerConfig.TagsSlice {
if tag == value {
c.ContainerConfig.TagsSlice = append(
c.ContainerConfig.TagsSlice[:i],
c.ContainerConfig.TagsSlice[i+1:]...,
)
}
}

c.ContainerConfig.Tags = strings.Join(c.ContainerConfig.TagsSlice, TagSeperator)
c.Tags = c.ContainerConfig.Tags // keep the parent object up to date
return c.Config(ctx, ContainerOption{
Name: "tags",
Value: c.ContainerConfig.Tags,
})
}

// SplitTags sets ContainerConfig TagsSlice my splitting the value of ContainerConfig.Tags with TagSeparator
func (c *Container) SplitTags() {
c.ContainerConfig.TagsSlice = strings.Split(c.ContainerConfig.Tags, TagSeperator)
}
60 changes: 60 additions & 0 deletions containers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,63 @@ func TestContainerInterfaces(t *testing.T) {
assert.NotEmpty(t, interfaces)
assert.Equal(t, interfaces, ContainerInterfaces{{HWAddr: "00:00:00:00:00:00", Inet: "127.0.0.1/8", Name: "lo", Inet6: "::1/128"}, {Inet6: "fe80::be24:11ff:fe89:6707/64", Name: "eth0", HWAddr: "bc:24:11:89:67:07", Inet: "192.168.3.95/22"}})
}

func TestContainerTagsSlice(t *testing.T) {
mocks.On(mockConfig)
defer mocks.Off()
client := mockClient()
ctx := context.Background()
node, err := client.Node(ctx, "node1")
assert.Nil(t, err)

container, err := node.Container(ctx, 101)
assert.Nil(t, err)

assert.NotEmpty(t, container.ContainerConfig.TagsSlice)
}

func TestContainer_AddTag(t *testing.T) {
mocks.On(mockConfig)
defer mocks.Off()
client := mockClient()
ctx := context.Background()
node, err := client.Node(ctx, "node1")
assert.Nil(t, err)

container, err := node.Container(ctx, 101)
assert.Nil(t, err)

container.AddTag(ctx, "newTag")
assert.True(t, container.HasTag("newTag"))
}

func TestContainer_HasTag(t *testing.T) {
mocks.On(mockConfig)
defer mocks.Off()
client := mockClient()
ctx := context.Background()
node, err := client.Node(ctx, "node1")
assert.Nil(t, err)

container, err := node.Container(ctx, 101)
assert.Nil(t, err)

assert.True(t, container.HasTag("tag1"))
assert.False(t, container.HasTag("not_there"))
}

func TestContainer_RemoveTag(t *testing.T) {
mocks.On(mockConfig)
defer mocks.Off()
client := mockClient()
ctx := context.Background()
node, err := client.Node(ctx, "node1")
assert.Nil(t, err)

container, err := node.Container(ctx, 101)
assert.Nil(t, err)

assert.True(t, container.HasTag("tag1"))
container.RemoveTag(ctx, "tag1")
assert.False(t, container.HasTag("tag1"))
}
2 changes: 2 additions & 0 deletions tests/mocks/pve7x/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,7 @@ func nodes() {
"status":"stopped",
"swap":0,
"template":1,
"tags":"tag1;tag2",
"type":"lxc",
"uptime":0,
"vmid":101
Expand All @@ -960,6 +961,7 @@ func nodes() {
Reply(200).
JSON(`{"data": "null"}`)

// Used for ContainerConfig
gock.New(config.C.URI).
Get("^/nodes/node1/lxc/101/config").
Reply(200).
Expand Down
Loading

0 comments on commit 824e06b

Please sign in to comment.