Skip to content

Commit dc00cfe

Browse files
Merge pull request #5679 from 1000hz/add-file-handling
workers_script: support `content_file` and `content_sha256` attribute pair as alternative to `content`
2 parents 78db71c + 6c850b0 commit dc00cfe

File tree

9 files changed

+390
-13
lines changed

9 files changed

+390
-13
lines changed

docs/resources/workers_script.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ resource "cloudflare_workers_script" "example_workers_script" {
2828
name = "MY_ENV_VAR"
2929
type = "plain_text"
3030
}]
31-
body_part = "worker.js"
3231
compatibility_date = "2021-01-01"
3332
compatibility_flags = ["nodejs_compat"]
34-
content = file("worker.js")
33+
content_file = "worker.js"
34+
content_sha256 = filesha256("worker.js")
3535
keep_assets = false
3636
keep_bindings = ["string"]
3737
logpush = false

examples/resources/cloudflare_workers_script/resource.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ resource "cloudflare_workers_script" "example_workers_script" {
1414
name = "MY_ENV_VAR"
1515
type = "plain_text"
1616
}]
17-
body_part = "worker.js"
1817
compatibility_date = "2021-01-01"
1918
compatibility_flags = ["nodejs_compat"]
20-
content = file("worker.js")
19+
content_file = "worker.js"
20+
content_sha256 = filesha256("worker.js")
2121
keep_assets = false
2222
keep_bindings = ["string"]
2323
logpush = false

internal/services/workers_script/custom.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
package workers_script
22

33
import (
4+
"context"
5+
"crypto/sha256"
6+
"encoding/hex"
47
"fmt"
58
"io"
69
"mime/multipart"
710
"net/textproto"
11+
"os"
12+
"path/filepath"
813
"strings"
14+
15+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
916
)
1017

1118
func writeFileBytes(partName string, filename string, contentType string, content io.Reader, writer *multipart.Writer) error {
@@ -35,3 +42,134 @@ var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
3542
func escapeQuotes(s string) string {
3643
return quoteEscaper.Replace(s)
3744
}
45+
46+
func readFile(path string) (string, error) {
47+
if strings.HasPrefix(path, "~/") {
48+
dirname, err := os.UserHomeDir()
49+
if err != nil {
50+
return "", fmt.Errorf("could not expand home directory in path %s: %w", path, err)
51+
}
52+
path = filepath.Join(dirname, path[2:])
53+
}
54+
55+
content, err := os.ReadFile(path)
56+
if err != nil {
57+
return "", fmt.Errorf("could not read file %s: %w", path, err)
58+
}
59+
60+
return string(content), nil
61+
}
62+
63+
func calculateFileHash(filePath string) (string, error) {
64+
file, err := os.Open(filePath)
65+
if err != nil {
66+
return "", fmt.Errorf("failed to open file: %w", err)
67+
}
68+
defer file.Close()
69+
70+
hasher := sha256.New()
71+
if _, err := io.Copy(hasher, file); err != nil {
72+
return "", fmt.Errorf("failed to read file: %w", err)
73+
}
74+
75+
return hex.EncodeToString(hasher.Sum(nil)), nil
76+
}
77+
78+
func calculateStringHash(content string) (string, error) {
79+
hash := sha256.Sum256([]byte(content))
80+
return hex.EncodeToString(hash[:]), nil
81+
}
82+
83+
var _ validator.String = &contentSHA256Validator{}
84+
85+
type contentSHA256Validator struct {
86+
ContentPath string
87+
ContentFilePath string
88+
}
89+
90+
func (v contentSHA256Validator) Description(_ context.Context) string {
91+
return fmt.Sprintf("Validates that the provided value matches the SHA-256 hash of content in either `content` or `content_file`.")
92+
}
93+
94+
func (v contentSHA256Validator) MarkdownDescription(ctx context.Context) string {
95+
return v.Description(ctx)
96+
}
97+
98+
func (v contentSHA256Validator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
99+
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
100+
return
101+
}
102+
103+
providedHash := req.ConfigValue.ValueString()
104+
105+
var config WorkersScriptModel
106+
107+
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
108+
109+
if resp.Diagnostics.HasError() {
110+
return
111+
}
112+
113+
var hasContent, hasContentFile bool
114+
115+
if !config.Content.IsNull() {
116+
hasContent = true
117+
}
118+
119+
if !config.ContentFile.IsNull() {
120+
hasContentFile = true
121+
}
122+
123+
if !hasContent && !hasContentFile {
124+
resp.Diagnostics.AddError("Missing required attributes", "One of `content` or `content_file` is required")
125+
return
126+
}
127+
128+
var actualHash string
129+
var err error
130+
131+
if hasContent {
132+
actualHash, err = calculateStringHash(config.Content.ValueString())
133+
if err != nil {
134+
resp.Diagnostics.AddAttributeError(
135+
req.Path,
136+
"Hash Calculation Error",
137+
fmt.Sprintf("Failed to calculate SHA-256 hash of content: %s", err.Error()),
138+
)
139+
return
140+
}
141+
} else if hasContentFile {
142+
actualHash, err = calculateFileHash(config.ContentFile.ValueString())
143+
if err != nil {
144+
resp.Diagnostics.AddAttributeError(
145+
req.Path,
146+
"Hash Calculation Error",
147+
fmt.Sprintf("Failed to calculate SHA-256 hash of file '%s': %s", config.ContentFile.ValueString(), err.Error()),
148+
)
149+
return
150+
}
151+
}
152+
153+
if providedHash != actualHash {
154+
var source string
155+
if hasContent {
156+
source = "content"
157+
} else if hasContentFile {
158+
source = fmt.Sprintf("content_file (%s)", config.ContentFile.ValueString())
159+
}
160+
161+
resp.Diagnostics.AddAttributeError(
162+
req.Path,
163+
"SHA-256 Hash Mismatch",
164+
fmt.Sprintf("The provided SHA-256 hash '%s' does not match the actual hash '%s' of %s",
165+
providedHash, actualHash, source),
166+
)
167+
}
168+
}
169+
170+
func ValidateContentSHA256() validator.String {
171+
return contentSHA256Validator{
172+
ContentPath: "content",
173+
ContentFilePath: "content_file",
174+
}
175+
}

internal/services/workers_script/model.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ type WorkersScriptModel struct {
3838
ID types.String `tfsdk:"id" json:"-,computed"`
3939
ScriptName types.String `tfsdk:"script_name" path:"script_name,required"`
4040
AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
41-
Content types.String `tfsdk:"content" json:"content,required"`
41+
Content types.String `tfsdk:"content" json:"-"`
42+
ContentFile types.String `tfsdk:"content_file" json:"-"`
43+
ContentSHA256 types.String `tfsdk:"content_sha256" json:"-"`
4244
CreatedOn timetypes.RFC3339 `tfsdk:"created_on" json:"created_on,computed" format:"date-time"`
4345
Etag types.String `tfsdk:"etag" json:"etag,computed"`
4446
HasAssets types.Bool `tfsdk:"has_assets" json:"has_assets,computed"`

internal/services/workers_script/resource.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ func (r *WorkersScriptResource) Create(ctx context.Context, req resource.CreateR
6868
return
6969
}
7070

71+
contentSHA256 := data.ContentSHA256
72+
73+
if !data.ContentFile.IsNull() {
74+
content, err := readFile((data.ContentFile.ValueString()))
75+
if err != nil {
76+
resp.Diagnostics.AddError("failed to read file", err.Error())
77+
return
78+
}
79+
data.Content = types.StringValue(content)
80+
}
81+
7182
dataBytes, contentType, err := data.MarshalMultipart()
7283
if err != nil {
7384
resp.Diagnostics.AddError("failed to serialize multipart http request", err.Error())
@@ -97,6 +108,12 @@ func (r *WorkersScriptResource) Create(ctx context.Context, req resource.CreateR
97108
}
98109
data = &env.Result
99110
data.ID = data.ScriptName
111+
data.ContentSHA256 = contentSHA256
112+
113+
// avoid storing `content` in state if `content_file` is configured
114+
if !data.ContentFile.IsNull() {
115+
data.Content = types.StringNull()
116+
}
100117

101118
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
102119
}
@@ -118,6 +135,17 @@ func (r *WorkersScriptResource) Update(ctx context.Context, req resource.UpdateR
118135
return
119136
}
120137

138+
contentSHA256 := data.ContentSHA256
139+
140+
if !data.ContentFile.IsNull() {
141+
content, err := readFile((data.ContentFile.ValueString()))
142+
if err != nil {
143+
resp.Diagnostics.AddError("failed to read file", err.Error())
144+
return
145+
}
146+
data.Content = types.StringValue(content)
147+
}
148+
121149
dataBytes, contentType, err := data.MarshalMultipart()
122150
if err != nil {
123151
resp.Diagnostics.AddError("failed to serialize multipart http request", err.Error())
@@ -147,6 +175,12 @@ func (r *WorkersScriptResource) Update(ctx context.Context, req resource.UpdateR
147175
}
148176
data = &env.Result
149177
data.ID = data.ScriptName
178+
data.ContentSHA256 = contentSHA256
179+
180+
// avoid storing `content` in state if `content_file` is configured
181+
if !data.ContentFile.IsNull() {
182+
data.Content = types.StringNull()
183+
}
150184

151185
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
152186
}
@@ -253,7 +287,17 @@ func (r *WorkersScriptResource) Read(ctx context.Context, req resource.ReadReque
253287
content = string(bytes)
254288
}
255289

256-
data.Content = types.StringValue(content)
290+
// only update `content` if it was already present in state
291+
// which might not be the case if `content_file` is used instead
292+
if !data.Content.IsNull() {
293+
data.Content = types.StringValue(content)
294+
}
295+
296+
// refresh the content hash in case the remote state has drifted
297+
if !data.ContentSHA256.IsNull() {
298+
hash, _ := calculateStringHash(content)
299+
data.ContentSHA256 = types.StringValue(hash)
300+
}
257301

258302
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
259303
}

0 commit comments

Comments
 (0)