Skip to content

Commit 9af4e3f

Browse files
authored
Merge pull request #5884 from WowVeryLogin/denis/FLPROD-1586
Get cloudflare_snippet resource working
2 parents dc21aba + 66e985b commit 9af4e3f

File tree

10 files changed

+338
-45
lines changed

10 files changed

+338
-45
lines changed

docs/resources/snippet.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
page_title: "cloudflare_snippet Resource - Cloudflare"
3+
subcategory: ""
4+
description: |-
5+
6+
---
7+
8+
# cloudflare_snippet (Resource)
9+
10+
11+
12+
## Example Usage
13+
14+
```terraform
15+
resource "cloudflare_snippet" "example_snippet" {
16+
zone_id = "9f1839b6152d298aca64c4e906b6d074"
17+
snippet_name = "my_snippet"
18+
files = [
19+
{
20+
name = "main.js"
21+
content = <<-EOT
22+
export default {
23+
async fetch(request) {
24+
return new Response('Hello, World!');
25+
}
26+
}
27+
EOT
28+
}
29+
]
30+
metadata = {
31+
main_module = "main.js"
32+
}
33+
}
34+
```
35+
36+
<!-- schema generated by tfplugindocs -->
37+
## Schema
38+
39+
### Required
40+
41+
- `files` (List of String) The list of files belonging to the snippet.
42+
- `metadata` (Attributes) Metadata about the snippet. (see [below for nested schema](#nestedatt--metadata))
43+
- `snippet_name` (String) The identifying name of the snippet.
44+
- `zone_id` (String) The unique ID of the zone.
45+
46+
### Read-Only
47+
48+
- `created_on` (String) The timestamp of when the snippet was created.
49+
- `modified_on` (String) The timestamp of when the snippet was last modified.
50+
51+
<a id="nestedatt--metadata"></a>
52+
### Nested Schema for `metadata`
53+
54+
Required:
55+
56+
- `main_module` (String) Name of the file that contains the main module of the snippet.
57+
58+

examples/resources/cloudflare_snippet/resource.tf

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
resource "cloudflare_snippet" "example_snippet" {
2-
zone_id = "9f1839b6152d298aca64c4e906b6d074"
2+
zone_id = "9f1839b6152d298aca64c4e906b6d074"
33
snippet_name = "my_snippet"
4-
files = [null]
4+
files = [
5+
{
6+
name = "main.js"
7+
content = <<-EOT
8+
export default {
9+
async fetch(request) {
10+
return new Response('Hello, World!');
11+
}
12+
}
13+
EOT
14+
}
15+
]
516
metadata = {
617
main_module = "main.js"
718
}

internal/apiform/encoder.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,10 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
326326
if ptag.name == "-" {
327327
continue
328328
}
329+
// Computed fields come from the server
330+
if ptag.computed && !ptag.forceEncode {
331+
continue
332+
}
329333

330334
dateFormat, ok := parseFormatStructTag(field)
331335
oldFormat := e.dateFormat

internal/apiform/tag.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ const formatStructTag = "format"
1111

1212
type parsedStructTag struct {
1313
name string
14-
required bool
1514
extras bool
16-
metadata bool
15+
computed bool
16+
// Don't skip this value, even if it's computed (no-op for computed optional fields)
17+
// If encodeStateForUnknown is set on a computed field, this flag should also be set;
18+
// otherwise this flag will have no effect
19+
// NOTE: won't work if update behavior is 'patch'
20+
forceEncode bool
1721
}
1822

1923
func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) {
@@ -31,12 +35,12 @@ func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool
3135
tag.name = parts[0]
3236
for _, part := range parts[1:] {
3337
switch part {
34-
case "required":
35-
tag.required = true
3638
case "extras":
3739
tag.extras = true
38-
case "metadata":
39-
tag.metadata = true
40+
case "computed":
41+
tag.computed = true
42+
case "force_encode":
43+
tag.forceEncode = true
4044
}
4145
}
4246
return

internal/services/snippet/model.go

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,39 @@ package snippet
44

55
import (
66
"bytes"
7+
"context"
8+
"fmt"
9+
"io"
10+
"mime"
711
"mime/multipart"
12+
"strings"
813

914
"github.com/cloudflare/terraform-provider-cloudflare/internal/apiform"
1015
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
16+
"github.com/hashicorp/terraform-plugin-framework/attr"
17+
"github.com/hashicorp/terraform-plugin-framework/diag"
1118
"github.com/hashicorp/terraform-plugin-framework/types"
19+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
20+
"github.com/hashicorp/terraform-plugin-go/tftypes"
1221
)
1322

1423
type SnippetResultEnvelope struct {
1524
Result SnippetModel `json:"result"`
1625
}
1726

27+
var SnippetFileType = snippetFileType{
28+
ObjectType: types.ObjectType{
29+
AttrTypes: map[string]attr.Type{
30+
"name": types.StringType,
31+
"content": types.StringType,
32+
},
33+
},
34+
}
35+
1836
type SnippetModel struct {
1937
SnippetName types.String `tfsdk:"snippet_name" path:"snippet_name,required"`
2038
ZoneID types.String `tfsdk:"zone_id" path:"zone_id,required"`
21-
Files *[]types.String `tfsdk:"files" json:"files,required,no_refresh"`
39+
Files *[]SnippetFile `tfsdk:"files" json:"files,metadata,required"`
2240
Metadata *SnippetMetadataModel `tfsdk:"metadata" json:"metadata,required,no_refresh"`
2341
CreatedOn timetypes.RFC3339 `tfsdk:"created_on" json:"created_on,computed" format:"date-time"`
2442
ModifiedOn timetypes.RFC3339 `tfsdk:"modified_on" json:"modified_on,computed" format:"date-time"`
@@ -42,3 +60,130 @@ func (r SnippetModel) MarshalMultipart() (data []byte, contentType string, err e
4260
type SnippetMetadataModel struct {
4361
MainModule types.String `tfsdk:"main_module" json:"main_module,required"`
4462
}
63+
64+
func (r *SnippetModel) UnmarshalMultipart(data []byte, contentType string) error {
65+
mediaType, params, err := mime.ParseMediaType(contentType)
66+
if err != nil {
67+
return fmt.Errorf("failed to parse media type: %w", err)
68+
}
69+
if mediaType != "multipart/form-data" {
70+
return fmt.Errorf("expected media type %q, got %q", "multipart/form-data", mediaType)
71+
}
72+
reader := multipart.NewReader(bytes.NewReader(data), params["boundary"])
73+
var files []SnippetFile
74+
for {
75+
part, err := reader.NextPart()
76+
if err == io.EOF {
77+
break
78+
}
79+
if err != nil {
80+
return fmt.Errorf("failed to get multipart part: %w", err)
81+
}
82+
if part.FormName() == "files" {
83+
bytes, err := io.ReadAll(part)
84+
if err != nil {
85+
return fmt.Errorf("failed to read multipart part: %w", err)
86+
}
87+
files = append(files, NewSnippetsFileValueMust(
88+
part.FileName(),
89+
string(bytes),
90+
))
91+
}
92+
}
93+
r.Files = &files
94+
return nil
95+
}
96+
97+
type snippetFileType struct {
98+
types.ObjectType
99+
}
100+
101+
func (t snippetFileType) Equal(other attr.Type) bool {
102+
_, ok := other.(snippetFileType)
103+
104+
return ok
105+
}
106+
107+
func (t snippetFileType) String() string {
108+
return "SnippetsFileContentType"
109+
}
110+
111+
func (t snippetFileType) ValueFromTerraform(
112+
ctx context.Context,
113+
in tftypes.Value,
114+
) (attr.Value, error) {
115+
val, err := t.ObjectType.ValueFromTerraform(ctx, in)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
obj, ok := val.(types.Object)
121+
if !ok {
122+
return nil, fmt.Errorf("unexpected value type of %T", val)
123+
}
124+
125+
return SnippetFile{obj, new(int64)}, nil
126+
}
127+
128+
func (t snippetFileType) ValueType(_ context.Context) attr.Value {
129+
return SnippetFile{}
130+
}
131+
132+
func (t snippetFileType) ValueFromObject(
133+
_ context.Context,
134+
obj basetypes.ObjectValue,
135+
) (basetypes.ObjectValuable, diag.Diagnostics) {
136+
return SnippetFile{obj, new(int64)}, nil
137+
}
138+
139+
type SnippetFile struct {
140+
types.Object
141+
offset *int64
142+
}
143+
144+
func NewSnippetsFileValueMust(name string, content string) SnippetFile {
145+
return SnippetFile{types.ObjectValueMust(
146+
SnippetFileType.AttrTypes,
147+
map[string]attr.Value{
148+
"name": types.StringValue(name),
149+
"content": types.StringValue(content),
150+
},
151+
), new(int64)}
152+
}
153+
154+
func (f SnippetFile) Type(_ context.Context) attr.Type {
155+
return SnippetFileType
156+
}
157+
158+
func (f SnippetFile) Equal(other attr.Value) bool {
159+
o, ok := other.(SnippetFile)
160+
if !ok {
161+
return false
162+
}
163+
164+
return f.Object.Equal(o.Object)
165+
}
166+
167+
func (f SnippetFile) Name() string {
168+
return f.Object.Attributes()["name"].(types.String).ValueString()
169+
}
170+
171+
func (f SnippetFile) ContentType() string {
172+
return "application/javascript+module"
173+
}
174+
175+
func (f SnippetFile) Read(p []byte) (n int, err error) {
176+
content := f.Object.Attributes()["content"].(types.String).ValueString()
177+
178+
reader := strings.NewReader(content)
179+
180+
if _, err := reader.Seek(*f.offset, io.SeekStart); err != nil {
181+
return 0, err
182+
}
183+
184+
n, err = reader.Read(p)
185+
186+
*f.offset += int64(n)
187+
188+
return n, err
189+
}

internal/services/snippet/resource.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
1515
"github.com/cloudflare/terraform-provider-cloudflare/internal/logging"
1616
"github.com/hashicorp/terraform-plugin-framework/resource"
17+
"github.com/hashicorp/terraform-plugin-framework/types"
1718
)
1819

1920
// Ensure provider defined types fully satisfy framework interfaces.
@@ -179,6 +180,33 @@ func (r *SnippetResource) Read(ctx context.Context, req resource.ReadRequest, re
179180
}
180181
data = &env.Result
181182

183+
res = new(http.Response)
184+
_, err = r.client.Snippets.Content.Get(
185+
ctx,
186+
data.SnippetName.ValueString(),
187+
snippets.ContentGetParams{
188+
ZoneID: cloudflare.F(data.ZoneID.ValueString()),
189+
},
190+
option.WithResponseBodyInto(&res),
191+
option.WithMiddleware(logging.Middleware(ctx)),
192+
)
193+
if res != nil && res.StatusCode == 404 {
194+
resp.Diagnostics.AddWarning("Resource not found", "The resource was not found on the server and will be removed from state.")
195+
resp.State.RemoveResource(ctx)
196+
return
197+
}
198+
if err != nil {
199+
resp.Diagnostics.AddError("failed to make http request", err.Error())
200+
return
201+
}
202+
data.Metadata.MainModule = types.StringValue(res.Header.Get("Cf-Entrypoint"))
203+
bytes, _ = io.ReadAll(res.Body)
204+
err = data.UnmarshalMultipart(bytes, res.Header.Get("Content-Type"))
205+
if err != nil {
206+
resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
207+
return
208+
}
209+
182210
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
183211
}
184212

0 commit comments

Comments
 (0)