Skip to content

Commit 245ce5d

Browse files
committed
feat(tag_attachment resource): ZSTAC-75183. add tag_attachment resource. Supports attaching tags to multiple resources, with or without tokens
1 parent 179c58b commit 245ce5d

File tree

9 files changed

+304
-13
lines changed

9 files changed

+304
-13
lines changed

docs/resources/tag_attachment.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
page_title: "zstack_tag_attachment Resource - terraform-provider-zstack"
3+
subcategory: ""
4+
description: |-
5+
ZStack Tag Attach to Resource
6+
---
7+
8+
# zstack_tag_attachment (Resource)
9+
10+
ZStack Tag Attach to Resource
11+
12+
## Example Usage
13+
14+
```terraform
15+
resource "zstack_tag_attachment" "test" {
16+
17+
tag_uuid = "4c49107f43554d87bb4163aa7d9f205d"
18+
resource_uuids = [
19+
"c24847378787471f85887e875c3d9064"
20+
]
21+
22+
# tokens = {
23+
# performance = "high"
24+
# }
25+
}
26+
27+
output "zstack_tag_attach" {
28+
value = zstack_tag_attachment.test
29+
}
30+
```
31+
32+
<!-- schema generated by tfplugindocs -->
33+
## Schema
34+
35+
### Required
36+
37+
- `resource_uuids` (List of String) The uuid of the resource.
38+
- `tag_uuid` (String) The UUID of the tag.
39+
40+
### Optional
41+
42+
- `tokens` (Map of String) If attach type is 'withToken', you must set this. For simple attach, leave it empty or omit.
43+
44+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
resource "zstack_tag_attachment" "test" {
2+
3+
tag_uuid = "4c49107f43554d87bb4163aa7d9f205d"
4+
resource_uuids = [
5+
"c24847378787471f85887e875c3d9064"
6+
]
7+
8+
# tokens = {
9+
# performance = "high"
10+
# }
11+
}
12+
13+
output "zstack_tag_attach" {
14+
value = zstack_tag_attachment.test
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}"
3+
subcategory: ""
4+
description: |-
5+
{{ .Description | plainmarkdown | trimspace | prefixlines " " }}
6+
---
7+
8+
# {{.Name}} ({{.Type}})
9+
10+
{{ .Description }}
11+
12+
## Example Usage
13+
14+
{{tffile "examples/resources/tag_attachment/resource.tf"}}
15+
16+
{{ .SchemaMarkdown }}

zstack-sdk-go/pkg/client/tags_actions.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ func (cli *ZSClient) GetTag(uuid string) (view.TagInventoryView, error) {
3636
return resp, cli.Get("v1/tags/", uuid, nil, &resp)
3737
}
3838

39+
func (cli *ZSClient) GetUserTag(uuid string) ([]view.TagInventory, error) {
40+
var tags []view.TagInventory
41+
queryParam := param.NewQueryParam()
42+
queryParam.AddQ("tagPatternUuid=" + uuid)
43+
return tags, cli.ListAll("v1/user-tags", &queryParam, &tags)
44+
}
45+
3946
// QueryTag
4047
func (cli *ZSClient) QueryTag(params param.QueryParam) ([]view.TagInventoryView, error) {
4148
var tags []view.TagInventoryView

zstack-sdk-go/pkg/test1/base_actions_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const (
1515
// The ZStack Cloud Basic Edition supports login authentication for AccessKey, super admin, and sub-accounts.
1616
// The ZStack Cloud Enterprise Edition supports login authentication for AccessKey, super admin, sub-accounts, and enterprise users.
1717

18-
accountLoginHostname = "172.24.190.38" //ZStack Cloud API endpoint IP address
18+
accountLoginHostname = "172.30.3.3" //ZStack Cloud API endpoint IP address
1919
accountLoginAccountName = "admin"
2020
accountLoginAccountPassword = "password"
2121

zstack-sdk-go/pkg/test1/user_tags_actions_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import (
1010
"zstack.io/zstack-sdk-go/pkg/param"
1111
)
1212

13-
func TestZSClient_GetUserTag(t *testing.T) {
13+
func TestZSClient_QueryUserTag(t *testing.T) {
1414
queryParam := param.NewQueryParam()
15-
queryParam.AddQ("resourceUuid=1e7e24218e314b2b80f3c0e655efc3cf")
15+
queryParam.AddQ("tagPatternUuid=fbe3c0f934ce417995cebbe79d415cc0")
1616
tags, err := accountLoginCli.QueryUserTag(queryParam)
1717
if err != nil {
1818
t.Errorf("TestQuerySystemTags %v", err)
@@ -99,13 +99,21 @@ func TestListAllTags(t *testing.T) {
9999
}
100100

101101
func TestGetTag(t *testing.T) {
102-
tag, err := accountLoginCli.GetTag("0b0a50adfcba457db09629b3c365d66f")
102+
tag, err := accountLoginCli.GetTag("15b4769130f542b18e0296da4ef2ef80")
103103
if err != nil {
104104
t.Errorf("TestGetTag %v", err)
105105
}
106106
golog.Info(tag)
107107
}
108108

109+
func TestGetUserTag(t *testing.T) {
110+
tag, err := accountLoginCli.GetUserTag("15b4769130f542b18e0296da4ef2ef80")
111+
if err != nil {
112+
t.Errorf("TestGetUserTag %v", err)
113+
}
114+
golog.Info(tag)
115+
}
116+
109117
func TestAttachTagToResource(t *testing.T) {
110118
tag, err := accountLoginCli.AttachTagToResource("3d7ae53107994d36a202bdf704c007b5", []string{"1e7e24218e314b2b80f3c0e655efc3cf"})
111119
if err != nil {
@@ -115,7 +123,7 @@ func TestAttachTagToResource(t *testing.T) {
115123
}
116124

117125
func TestAttachTagToResourceWithToken(t *testing.T) {
118-
tag, err := accountLoginCli.AttachTagToResource("0b0a50adfcba457db09629b3c365d66f", []string{"1e7e24218e314b2b80f3c0e655efc3cf"}, "withToken", `{"performance1":"low"}`)
126+
tag, err := accountLoginCli.AttachTagToResource("fbe3c0f934ce417995cebbe79d415cc0", []string{"1bfd7c3dd7ba48b29b9c13cf26ec7037"}, "withToken", `{"performance1":"low"}`)
119127
if err != nil {
120128
t.Errorf("TestAttachTagToResourceWithToken %v", err)
121129
}

zstack-sdk-go/pkg/view/tag_views.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,13 @@ type TagInventory struct {
3838
}
3939

4040
type TagPatternView struct {
41-
Uuid string `json:"uuid"` // Tag pattern UUID
42-
Name string `json:"name"` // Name of the tag pattern
43-
Value string `json:"value"` // Actual tag value (e.g. withToken::{xxx})
44-
Description string `json:"description"` // Description of the tag
45-
Color string `json:"color"` // Color associated with the tag
46-
Type string `json:"type"` // Tag type: simple | withToken
47-
CreateDate string `json:"createDate"` // Creation timestamp
48-
LastOpDate string `json:"lastOpDate"` // Last update timestamp
41+
Uuid string `json:"uuid"` // Tag pattern UUID
42+
Name string `json:"name"` // Name of the tag pattern
43+
Value string `json:"value"` // Actual tag value (e.g. withToken::{xxx})
44+
Color string `json:"color"` // Color associated with the tag
45+
Type string `json:"type"` // Tag type: simple | withToken
46+
CreateDate string `json:"createDate"` // Creation timestamp
47+
LastOpDate string `json:"lastOpDate"` // Last update timestamp
4948
}
5049

5150
type AttachTagToResourceResult struct {

zstack/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ func (p *ZStackProvider) Resources(ctx context.Context) []func() resource.Resour
298298
ScriptResource,
299299
ScriptExecutionResource,
300300
TagResource,
301+
TagAttachmentResource,
301302
}
302303
}
303304

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Copyright (c) ZStack.io, Inc.
2+
3+
package provider
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/attr"
11+
"github.com/hashicorp/terraform-plugin-framework/resource"
12+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
14+
"zstack.io/zstack-sdk-go/pkg/client"
15+
)
16+
17+
var (
18+
_ resource.Resource = &tagAttachmentResource{}
19+
_ resource.ResourceWithConfigure = &tagAttachmentResource{}
20+
)
21+
22+
type tagAttachmentResource struct {
23+
client *client.ZSClient
24+
}
25+
26+
type tagAttachmentModel struct {
27+
//ID types.String `tfsdk:"id"`
28+
TagUuid types.String `tfsdk:"tag_uuid"`
29+
ResourceUuids types.List `tfsdk:"resource_uuids"`
30+
Tokens types.Map `tfsdk:"tokens"`
31+
}
32+
33+
func TagAttachmentResource() resource.Resource {
34+
return &tagAttachmentResource{}
35+
}
36+
37+
func (r *tagAttachmentResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) {
38+
if request.ProviderData == nil {
39+
return
40+
}
41+
42+
client, ok := request.ProviderData.(*client.ZSClient)
43+
if !ok {
44+
response.Diagnostics.AddError(
45+
"Unexpected Data Source Configure Type",
46+
fmt.Sprintf("Expected *client.ZSClient, got: %T. Please report this issue to the Provider developer. ", request.ProviderData),
47+
)
48+
49+
return
50+
}
51+
r.client = client
52+
}
53+
54+
func (r *tagAttachmentResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) {
55+
response.TypeName = request.ProviderTypeName + "_tag_attachment"
56+
}
57+
58+
func (r *tagAttachmentResource) Schema(_ context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) {
59+
response.Schema = schema.Schema{
60+
Description: "ZStack Tag Attach to Resource",
61+
Attributes: map[string]schema.Attribute{
62+
"tag_uuid": schema.StringAttribute{
63+
Required: true,
64+
Description: "The UUID of the tag.",
65+
},
66+
"resource_uuids": schema.ListAttribute{
67+
Required: true,
68+
ElementType: types.StringType,
69+
Description: "The uuid of the resource.",
70+
},
71+
"tokens": schema.MapAttribute{
72+
Optional: true,
73+
ElementType: types.StringType,
74+
Description: "If attach type is 'withToken', you must set this. For simple attach, leave it empty or omit.",
75+
},
76+
},
77+
}
78+
}
79+
80+
func (r *tagAttachmentResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
81+
var plan tagAttachmentModel
82+
diags := request.Plan.Get(ctx, &plan)
83+
response.Diagnostics.Append(diags...)
84+
if response.Diagnostics.HasError() {
85+
return
86+
}
87+
88+
if r.client == nil {
89+
response.Diagnostics.AddWarning("Client Not Configured", "The client was not properly configured.")
90+
return
91+
}
92+
93+
resourceUuids := make([]string, 0, len(plan.ResourceUuids.Elements()))
94+
for _, v := range plan.ResourceUuids.Elements() {
95+
resourceUuids = append(resourceUuids, v.(types.String).ValueString())
96+
}
97+
98+
attachType := []string{}
99+
if !plan.Tokens.IsNull() && len(plan.Tokens.Elements()) > 0 {
100+
tokenMap := make(map[string]interface{})
101+
for k, v := range plan.Tokens.Elements() {
102+
tokenMap[k] = v.(types.String).ValueString()
103+
}
104+
tokenJson, err := json.Marshal(tokenMap)
105+
if err != nil {
106+
response.Diagnostics.AddError("Error marshaling tokens", err.Error())
107+
return
108+
}
109+
attachType = []string{"withToken", string(tokenJson)}
110+
}
111+
112+
_, err := r.client.AttachTagToResource(plan.TagUuid.ValueString(), resourceUuids, attachType...)
113+
if err != nil {
114+
response.Diagnostics.AddError("Error attaching tag", err.Error())
115+
return
116+
}
117+
118+
//id := fmt.Sprintf("%s|%s", plan.TagUuid.ValueString(), strings.Join(resourceUuids, ","))
119+
response.State.Set(ctx, &tagAttachmentModel{
120+
TagUuid: plan.TagUuid,
121+
ResourceUuids: plan.ResourceUuids,
122+
Tokens: plan.Tokens,
123+
})
124+
125+
}
126+
127+
func (r *tagAttachmentResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
128+
129+
var state tagAttachmentModel
130+
diags := request.State.Get(ctx, &state)
131+
response.Diagnostics.Append(diags...)
132+
if response.Diagnostics.HasError() {
133+
return
134+
}
135+
136+
if r.client == nil {
137+
response.Diagnostics.AddWarning("Client Not Configured", "The client was not properly configured.")
138+
return
139+
}
140+
141+
tagUuid := state.TagUuid.ValueString()
142+
if tagUuid == "" {
143+
response.Diagnostics.AddError("Tag UUID is empty", "Cannot read tag attachment without a valid tag UUID.")
144+
return
145+
}
146+
result, err := r.client.GetUserTag(tagUuid)
147+
if err != nil {
148+
response.Diagnostics.AddError(
149+
"Unable to Read Tag Binding",
150+
fmt.Sprintf("Error retrieving tag binding for tag UUID %s: %s", tagUuid, err),
151+
)
152+
return
153+
}
154+
if len(result) == 0 {
155+
response.State.RemoveResource(ctx)
156+
return
157+
}
158+
159+
var resourceUuids []string
160+
for _, item := range result {
161+
resourceUuids = append(resourceUuids, item.ResourceUuid)
162+
}
163+
164+
// Update state.ResourceUuids
165+
var resourceUuidsValues []attr.Value
166+
for _, uuid := range resourceUuids {
167+
resourceUuidsValues = append(resourceUuidsValues, types.StringValue(uuid))
168+
}
169+
170+
state.ResourceUuids = types.ListValueMust(types.StringType, resourceUuidsValues)
171+
state.TagUuid = types.StringValue(tagUuid)
172+
173+
diags = response.State.Set(ctx, &state)
174+
response.Diagnostics.Append(diags...)
175+
176+
}
177+
178+
func (r *tagAttachmentResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
179+
response.Diagnostics.AddWarning("Update not supported", "Please recreate the resource if you need changes")
180+
}
181+
182+
func (r *tagAttachmentResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
183+
var state tagAttachmentModel
184+
diags := request.State.Get(ctx, &state)
185+
response.Diagnostics.Append(diags...)
186+
if response.Diagnostics.HasError() {
187+
return
188+
}
189+
190+
resourceUuids := make([]string, 0, len(state.ResourceUuids.Elements()))
191+
for _, v := range state.ResourceUuids.Elements() {
192+
resourceUuids = append(resourceUuids, v.(types.String).ValueString())
193+
}
194+
195+
err := r.client.DetachTagFromResource(state.TagUuid.ValueString(), resourceUuids)
196+
if err != nil {
197+
response.Diagnostics.AddError("Error detaching tag", err.Error())
198+
return
199+
}
200+
201+
}

0 commit comments

Comments
 (0)