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

Overhaul: Guest tags #332

Merged
merged 2 commits into from
May 15, 2024
Merged
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
5 changes: 2 additions & 3 deletions proxmox/config_guest.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package proxmox
import (
"errors"
"strconv"
"strings"
)

// All code LXC and Qemu have in common should be placed here.
Expand All @@ -25,7 +24,7 @@ type GuestResource struct {
Node string `json:"node"` // TODO custom type
Pool string `json:"pool"` // TODO custom type
Status string `json:"status"` // TODO custom type?
Tags []string `json:"tags"` // TODO custom type
Tags []Tag `json:"tags"`
Template bool `json:"template"`
Type GuestType `json:"type"`
UptimeInSeconds uint `json:"uptime"`
Expand Down Expand Up @@ -85,7 +84,7 @@ func (GuestResource) mapToStruct(params []interface{}) []GuestResource {
resources[i].Status = tmpParams["status"].(string)
}
if _, isSet := tmpParams["tags"]; isSet {
resources[i].Tags = strings.Split(tmpParams["tags"].(string), ";")
resources[i].Tags = Tag("").mapToSDK(tmpParams["tags"].(string))
}
if _, isSet := tmpParams["template"]; isSet {
resources[i].Template = Itob(int(tmpParams["template"].(float64)))
Expand Down
8 changes: 4 additions & 4 deletions proxmox/config_guest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func Test_GuestResource_mapToStruct(t *testing.T) {
},
{name: "Tags",
input: []interface{}{map[string]interface{}{"tags": "tag1;tag2;tag3"}},
output: []GuestResource{{Tags: []string{"tag1", "tag2", "tag3"}}},
output: []GuestResource{{Tags: []Tag{"tag1", "tag2", "tag3"}}},
},
{name: "Template",
input: []interface{}{map[string]interface{}{"template": float64(1)}},
Expand Down Expand Up @@ -171,7 +171,7 @@ func Test_GuestResource_mapToStruct(t *testing.T) {
NetworkOut: 1000123465987,
Node: "pve1",
Status: "running",
Tags: []string{"tag1", "tag2", "tag3"},
Tags: []Tag{"tag1", "tag2", "tag3"},
Template: false,
Type: GuestQemu,
UptimeInSeconds: 72169,
Expand All @@ -192,7 +192,7 @@ func Test_GuestResource_mapToStruct(t *testing.T) {
NetworkOut: 88775378423476,
Node: "pve2",
Status: "running",
Tags: []string{"dev"},
Tags: []Tag{"dev"},
Template: false,
Type: GuestLXC,
UptimeInSeconds: 88678345,
Expand All @@ -213,7 +213,7 @@ func Test_GuestResource_mapToStruct(t *testing.T) {
NetworkOut: 1000123465987,
Node: "node3",
Status: "stopped",
Tags: []string{"template"},
Tags: []Tag{"template"},
Template: true,
Type: GuestQemu,
UptimeInSeconds: 0,
Expand Down
14 changes: 10 additions & 4 deletions proxmox/config_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ type ConfigQemu struct {
Startup string `json:"startup,omitempty"` // TODO should be a struct?
TPM *TpmState `json:"tpm,omitempty"`
Tablet *bool `json:"tablet,omitempty"`
Tags string `json:"tags,omitempty"` // TODO should be an array of a custom type as there are character and length limitations
Tags *[]Tag `json:"tags,omitempty"`
VmID int `json:"vmid,omitempty"` // TODO should be a custom type as there are limitations
}

Expand Down Expand Up @@ -269,8 +269,8 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire
if config.Tablet != nil {
params["tablet"] = *config.Tablet
}
if config.Tags != "" {
params["tags"] = config.Tags
if config.Tags != nil {
params["tags"] = Tag("").mapToApi(*config.Tags)
}
if config.QemuVcpus >= 1 {
params["vcpus"] = config.QemuVcpus
Expand Down Expand Up @@ -477,7 +477,8 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi
config.Tablet = util.Pointer(Itob(int(params["tablet"].(float64))))
}
if _, isSet := params["tags"]; isSet {
config.Tags = strings.TrimSpace(params["tags"].(string))
tmpTags := Tag("").mapToSDK(params["tags"].(string))
config.Tags = &tmpTags
}
if _, isSet := params["smbios1"]; isSet {
config.Smbios1 = params["smbios1"].(string)
Expand Down Expand Up @@ -910,6 +911,11 @@ func (config ConfigQemu) Validate(current *ConfigQemu) (err error) {
}
}
}
if config.Tags != nil {
if err := Tag("").validate(*config.Tags); err != nil {
return err
}
}

return
}
Expand Down
44 changes: 44 additions & 0 deletions proxmox/config_qemu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/Telmate/proxmox-api-go/internal/util"
"github.com/Telmate/proxmox-api-go/test/data/test_data_qemu"
"github.com/Telmate/proxmox-api-go/test/data/test_data_tag"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -1302,6 +1303,15 @@ func Test_ConfigQemu_mapToApiValues(t *testing.T) {
config: &ConfigQemu{Iso: &IsoFile{Storage: "test", File: "file.iso"}},
output: map[string]interface{}{"ide2": "test:iso/file.iso,media=cdrom"},
},
// Create Tags
{name: `Create Tags Empty`,
config: &ConfigQemu{Tags: util.Pointer([]Tag{})},
output: map[string]interface{}{"tags": string("")},
},
{name: `Create Tags Full`,
config: &ConfigQemu{Tags: util.Pointer([]Tag{"tag1", "tag2"})},
output: map[string]interface{}{"tags": string("tag1;tag2")},
},
// Create TPM
{name: "Create TPM",
config: &ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion_2_0)}},
Expand Down Expand Up @@ -3367,6 +3377,15 @@ func Test_ConfigQemu_mapToApiValues(t *testing.T) {
config: &ConfigQemu{Iso: &IsoFile{Storage: "NewStorage", File: "file.iso"}},
output: map[string]interface{}{"ide2": "NewStorage:iso/file.iso,media=cdrom"},
},
// Update Tags
{name: `Update Tags Empty`,
config: &ConfigQemu{Tags: util.Pointer([]Tag{})},
output: map[string]interface{}{"tags": string("")},
},
{name: `Update Tags Full`,
config: &ConfigQemu{Tags: util.Pointer([]Tag{"tag1", "tag2"})},
output: map[string]interface{}{"tags": string("tag1;tag2")},
},
// Update TPM
{name: "Update TPM",
config: &ConfigQemu{TPM: &TpmState{Storage: "aaaa", Version: util.Pointer(TpmVersion_1_2)}},
Expand Down Expand Up @@ -6046,6 +6065,14 @@ func Test_ConfigQemu_Validate(t *testing.T) {
},
}
validCloudInit := QemuCloudInitDisk{Format: QemuDiskFormat_Raw, Storage: "Test"}
validTags := func() []Tag {
array := test_data_tag.Tag_Legal()
tags := make([]Tag, len(array))
for i, e := range array {
tags[i] = Tag(e)
}
return tags
}
testData := []struct {
name string
input ConfigQemu
Expand Down Expand Up @@ -6164,6 +6191,10 @@ func Test_ConfigQemu_Validate(t *testing.T) {
}}},
}},
},
// Valid Tags
{name: "Valid Tags",
input: ConfigQemu{Tags: util.Pointer(validTags())},
},
// Valid Tpm
{name: "Valid TPM Create",
input: ConfigQemu{TPM: &TpmState{Storage: "test", Version: util.Pointer(TpmVersion("v2.0"))}}},
Expand Down Expand Up @@ -7256,6 +7287,19 @@ func Test_ConfigQemu_Validate(t *testing.T) {
input: ConfigQemu{Disks: &QemuStorages{VirtIO: &QemuVirtIODisks{Disk_13: &QemuVirtIOStorage{Passthrough: &QemuVirtIOPassthrough{File: "/dev/disk/by-id/scsi1", WorldWideName: "0x5004A3B2C1D0E0F1#"}}}}},
err: errors.New(Error_QemuWorldWideName_Invalid),
},
// Invalid Tags
{name: `Invalid Tags errors.New(Tag_Error_Invalid)`,
input: ConfigQemu{Tags: util.Pointer([]Tag{Tag(test_data_tag.Tag_Illegal())})},
err: errors.New(Tag_Error_Invalid)},
{name: `Invalid Tags errors.New(Tag_Error_Duplicate)`,
input: ConfigQemu{Tags: util.Pointer([]Tag{Tag(test_data_tag.Tag_Max_Legal()), Tag(test_data_tag.Tag_Max_Legal())})},
err: errors.New(Tag_Error_Duplicate)},
{name: `Invalid Tags errors.New(Tag_Error_Empty)`,
input: ConfigQemu{Tags: util.Pointer([]Tag{Tag(test_data_tag.Tag_Empty())})},
err: errors.New(Tag_Error_Empty)},
{name: `Invalid Tags errors.New(Tag_Error_MaxLength)`,
input: ConfigQemu{Tags: util.Pointer([]Tag{Tag(test_data_tag.Tag_Max_Illegal())})},
err: errors.New(Tag_Error_MaxLength)},
// invalid TMP
{name: "Invalid TPM errors.New(storage is required) Create",
input: ConfigQemu{TPM: &TpmState{Storage: ""}},
Expand Down
70 changes: 70 additions & 0 deletions proxmox/type_tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package proxmox

import (
"errors"
"regexp"
"strings"
)

type Tag string

var (
regexTag = regexp.MustCompile(`^[a-z0-9_]+$`)
)

const (
Tag_Error_Invalid string = "tag may only include the following characters: abcdefghijklmnopqrstuvwxyz0123456789_"
Tag_Error_Duplicate string = "duplicate tag found"
Tag_Error_MaxLength string = "tag may only be 124 characters"
Tag_Error_Empty string = "tag may not be empty"
)

func (Tag) mapToApi(tags []Tag) string {
if len(tags) == 0 {
return ""
}
tagsString := ""
for _, e := range tags {
tagsString += string(e) + ";"
}
return tagsString[:len(tagsString)-1]
}

func (Tag) mapToSDK(tags string) []Tag {
tmpTags := strings.Split(tags, ";")
typedTags := make([]Tag, len(tmpTags))
for i, e := range tmpTags {
typedTags[i] = Tag(e)
}
return typedTags
}

func (Tag) validate(tags []Tag) error {
if len(tags) == 0 {
return nil
}
for i, e := range tags {
if err := e.Validate(); err != nil {
return err
}
for j := i + 1; j < len(tags); j++ {
if e == tags[j] {
return errors.New(Tag_Error_Duplicate)
}
}
}
return nil
}

func (t Tag) Validate() error {
if len(t) == 0 {
return errors.New(Tag_Error_Empty)
}
if len(t) > 124 {
return errors.New(Tag_Error_MaxLength)
}
if !regexTag.MatchString(string(t)) {
return errors.New(Tag_Error_Invalid)
}
return nil
}
41 changes: 41 additions & 0 deletions proxmox/type_tag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package proxmox

import (
"errors"
"testing"

"github.com/Telmate/proxmox-api-go/test/data/test_data_tag"
"github.com/stretchr/testify/require"
)

func Test_Tag_Validate(t *testing.T) {
tests := []struct {
name string
input []string
output error
}{
{name: `Valid Tag`,
input: test_data_tag.Tag_Legal(),
output: nil,
},
{name: `Invalid Tag`,
input: test_data_tag.Tag_Character_Illegal(),
output: errors.New(Tag_Error_Invalid),
},
{name: `Invalid Tag Empty`,
input: []string{test_data_tag.Tag_Empty()},
output: errors.New(Tag_Error_Empty),
},
{name: `Invalid Tag Max Length`,
input: []string{test_data_tag.Tag_Max_Illegal()},
output: errors.New(Tag_Error_MaxLength),
},
}
for _, test := range tests {
for _, e := range test.input {
t.Run(test.name+": "+e, func(t *testing.T) {
require.Equal(t, test.output, (Tag(e)).Validate())
})
}
}
}
68 changes: 68 additions & 0 deletions test/data/test_data_tag/type_Tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package test_data_tag

// illegal character
func Tag_Character_Illegal() []string {
return append([]string{
`Tag!`,
`#InvalidTag`,
`$MoneyTag`,
`Tag@`,
`Tag with space`,
`Tag&`,
`Tag*Name`,
`Tag-Name`,
`Tag#Name`,
`Tag(Name)`,
`Tag/Name`,
`Tag|Name`,
`Tag[Name]`,
`Tag{Name}`,
`Tag=Name`,
`Tag+Name`,
`Tag'Name`,
`Tag~Name`,
`Name<Tag`,
`dash-first`,
`tag.with.dot`,
`tag,with,comma`,
`tag:name`,
`Tag?`,
`Tag[Bracket]`,
`Tag{Name}`,
`!InvalidTag`,
}, Tag_Illegal())
}

func Tag_Illegal() string {
return "!@^$^&$^&"
}

func Tag_Empty() string {
return ""
}

func Tag_Max_Illegal() string {
return Tag_Max_Legal() + "A"
}

func Tag_Max_Legal() string {
return "abcdefghijklmnopqrstuvqxyz0123456789_abcdefghijklmnopqrstuvqxyz0123456789_abcdefghijklmnopqrstuvqxyz0123456789_abcdefghijklm"
}

func Tag_Legal() []string {
return append([]string{
`tag1`,
`tag2`,
`tag3`,
`my_tag`,
`important_tag`,
`tech`,
`science`,
`art`,
`music`,
`coding`,
`programming`,
`python`,
`72d1109e_97f6_41e7_96cc_18a8b7dc19dc`,
}, Tag_Max_Legal())
}
Loading