diff --git a/go.mod b/go.mod index fc238480..3f4871e8 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 toolchain go1.21.0 require ( - github.com/Telmate/proxmox-api-go v0.0.0-20240513153448-8ecac06c5879 + github.com/Telmate/proxmox-api-go v0.0.0-20240515161628-30cfc1aa4ae4 github.com/google/uuid v1.6.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 diff --git a/go.sum b/go.sum index 9122548b..8528bc91 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.1.0-alpha.2-proton h1:HKz85FwoXx86kVtTvFke7rgHvq/HoloSUvW5semjFWs= github.com/ProtonMail/go-crypto v1.1.0-alpha.2-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/Telmate/proxmox-api-go v0.0.0-20240513153448-8ecac06c5879 h1:RHyitzMb2aEtzMgNy80TOnkbvvjZABBWzyhHy+xHMQA= -github.com/Telmate/proxmox-api-go v0.0.0-20240513153448-8ecac06c5879/go.mod h1:bscBzOUx0tJAdVGmQvcnoWPg5eI2eJ6anJKV1ueZ1oU= +github.com/Telmate/proxmox-api-go v0.0.0-20240515161628-30cfc1aa4ae4 h1:UsCuxsMGA0ePmUBYaYNvh1kFxoZ35ARpHCfEw9KZxec= +github.com/Telmate/proxmox-api-go v0.0.0-20240515161628-30cfc1aa4ae4/go.mod h1:bscBzOUx0tJAdVGmQvcnoWPg5eI2eJ6anJKV1ueZ1oU= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= diff --git a/proxmox/Internal/pxapi/guest/tags/tags.go b/proxmox/Internal/pxapi/guest/tags/tags.go new file mode 100644 index 00000000..c97e8e84 --- /dev/null +++ b/proxmox/Internal/pxapi/guest/tags/tags.go @@ -0,0 +1,93 @@ +package tags + +import ( + "sort" + "strings" + + pxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// Returns an unordered list of unique tags +func RemoveDuplicates(tags *[]pxapi.Tag) *[]pxapi.Tag { + if tags == nil || len(*tags) == 0 { + return nil + } + tagMap := make(map[pxapi.Tag]struct{}) + for _, tag := range *tags { + tagMap[tag] = struct{}{} + } + uniqueTags := make([]pxapi.Tag, len(tagMap)) + var index uint + for tag := range tagMap { + uniqueTags[index] = tag + index++ + } + return &uniqueTags +} + +func Schema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + v, ok := i.(string) + if !ok { + return diag.Errorf("expected a string, got: %s", i) + } + for _, e := range *Split(v) { + if err := e.Validate(); err != nil { + return diag.Errorf("tag validation failed: %s", err) + } + } + return nil + }, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return String(sortArray(RemoveDuplicates(Split(old)))) == String(sortArray(RemoveDuplicates(Split(new)))) + }, + } +} + +func sortArray(tags *[]pxapi.Tag) *[]pxapi.Tag { + if tags == nil || len(*tags) == 0 { + return nil + } + sort.SliceStable(*tags, func(i, j int) bool { + return (*tags)[i] < (*tags)[j] + }) + return tags +} + +func Split(rawTags string) *[]pxapi.Tag { + tags := make([]pxapi.Tag, 0) + if rawTags == "" { + return &tags + } + tagArrays := strings.Split(rawTags, ";") + for _, tag := range tagArrays { + tagSubArrays := strings.Split(tag, ",") + if len(tagSubArrays) > 1 { + tmpTags := make([]pxapi.Tag, len(tagSubArrays)) + for i, e := range tagSubArrays { + tmpTags[i] = pxapi.Tag(e) + } + tags = append(tags, tmpTags...) + } else { + tags = append(tags, pxapi.Tag(tag)) + } + } + return &tags +} + +func String(tags *[]pxapi.Tag) (tagList string) { + if tags == nil || len(*tags) == 0 { + return "" + } + for _, tag := range *tags { + tagList += ";" + string(tag) + } + return tagList[1:] +} diff --git a/proxmox/Internal/pxapi/guest/tags/tags_test.go b/proxmox/Internal/pxapi/guest/tags/tags_test.go new file mode 100644 index 00000000..d4e314df --- /dev/null +++ b/proxmox/Internal/pxapi/guest/tags/tags_test.go @@ -0,0 +1,82 @@ +package tags + +import ( + "testing" + + pxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/stretchr/testify/require" +) + +func Test_RemoveDuplicates(t *testing.T) { + tests := []struct { + name string + input *[]pxapi.Tag + output *[]pxapi.Tag + }{ + {name: `nil`}, + {name: `empty`, input: &[]pxapi.Tag{}}, + {name: `single`, input: &[]pxapi.Tag{"a"}, output: &[]pxapi.Tag{"a"}}, + {name: `multiple`, input: &[]pxapi.Tag{"b", "a", "c"}, output: &[]pxapi.Tag{"a", "b", "c"}}, + {name: `duplicate`, input: &[]pxapi.Tag{"b", "a", "c", "b", "a"}, output: &[]pxapi.Tag{"a", "b", "c"}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, sortArray(RemoveDuplicates(test.input))) + }) + } +} + +func Test_sort(t *testing.T) { + tests := []struct { + name string + input *[]pxapi.Tag + output *[]pxapi.Tag + }{ + {name: `nil`}, + {name: `empty`, input: &[]pxapi.Tag{}}, + {name: `single`, input: &[]pxapi.Tag{"a"}, output: &[]pxapi.Tag{"a"}}, + {name: `multiple`, input: &[]pxapi.Tag{"b", "a", "c"}, output: &[]pxapi.Tag{"a", "b", "c"}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, sortArray(test.input)) + }) + } +} + +func Test_Split(t *testing.T) { + tests := []struct { + name string + input string + output *[]pxapi.Tag + }{ + {name: `empty`, output: &[]pxapi.Tag{}}, + {name: `single`, input: "a", output: &[]pxapi.Tag{"a"}}, + {name: `multiple ,`, input: "b,a,c", output: &[]pxapi.Tag{"b", "a", "c"}}, + {name: `multiple ;`, input: "b;a;c", output: &[]pxapi.Tag{"b", "a", "c"}}, + {name: `multiple mixed`, input: "b,a;c,d;e", output: &[]pxapi.Tag{"b", "a", "c", "d", "e"}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, Split(test.input)) + }) + } +} + +func Test_String(t *testing.T) { + tests := []struct { + name string + input *[]pxapi.Tag + output string + }{ + {name: `nil`}, + {name: `empty`, input: &[]pxapi.Tag{}}, + {name: `single`, input: &[]pxapi.Tag{"a"}, output: "a"}, + {name: `multiple`, input: &[]pxapi.Tag{"b", "a", "c"}, output: "b;a;c"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.output, String(test.input)) + }) + } +} diff --git a/proxmox/resource_lxc.go b/proxmox/resource_lxc.go index 93ccd6ba..aa1d7098 100644 --- a/proxmox/resource_lxc.go +++ b/proxmox/resource_lxc.go @@ -8,11 +8,13 @@ import ( "time" pxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/Telmate/terraform-provider-proxmox/v2/proxmox/Internal/pxapi/guest/tags" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) var lxcResourceDef *schema.Resource +// TODO update tag schema func resourceLxc() *schema.Resource { lxcResourceDef = &schema.Resource{ Create: resourceLxcCreate, @@ -142,10 +144,7 @@ func resourceLxc() *schema.Resource { Type: schema.TypeString, Optional: true, }, - "tags": { - Type: schema.TypeString, - Optional: true, - }, + "tags": tags.Schema(), "memory": { Type: schema.TypeInt, Optional: true, @@ -488,7 +487,7 @@ func resourceLxcCreate(d *schema.ResourceData, meta interface{}) error { config.Start = d.Get("start").(bool) config.Startup = d.Get("startup").(string) config.Swap = d.Get("swap").(int) - config.Tags = d.Get("tags").(string) + config.Tags = tags.String(tags.RemoveDuplicates(tags.Split(d.Get("tags").(string)))) config.Template = d.Get("template").(bool) config.Tty = d.Get("tty").(int) config.Unique = d.Get("unique").(bool) @@ -659,7 +658,7 @@ func resourceLxcUpdate(d *schema.ResourceData, meta interface{}) error { config.Start = d.Get("start").(bool) config.Startup = d.Get("startup").(string) config.Swap = d.Get("swap").(int) - config.Tags = d.Get("tags").(string) + config.Tags = tags.String(tags.RemoveDuplicates(tags.Split(d.Get("tags").(string)))) config.Template = d.Get("template").(bool) config.Tty = d.Get("tty").(int) config.Unique = d.Get("unique").(bool) @@ -884,7 +883,7 @@ func _resourceLxcRead(d *schema.ResourceData, meta interface{}) error { d.Set("searchdomain", config.SearchDomain) d.Set("startup", config.Startup) d.Set("swap", config.Swap) - d.Set("tags", config.Tags) + d.Set("tags", tags.String(tags.Split(config.Tags))) d.Set("template", config.Template) d.Set("tty", config.Tty) d.Set("unique", config.Unique) diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go index 1ce58a86..7d0cb2b4 100755 --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -15,6 +15,7 @@ import ( "time" pxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/Telmate/terraform-provider-proxmox/v2/proxmox/Internal/pxapi/guest/tags" "github.com/google/uuid" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -215,11 +216,7 @@ func resourceVmQemu() *schema.Resource { return strings.TrimSpace(old) == strings.TrimSpace(new) }, }, - "tags": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, + "tags": tags.Schema(), "args": { Type: schema.TypeString, Optional: true, @@ -891,7 +888,7 @@ func resourceVmQemuCreate(ctx context.Context, d *schema.ResourceData, meta inte HaState: d.Get("hastate").(string), HaGroup: d.Get("hagroup").(string), QemuOs: d.Get("qemu_os").(string), - Tags: d.Get("tags").(string), + Tags: tags.RemoveDuplicates(tags.Split(d.Get("tags").(string))), Args: d.Get("args").(string), QemuNetworks: qemuNetworks, QemuSerials: qemuSerials, @@ -1168,7 +1165,7 @@ func resourceVmQemuUpdate(ctx context.Context, d *schema.ResourceData, meta inte HaState: d.Get("hastate").(string), HaGroup: d.Get("hagroup").(string), QemuOs: d.Get("qemu_os").(string), - Tags: d.Get("tags").(string), + Tags: tags.RemoveDuplicates(tags.Split(d.Get("tags").(string))), Args: d.Get("args").(string), QemuNetworks: qemuNetworks, QemuSerials: qemuSerials, @@ -1481,7 +1478,7 @@ func resourceVmQemuRead(ctx context.Context, d *schema.ResourceData, meta interf d.Set("hastate", vmr.HaState()) d.Set("hagroup", vmr.HaGroup()) d.Set("qemu_os", config.QemuOs) - d.Set("tags", config.Tags) + d.Set("tags", tags.String(config.Tags)) d.Set("args", config.Args) // Cloud-init. d.Set("ciuser", config.CIuser)