Skip to content

Commit a305aa4

Browse files
authored
Merge pull request #44559 from hashicorp/f-aws_transfer_host_key
r/aws_transfer_host_key: New resource
2 parents b1f8548 + 9e21769 commit a305aa4

File tree

11 files changed

+1159
-0
lines changed

11 files changed

+1159
-0
lines changed

.changelog/44559.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:new-resource
2+
aws_transfer_host_key
3+
```
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package stringplanmodifier
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
10+
"github.com/hashicorp/terraform-provider-aws/internal/framework/privatestate"
11+
)
12+
13+
// RequiresReplaceWO returns a plan modifier that forces resource replacement
14+
// if a write-only value changes.
15+
func RequiresReplaceWO(privateStateKey string) planmodifier.String {
16+
return requiresReplaceWO{
17+
privateStateKey: privateStateKey,
18+
}
19+
}
20+
21+
type requiresReplaceWO struct {
22+
privateStateKey string
23+
}
24+
25+
func (m requiresReplaceWO) Description(ctx context.Context) string {
26+
return m.MarkdownDescription(ctx)
27+
}
28+
29+
func (m requiresReplaceWO) MarkdownDescription(context.Context) string {
30+
return "If the value of this write-only attribute changes, Terraform will destroy and recreate the resource."
31+
}
32+
33+
func (m requiresReplaceWO) PlanModifyString(ctx context.Context, request planmodifier.StringRequest, response *planmodifier.StringResponse) {
34+
newValue := request.ConfigValue
35+
newValueExists := !newValue.IsNull()
36+
37+
woStore := privatestate.NewWriteOnlyValueStore(request.Private, m.privateStateKey)
38+
oldValueExists, diags := woStore.HasValue(ctx)
39+
response.Diagnostics.Append(diags...)
40+
if response.Diagnostics.HasError() {
41+
return
42+
}
43+
44+
if !newValueExists {
45+
if oldValueExists {
46+
response.RequiresReplace = true
47+
}
48+
return
49+
}
50+
51+
if !oldValueExists {
52+
response.RequiresReplace = true
53+
return
54+
}
55+
56+
equal, diags := woStore.EqualValue(ctx, newValue)
57+
response.Diagnostics.Append(diags...)
58+
if response.Diagnostics.HasError() {
59+
return
60+
}
61+
62+
if !equal {
63+
response.RequiresReplace = true
64+
}
65+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package privatestate
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
)
11+
12+
// PrivateState defines an interface for managing provider-defined resource private state data.
13+
type PrivateState interface {
14+
// GetKey returns the private state data associated with the given key.
15+
GetKey(context.Context, string) ([]byte, diag.Diagnostics)
16+
// SetKey sets the private state data at the given key.
17+
SetKey(context.Context, string, []byte) diag.Diagnostics
18+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package privatestate
5+
6+
import (
7+
"context"
8+
"crypto/sha256"
9+
"encoding/hex"
10+
"strconv"
11+
12+
"github.com/hashicorp/terraform-plugin-framework/diag"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
14+
tfjson "github.com/hashicorp/terraform-provider-aws/internal/json"
15+
)
16+
17+
func NewWriteOnlyValueStore(private PrivateState, key string) *WriteOnlyValueStore {
18+
return &WriteOnlyValueStore{
19+
key: key,
20+
private: private,
21+
}
22+
}
23+
24+
type WriteOnlyValueStore struct {
25+
key string
26+
private PrivateState
27+
}
28+
29+
func (w *WriteOnlyValueStore) EqualValue(ctx context.Context, value types.String) (bool, diag.Diagnostics) {
30+
bytes, diags := w.private.GetKey(ctx, w.key)
31+
if diags.HasError() {
32+
return false, diags
33+
}
34+
35+
var s string
36+
if err := tfjson.DecodeFromBytes(bytes, &s); err != nil {
37+
diags.AddError("decoding private state", err.Error())
38+
return false, diags
39+
}
40+
41+
return s == sha256Hash(value.ValueString()), diags
42+
}
43+
44+
func (w *WriteOnlyValueStore) HasValue(ctx context.Context) (bool, diag.Diagnostics) {
45+
bytes, diags := w.private.GetKey(ctx, w.key)
46+
return len(bytes) > 0, diags
47+
}
48+
49+
func (w *WriteOnlyValueStore) SetValue(ctx context.Context, val types.String) diag.Diagnostics {
50+
if val.IsNull() {
51+
return w.private.SetKey(ctx, w.key, []byte(""))
52+
}
53+
54+
return w.private.SetKey(ctx, w.key, []byte(strconv.Quote(sha256Hash(val.ValueString()))))
55+
}
56+
57+
func sha256Hash(data string) string {
58+
hash := sha256.New()
59+
hash.Write([]byte(data))
60+
return hex.EncodeToString(hash.Sum(nil))
61+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package privatestate_test
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
"github.com/hashicorp/terraform-plugin-framework/types"
13+
"github.com/hashicorp/terraform-provider-aws/internal/framework/privatestate"
14+
)
15+
16+
func TestWriteOnlyValueStore_HasValue(t *testing.T) {
17+
t.Parallel()
18+
19+
ctx := t.Context()
20+
store1 := privatestate.NewWriteOnlyValueStore(&privateState{}, "key")
21+
store2 := privatestate.NewWriteOnlyValueStore(&privateState{}, "key")
22+
store2.SetValue(ctx, types.StringValue("value1"))
23+
24+
testCases := []struct {
25+
testName string
26+
store *privatestate.WriteOnlyValueStore
27+
wantValue bool
28+
}{
29+
{
30+
testName: "empty state",
31+
store: store1,
32+
},
33+
{
34+
testName: "has value",
35+
store: store2,
36+
wantValue: true,
37+
},
38+
}
39+
40+
for _, testCase := range testCases {
41+
t.Run(testCase.testName, func(t *testing.T) {
42+
t.Parallel()
43+
gotValue, diags := testCase.store.HasValue(ctx)
44+
if diags.HasError() {
45+
t.Fatal("unexpected error")
46+
}
47+
if got, want := gotValue, testCase.wantValue; !cmp.Equal(got, want) {
48+
t.Errorf("got %t, want %t", got, want)
49+
}
50+
})
51+
}
52+
}
53+
54+
func TestWriteOnlyValueStore_EqualValue(t *testing.T) {
55+
t.Parallel()
56+
57+
ctx := t.Context()
58+
store1 := privatestate.NewWriteOnlyValueStore(&privateState{}, "key")
59+
store1.SetValue(ctx, types.StringValue("value1"))
60+
store2 := privatestate.NewWriteOnlyValueStore(&privateState{}, "key")
61+
store2.SetValue(ctx, types.StringValue("value2"))
62+
63+
testCases := []struct {
64+
testName string
65+
store *privatestate.WriteOnlyValueStore
66+
wantEqual bool
67+
}{
68+
{
69+
testName: "equal",
70+
store: store1,
71+
wantEqual: true,
72+
},
73+
{
74+
testName: "not equal",
75+
store: store2,
76+
},
77+
}
78+
79+
for _, testCase := range testCases {
80+
t.Run(testCase.testName, func(t *testing.T) {
81+
t.Parallel()
82+
gotEqual, diags := testCase.store.EqualValue(ctx, types.StringValue("value1"))
83+
if diags.HasError() {
84+
t.Fatal("unexpected error")
85+
}
86+
if got, want := gotEqual, testCase.wantEqual; !cmp.Equal(got, want) {
87+
t.Errorf("got %t, want %t", got, want)
88+
}
89+
})
90+
}
91+
}
92+
93+
type privateState struct {
94+
data map[string][]byte
95+
}
96+
97+
func (p *privateState) GetKey(_ context.Context, key string) ([]byte, diag.Diagnostics) {
98+
var diags diag.Diagnostics
99+
bytes := p.data[key]
100+
return bytes, diags
101+
}
102+
103+
func (p *privateState) SetKey(_ context.Context, key string, value []byte) diag.Diagnostics {
104+
var diags diag.Diagnostics
105+
106+
if p.data == nil {
107+
p.data = make(map[string][]byte)
108+
}
109+
110+
p.data[key] = value
111+
return diags
112+
}

internal/service/transfer/exports_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ var (
99
ResourceAgreement = resourceAgreement
1010
ResourceCertificate = resourceCertificate
1111
ResourceConnector = resourceConnector
12+
ResourceHostKey = newHostKeyResource
1213
ResourceProfile = resourceProfile
1314
ResourceServer = resourceServer
1415
ResourceSSHKey = resourceSSHKey
@@ -22,6 +23,7 @@ var (
2223
FindAgreementByTwoPartKey = findAgreementByTwoPartKey
2324
FindCertificateByID = findCertificateByID
2425
FindConnectorByID = findConnectorByID
26+
FindHostKeyByTwoPartKey = findHostKeyByTwoPartKey
2527
FindProfileByID = findProfileByID
2628
FindServerByID = findServerByID
2729
FindTag = findTag

0 commit comments

Comments
 (0)