Skip to content

Commit ed856c1

Browse files
committed
feat!: implemented value_path_exists
1 parent a52ff1d commit ed856c1

File tree

18 files changed

+571
-85
lines changed

18 files changed

+571
-85
lines changed

docs/resources/path_exists.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "value_path_exists Resource - terraform-provider-value"
4+
subcategory: ""
5+
description: |-
6+
Checks if an OS path exists and caches its computation at plan-time and won't change afterapply-time even the path may have been removed.
7+
Provider Metadata
8+
Each module can use providermeta. Please keep in mind that these settings only count for resources of this module! (see https://www.terraform.io/internals/provider-meta https://www.terraform.io/internals/provider-meta):
9+
```terraform
10+
// Terraform providermeta example
11+
terraform {
12+
// "value" is the provider name
13+
providermeta "value" {
14+
// {workdir} -> The only available placeholder currently (see below for more information)
15+
guidseed_addition = "{workdir}#for-example" // Results into "/path/to/workdir#for-example"
16+
}
17+
}
18+
```
19+
Optional
20+
guid_seed_addition (String) It serves as addition to each seed of any value_is_fully_known (resource) or value_is_known (resource) within the project if specified in provider, or within the same module if specified in provider-meta.
21+
Placeholders:
22+
"{workdir}" (Keyword) The actual workdir; equals to terraform's path.root. This placeholder is
23+
recommended because this value won't be dragged along the plan and apply phase in comparison to
24+
"abspath(path.root)" that you would add to resource seed where a change to path.root would be
25+
recognized just as usual from terraform.
26+
---
27+
28+
# value_path_exists (Resource)
29+
30+
Checks if an OS path exists and caches its computation at plan-time and won't change afterapply-time even the path may have been removed.
31+
## Provider Metadata
32+
Each module can use provider_meta. Please keep in mind that these settings only count for resources of this module! (see [https://www.terraform.io/internals/provider-meta](https://www.terraform.io/internals/provider-meta)):
33+
```terraform
34+
// Terraform provider_meta example
35+
terraform {
36+
// "value" is the provider name
37+
provider_meta "value" {
38+
// {workdir} -> The only available placeholder currently (see below for more information)
39+
guid_seed_addition = "{workdir}#for-example" // Results into "/path/to/workdir#for-example"
40+
}
41+
}
42+
```
43+
### Optional
44+
- `guid_seed_addition` (String) It serves as addition to each seed of any `value_is_fully_known` (resource) or `value_is_known` (resource) within the project if specified in provider, or within the same module if specified in provider-meta.
45+
46+
**Placeholders**:
47+
- "{workdir}" (Keyword) The actual workdir; equals to terraform's path.root. This placeholder is
48+
recommended because this value won't be dragged along the plan and apply phase in comparison to
49+
"abspath(path.root)" that you would add to resource seed where a change to path.root would be
50+
recognized just as usual from terraform.
51+
52+
53+
54+
<!-- schema generated by tfplugindocs -->
55+
## Schema
56+
57+
### Required
58+
59+
- `guid_seed` (String) Attention! The seed is being used to determine resource uniqueness prior (first plan phase) and during apply phase (second plan phase). Very important to state is that the **seed must be fully known during the plan phase**, otherwise, an error is thrown. Within one terraform plan & apply the **seed of every "value_path_exists" must be unique**! I really recommend you to use the provider configuration and/or provider_meta configuration to increase resource uniqueness. Besides `guid_seed`, the provider configuration seed, the provider_meta configuration seed and the resource type itself will become part of the final seed. Under certain circumstances you may face problems if you run terraform concurrenctly. If you do so, then I recommend you to pass-through a random value via a user (environment) variable that you then add to the seed.
60+
- `path` (String) A path to a file or directory.
61+
- `proposed_unknown` (Boolean) It is very crucial that this field is **not** filled by any custom value except the one produced by `value_unknown_proposer` (resource). This has the reason as its `value` is **always** unknown during the plan phase. On this behaviour this resource must rely and it cannot check if you do not so!
62+
63+
### Read-Only
64+
65+
- `exists` (Boolean) The computation whether the path exists or not.
66+
67+

examples/replaced_when/file_inexistence_check/findme

Whitespace-only changes.

examples/replaced_when/file_inexistence_check/main.tf

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,36 @@ terraform {
77
}
88
}
99

10-
resource "value_replaced_when" "findme_inexistence" {
11-
for_each = {
10+
resource "value_unknown_proposer" "default" {}
11+
12+
locals {
13+
files = {
1214
"findme" : {
1315
fullname = "${path.module}/findme"
1416
}
1517
}
18+
}
19+
20+
resource "value_path_exists" "findme" {
21+
path = local.files["findme"].fullname
22+
guid_seed = "findme"
23+
proposed_unknown = value_unknown_proposer.default.value
24+
}
25+
26+
resource "value_replaced_when" "findme_inexistence" {
27+
condition = !value_path_exists.findme.exists
28+
}
29+
30+
resource "local_file" "findme" {
31+
count = !value_path_exists.findme.exists ? 1 : 0
32+
content = ""
33+
filename = local.files["findme"].fullname
34+
}
1635

17-
condition = !fileexists(each.value.fullname)
36+
output "is_findme_inexistent" {
37+
value = !value_path_exists.findme.exists
1838
}
1939

20-
output "findme_inexistence_check" {
21-
// On craetion this should contain a non-null value.value
22-
value = value_replaced_when.findme_inexistence
40+
output "findme_inexistence_caused_new_value" {
41+
value = value_replaced_when.findme_inexistence.value
2342
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package fwkprovider
2+
3+
import (
4+
"context"
5+
"os"
6+
"strconv"
7+
8+
"github.com/pseudo-dynamic/terraform-provider-value/internal/fwkproviderconfig"
9+
"github.com/pseudo-dynamic/terraform-provider-value/internal/goproviderconfig"
10+
"github.com/pseudo-dynamic/terraform-provider-value/internal/guid"
11+
12+
"github.com/hashicorp/terraform-plugin-framework/diag"
13+
"github.com/hashicorp/terraform-plugin-framework/resource"
14+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
15+
"github.com/hashicorp/terraform-plugin-framework/types"
16+
)
17+
18+
const pathExistsResourceSuffix string = "_path_exists"
19+
const pathExistsResourceName string = providerName + pathExistsResourceSuffix
20+
21+
type pathExistsResource struct {
22+
ProviderGuidSeedAddition *string
23+
}
24+
25+
type pathExistsResourceWithTraits interface {
26+
resource.ResourceWithMetadata
27+
resource.ResourceWithGetSchema
28+
resource.ResourceWithModifyPlan
29+
resource.ResourceWithConfigure
30+
}
31+
32+
func NewPathExistsResource() pathExistsResourceWithTraits {
33+
return &pathExistsResource{}
34+
}
35+
36+
// Configure implements pathExistsResourceWithTraits
37+
func (r *pathExistsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
38+
if req.ProviderData == nil {
39+
// For whatever reason Configure gets called with nil ProviderData.
40+
return
41+
}
42+
43+
provderData := req.ProviderData.(*providerData)
44+
r.ProviderGuidSeedAddition = provderData.GuidSeedAddition
45+
}
46+
47+
func (r pathExistsResource) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
48+
return tfsdk.Schema{
49+
Description: "Checks if an OS path exists and caches its computation at plan-time and won't change after" +
50+
"apply-time even the path may have been removed." + "\n" + goproviderconfig.GetProviderMetaGuidSeedAdditionAttributeDescription(),
51+
Attributes: map[string]tfsdk.Attribute{
52+
"path": {
53+
Type: types.StringType,
54+
Required: true,
55+
Description: "A path to a file or directory.",
56+
},
57+
"proposed_unknown": {
58+
Type: types.BoolType,
59+
Required: true,
60+
Description: goproviderconfig.GetProposedUnknownAttributeDescription(),
61+
},
62+
"guid_seed": {
63+
Type: types.StringType,
64+
Required: true,
65+
Description: goproviderconfig.GetGuidSeedAttributeDescription(pathExistsResourceName),
66+
Validators: []tfsdk.AttributeValidator{
67+
&fwkproviderconfig.PlanKnownValidator{},
68+
},
69+
},
70+
"exists": {
71+
Type: types.BoolType,
72+
Computed: true,
73+
Description: "The computation whether the path exists or not.",
74+
},
75+
},
76+
}, nil
77+
}
78+
79+
type pathExistsState struct {
80+
Path types.String `tfsdk:"path"`
81+
GuidSeed types.String `tfsdk:"guid_seed"`
82+
ProposedUnknown types.Bool `tfsdk:"proposed_unknown"`
83+
Exists types.Bool `tfsdk:"exists"`
84+
}
85+
86+
func (r *pathExistsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
87+
resp.TypeName = req.ProviderTypeName + pathExistsResourceSuffix
88+
}
89+
90+
// ModifyPlan implements PathExistsResourceWithTraits
91+
func (r *pathExistsResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
92+
if r.ProviderGuidSeedAddition == nil {
93+
resp.Diagnostics.AddError("Bad provider guid seed", "Provider guid seed is null but was expected to be empty")
94+
return
95+
}
96+
97+
// Get current config
98+
var config pathExistsState
99+
diags := req.Config.Get(ctx, &config)
100+
resp.Diagnostics.Append(diags...)
101+
102+
if resp.Diagnostics.HasError() {
103+
return
104+
}
105+
suppliedGuidSeed := config.GuidSeed.Value
106+
isPlanPhase := config.ProposedUnknown.IsUnknown()
107+
108+
var providerMetaSeedAddition string
109+
var isSuccessful bool
110+
if providerMetaSeedAddition, _, isSuccessful = goproviderconfig.TryUnmarshalValueThenExtractGuidSeedAddition(&req.ProviderMeta.Raw); !isSuccessful {
111+
resp.Diagnostics.AddError("Extraction failed", "Could not extract provider meta guid seed addition")
112+
return
113+
}
114+
115+
composedGuidSeed := guid.ComposeGuidSeed(r.ProviderGuidSeedAddition,
116+
&providerMetaSeedAddition,
117+
pathExistsResourceName,
118+
"exists",
119+
&suppliedGuidSeed)
120+
121+
checkPathExistence := func() types.Bool {
122+
if config.Path.Unknown {
123+
return types.Bool{Unknown: true}
124+
}
125+
126+
_, err := os.Stat(config.Path.Value)
127+
pathExists := err == nil
128+
return types.Bool{Value: pathExists}
129+
}
130+
131+
cachedExists, err := guid.GetPlanCachedBoolean(
132+
isPlanPhase,
133+
composedGuidSeed,
134+
pathExistsResourceName,
135+
checkPathExistence)
136+
137+
if err != nil {
138+
resp.Diagnostics.AddError("Plan cache mechanism failed for exists attribute", err.Error())
139+
return
140+
}
141+
142+
resp.Diagnostics.AddWarning("Exists (cached) is unknown: "+strconv.FormatBool(cachedExists.Unknown), "")
143+
144+
config.Exists = cachedExists
145+
resp.Plan.Set(ctx, &config)
146+
}
147+
148+
// Create a new resource
149+
func (r pathExistsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
150+
// Retrieve values from plan
151+
var plan pathExistsState
152+
diags := req.Plan.Get(ctx, &plan)
153+
resp.Diagnostics.Append(diags...)
154+
155+
if resp.Diagnostics.HasError() {
156+
return
157+
}
158+
159+
diags = resp.State.Set(ctx, &plan)
160+
resp.Diagnostics.Append(diags...)
161+
162+
if resp.Diagnostics.HasError() {
163+
return
164+
}
165+
}
166+
167+
// Read resource information
168+
func (r pathExistsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
169+
// Get current state
170+
var state pathExistsState
171+
diags := req.State.Get(ctx, &state)
172+
resp.Diagnostics.Append(diags...)
173+
174+
if resp.Diagnostics.HasError() {
175+
return
176+
}
177+
178+
diags = resp.State.Set(ctx, &state)
179+
resp.Diagnostics.Append(diags...)
180+
181+
if resp.Diagnostics.HasError() {
182+
return
183+
}
184+
}
185+
186+
// Update resource
187+
func (r pathExistsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
188+
// Get plan
189+
var plan pathExistsState
190+
diags := req.Plan.Get(ctx, &plan)
191+
resp.Diagnostics.Append(diags...)
192+
193+
if resp.Diagnostics.HasError() {
194+
return
195+
}
196+
197+
// Set new state
198+
diags = resp.State.Set(ctx, &plan)
199+
resp.Diagnostics.Append(diags...)
200+
201+
if resp.Diagnostics.HasError() {
202+
return
203+
}
204+
}
205+
206+
// Delete resource
207+
func (r pathExistsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
208+
var state pathExistsState
209+
diags := req.State.Get(ctx, &state)
210+
resp.Diagnostics.Append(diags...)
211+
212+
if resp.Diagnostics.HasError() {
213+
return
214+
}
215+
216+
// Remove resource from state
217+
resp.State.RemoveResource(ctx)
218+
}

internal/fwkprovider/provider.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"github.com/pseudo-dynamic/terraform-provider-value/internal/fwkproviderconfig"
1313
)
1414

15+
const providerName string = "value"
16+
1517
type provider struct {
1618
}
1719

@@ -26,6 +28,10 @@ type providerConfig struct {
2628
GuidSeedAddition types.String `tfsdk:"guid_seed_addition"`
2729
}
2830

31+
type providerData struct {
32+
GuidSeedAddition *string
33+
}
34+
2935
func NewProvider() providerWithTraits {
3036
return &provider{}
3137
}
@@ -49,6 +55,10 @@ func (p *provider) Configure(ctx context.Context, req fwkprovider.ConfigureReque
4955
if resp.Diagnostics.HasError() {
5056
return
5157
}
58+
59+
resp.ResourceData = &providerData{
60+
GuidSeedAddition: &config.GuidSeedAddition.Value,
61+
}
5262
}
5363

5464
// GetResources - Defines provider resources
@@ -60,6 +70,9 @@ func (p *provider) Resources(_ context.Context) []func() resource.Resource {
6070
func() resource.Resource {
6171
return NewUnknownProposerResource()
6272
},
73+
func() resource.Resource {
74+
return NewPathExistsResource()
75+
},
6376
}
6477
}
6578

internal/fwkproviderconfig/attribute_guid_seed_attribute.go renamed to internal/fwkproviderconfig/attribute_guid_seed_addition.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,6 @@ import (
77
"github.com/hashicorp/terraform-plugin-framework/types"
88
)
99

10-
type guidSeedAdditionUnknownValidator struct{}
11-
12-
func (v *guidSeedAdditionUnknownValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) {
13-
if req.AttributeConfig != nil && req.AttributeConfig.IsUnknown() {
14-
resp.Diagnostics.AddError(
15-
"Guid seed addition is unknown",
16-
"Guid seed addition must be fully known at plan-time. For further informations take a look into the documentation.")
17-
}
18-
}
19-
20-
func getGuidSeedAdditionUnknownValidatorDescription() string {
21-
return "Validates that guid seed addition is not unknown."
22-
}
23-
24-
func (*guidSeedAdditionUnknownValidator) Description(context.Context) string {
25-
return getGuidSeedAdditionUnknownValidatorDescription()
26-
}
27-
28-
func (*guidSeedAdditionUnknownValidator) MarkdownDescription(context.Context) string {
29-
return getGuidSeedAdditionUnknownValidatorDescription()
30-
}
31-
3210
type guidSeedAdditionDefaultEmptyModifier struct{}
3311

3412
func (r guidSeedAdditionDefaultEmptyModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) {

0 commit comments

Comments
 (0)