Skip to content

Commit 429474d

Browse files
authored
fix: Save cluster state after creating it in the API (#109)
1 parent 9dd51fb commit 429474d

File tree

7 files changed

+231
-25
lines changed

7 files changed

+231
-25
lines changed

docs/resources/cluster.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ A representation of a cluster you can deploy to.
3333

3434
### Read-Only
3535

36+
- `agent_deployed` (Boolean) Whether the agent was deployed to the cluster.
3637
- `id` (String) Internal identifier of this cluster.
3738
- `inserted_at` (String) Creation date of this cluster.
3839

example/cluster/main.tf

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ terraform {
22
required_providers {
33
plural = {
44
source = "pluralsh/plural"
5-
version = "0.2.28"
5+
version = "0.2.30"
66
}
77
}
88
}
99

1010
provider "plural" {
1111
use_cli = true
12+
kubeconfig = {
13+
# It can be sourced from environment variables instead, i.e.: export PLURAL_KUBE_CONFIG_PATH=$KUBECONFIG
14+
config_path = pathexpand("~/.kube/config")
15+
}
1216
}
1317

1418
data "plural_project" "test" {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package resource
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/path"
7+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
)
10+
11+
type ensureAgentPlanModifier struct{}
12+
13+
func (in ensureAgentPlanModifier) Description(_ context.Context) string {
14+
return "Forces resource update when agent is not deployed"
15+
}
16+
17+
func (in ensureAgentPlanModifier) MarkdownDescription(_ context.Context) string {
18+
return "Forces resource update when agent is not deployed"
19+
}
20+
21+
func (in ensureAgentPlanModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
22+
if req.State.Raw.IsNull() {
23+
return
24+
}
25+
26+
var agentDeployed types.Bool
27+
resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("agent_deployed"), &agentDeployed)...)
28+
if resp.Diagnostics.HasError() {
29+
return
30+
}
31+
32+
// If the agent is not deployed, force update by setting the field to unknown.
33+
if !agentDeployed.IsNull() && !agentDeployed.ValueBool() {
34+
resp.PlanValue = types.BoolUnknown()
35+
}
36+
}
37+
38+
func EnsureAgent() planmodifier.Bool {
39+
return ensureAgentPlanModifier{}
40+
}

internal/resource/cluster.go

Lines changed: 158 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,21 @@ import (
1111

1212
"github.com/hashicorp/terraform-plugin-framework/path"
1313
"github.com/hashicorp/terraform-plugin-framework/resource"
14+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
15+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
19+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
20+
"github.com/hashicorp/terraform-plugin-framework/types"
21+
"github.com/pluralsh/plural-cli/pkg/console"
1422
"github.com/samber/lo"
1523
"k8s.io/apimachinery/pkg/util/wait"
1624
)
1725

1826
var _ resource.Resource = &clusterResource{}
1927
var _ resource.ResourceWithImportState = &clusterResource{}
28+
var _ resource.ResourceWithUpgradeState = &clusterResource{}
2029

2130
func NewClusterResource() resource.Resource {
2231
return &clusterResource{}
@@ -68,21 +77,20 @@ func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest
6877
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create cluster, got error: %s", err))
6978
return
7079
}
80+
data.FromCreate(result, ctx, &resp.Diagnostics)
7181

7282
if r.kubeClient != nil || data.HasKubeconfig() {
73-
if result.CreateCluster.DeployToken == nil {
74-
resp.Diagnostics.AddError("Client Error", "Unable to fetch cluster deploy token")
75-
return
76-
}
77-
78-
if err = InstallOrUpgradeAgent(ctx, r.client, data.GetKubeconfig(), r.kubeClient, data.HelmRepoUrl.ValueString(),
79-
data.HelmValues.ValueStringPointer(), r.consoleUrl, lo.FromPtr(result.CreateCluster.DeployToken), result.CreateCluster.ID, &resp.Diagnostics); err != nil {
80-
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to install operator, got error: %s", err))
81-
return
83+
err = InstallOrUpgradeAgent(ctx, r.client, data.GetKubeconfig(), r.kubeClient, data.HelmRepoUrl.ValueString(),
84+
data.HelmValues.ValueStringPointer(), r.consoleUrl, lo.FromPtr(result.CreateCluster.DeployToken),
85+
result.CreateCluster.ID, &resp.Diagnostics)
86+
if err != nil {
87+
resp.Diagnostics.AddWarning("Agent Installation Failed", fmt.Sprintf(
88+
"Unable to install agent, in order to retry run `terraform apply` again. Got error: %s", err))
89+
} else {
90+
data.AgentDeployed = types.BoolValue(true)
8291
}
8392
}
8493

85-
data.FromCreate(result, ctx, &resp.Diagnostics)
8694
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
8795
}
8896

@@ -142,7 +150,7 @@ func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest
142150
}
143151

144152
kubeconfigChanged := data.HasKubeconfig() && !data.GetKubeconfig().Unchanged(state.GetKubeconfig())
145-
reinstallable := !data.HelmRepoUrl.Equal(state.HelmRepoUrl) || kubeconfigChanged
153+
reinstallable := !data.AgentDeployed.ValueBool() || !data.HelmRepoUrl.Equal(state.HelmRepoUrl) || kubeconfigChanged
146154
if reinstallable && (r.kubeClient != nil || data.HasKubeconfig()) {
147155
clusterWithToken, err := r.client.GetClusterWithToken(ctx, data.Id.ValueStringPointer(), nil)
148156
if err != nil {
@@ -155,6 +163,8 @@ func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest
155163
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to install operator, got error: %s", err))
156164
return
157165
}
166+
167+
data.AgentDeployed = types.BoolValue(true)
158168
}
159169

160170
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
@@ -210,3 +220,140 @@ func (r *clusterResource) ImportState(ctx context.Context, req resource.ImportSt
210220

211221
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
212222
}
223+
224+
func (r *clusterResource) UpgradeState(_ context.Context) map[int64]resource.StateUpgrader {
225+
return map[int64]resource.StateUpgrader{
226+
// State upgrade from 0 to 1
227+
0: {
228+
PriorSchema: &schema.Schema{
229+
Version: 0,
230+
Attributes: map[string]schema.Attribute{
231+
"id": schema.StringAttribute{
232+
Computed: true,
233+
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
234+
},
235+
"inserted_at": schema.StringAttribute{
236+
Computed: true,
237+
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
238+
},
239+
"name": schema.StringAttribute{
240+
Required: true,
241+
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
242+
},
243+
"handle": schema.StringAttribute{
244+
Optional: true,
245+
Computed: true,
246+
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
247+
},
248+
"project_id": schema.StringAttribute{
249+
Optional: true,
250+
},
251+
"detach": schema.BoolAttribute{
252+
Optional: true,
253+
Computed: true,
254+
Default: booldefault.StaticBool(false),
255+
},
256+
"metadata": schema.StringAttribute{
257+
Optional: true,
258+
Computed: true,
259+
Default: stringdefault.StaticString("{}"),
260+
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
261+
},
262+
"helm_repo_url": schema.StringAttribute{
263+
Optional: true,
264+
Computed: true,
265+
Default: stringdefault.StaticString(console.RepoUrl),
266+
},
267+
"helm_values": schema.StringAttribute{
268+
Optional: true,
269+
},
270+
"kubeconfig": common.KubeconfigResourceSchema(),
271+
"protect": schema.BoolAttribute{
272+
Optional: true,
273+
Computed: true,
274+
Default: booldefault.StaticBool(false),
275+
},
276+
"tags": schema.MapAttribute{
277+
Optional: true,
278+
ElementType: types.StringType,
279+
},
280+
"bindings": schema.SingleNestedAttribute{
281+
Optional: true,
282+
Attributes: map[string]schema.Attribute{
283+
"read": schema.SetNestedAttribute{
284+
Optional: true,
285+
NestedObject: schema.NestedAttributeObject{
286+
Attributes: map[string]schema.Attribute{
287+
"group_id": schema.StringAttribute{Optional: true},
288+
"id": schema.StringAttribute{Optional: true},
289+
"user_id": schema.StringAttribute{Optional: true},
290+
},
291+
},
292+
},
293+
"write": schema.SetNestedAttribute{
294+
Optional: true,
295+
Description: "Write policies of this cluster.",
296+
MarkdownDescription: "Write policies of this cluster.",
297+
NestedObject: schema.NestedAttributeObject{
298+
Attributes: map[string]schema.Attribute{
299+
"group_id": schema.StringAttribute{
300+
Optional: true,
301+
},
302+
"id": schema.StringAttribute{
303+
Optional: true,
304+
},
305+
"user_id": schema.StringAttribute{
306+
Optional: true,
307+
},
308+
},
309+
},
310+
},
311+
},
312+
PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()},
313+
},
314+
},
315+
},
316+
StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
317+
var priorStateData struct {
318+
Id types.String `tfsdk:"id"`
319+
InsertedAt types.String `tfsdk:"inserted_at"`
320+
Name types.String `tfsdk:"name"`
321+
Handle types.String `tfsdk:"handle"`
322+
ProjectId types.String `tfsdk:"project_id"`
323+
Detach types.Bool `tfsdk:"detach"`
324+
Protect types.Bool `tfsdk:"protect"`
325+
Tags types.Map `tfsdk:"tags"`
326+
Metadata types.String `tfsdk:"metadata"`
327+
Bindings *common.Bindings `tfsdk:"bindings"`
328+
HelmRepoUrl types.String `tfsdk:"helm_repo_url"`
329+
HelmValues types.String `tfsdk:"helm_values"`
330+
Kubeconfig *common.Kubeconfig `tfsdk:"kubeconfig"`
331+
}
332+
333+
resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...)
334+
if resp.Diagnostics.HasError() {
335+
return
336+
}
337+
338+
upgradedStateData := cluster{
339+
Id: priorStateData.Id,
340+
InsertedAt: priorStateData.InsertedAt,
341+
Name: priorStateData.Name,
342+
Handle: priorStateData.Handle,
343+
ProjectId: priorStateData.ProjectId,
344+
Detach: priorStateData.Detach,
345+
Protect: priorStateData.Protect,
346+
Tags: priorStateData.Tags,
347+
Metadata: priorStateData.Metadata,
348+
Bindings: priorStateData.Bindings,
349+
HelmRepoUrl: priorStateData.HelmRepoUrl,
350+
HelmValues: priorStateData.HelmValues,
351+
Kubeconfig: priorStateData.Kubeconfig,
352+
AgentDeployed: types.BoolValue(true),
353+
}
354+
355+
resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateData)...)
356+
},
357+
},
358+
}
359+
}

internal/resource/cluster_model.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ import (
1313
)
1414

1515
type cluster struct {
16-
Id types.String `tfsdk:"id"`
17-
InsertedAt types.String `tfsdk:"inserted_at"`
18-
Name types.String `tfsdk:"name"`
19-
Handle types.String `tfsdk:"handle"`
20-
ProjectId types.String `tfsdk:"project_id"`
21-
Detach types.Bool `tfsdk:"detach"`
22-
Protect types.Bool `tfsdk:"protect"`
23-
Tags types.Map `tfsdk:"tags"`
24-
Metadata types.String `tfsdk:"metadata"`
25-
Bindings *common.Bindings `tfsdk:"bindings"`
26-
HelmRepoUrl types.String `tfsdk:"helm_repo_url"`
27-
HelmValues types.String `tfsdk:"helm_values"`
28-
Kubeconfig *common.Kubeconfig `tfsdk:"kubeconfig"`
16+
Id types.String `tfsdk:"id"`
17+
InsertedAt types.String `tfsdk:"inserted_at"`
18+
Name types.String `tfsdk:"name"`
19+
Handle types.String `tfsdk:"handle"`
20+
ProjectId types.String `tfsdk:"project_id"`
21+
Detach types.Bool `tfsdk:"detach"`
22+
Protect types.Bool `tfsdk:"protect"`
23+
Tags types.Map `tfsdk:"tags"`
24+
Metadata types.String `tfsdk:"metadata"`
25+
Bindings *common.Bindings `tfsdk:"bindings"`
26+
HelmRepoUrl types.String `tfsdk:"helm_repo_url"`
27+
HelmValues types.String `tfsdk:"helm_values"`
28+
Kubeconfig *common.Kubeconfig `tfsdk:"kubeconfig"`
29+
AgentDeployed types.Bool `tfsdk:"agent_deployed"`
2930
}
3031

3132
func (c *cluster) TagsAttribute(ctx context.Context, d *diag.Diagnostics) []*console.TagAttributes {
@@ -94,6 +95,7 @@ func (c *cluster) FromCreate(cc *console.CreateCluster, _ context.Context, d *di
9495
c.Handle = types.StringPointerValue(cc.CreateCluster.Handle)
9596
c.Protect = types.BoolPointerValue(cc.CreateCluster.Protect)
9697
c.Tags = common.TagsFrom(cc.CreateCluster.Tags, c.Tags, d)
98+
c.AgentDeployed = types.BoolValue(false)
9799
}
98100

99101
func (c *cluster) ClusterVersionFrom(prov *console.ClusterProviderFragment, version, currentVersion *string) types.String {

internal/resource/cluster_operator_handler.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ import (
3333

3434
func InstallOrUpgradeAgent(ctx context.Context, client *client.Client, kubeconfig *common.Kubeconfig, kubeClient *common.KubeClient,
3535
repoUrl string, values *string, consoleUrl string, token string, clusterId string, d *diag.Diagnostics) error {
36+
if lo.IsEmpty(token) {
37+
return fmt.Errorf("deploy token cannot be empty")
38+
}
39+
3640
workingDir, chartPath, err := fetchVendoredAgentChart(consoleUrl)
3741
if err != nil {
3842
d.AddWarning("Client Warning", fmt.Sprintf("Could not fetch vendored agent chart, using chart from the registry: %s", err))

internal/resource/cluster_schema.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package resource
22

33
import (
44
"terraform-provider-plural/internal/common"
5+
resource "terraform-provider-plural/internal/planmodifier"
56

67
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
78
"github.com/pluralsh/plural-cli/pkg/console"
@@ -18,6 +19,7 @@ func (r *clusterResource) schema() schema.Schema {
1819
return schema.Schema{
1920
Description: "A representation of a cluster you can deploy to.",
2021
MarkdownDescription: "A representation of a cluster you can deploy to.",
22+
Version: 1,
2123
Attributes: map[string]schema.Attribute{
2224
"id": schema.StringAttribute{
2325
Description: "Internal identifier of this cluster.",
@@ -134,6 +136,12 @@ func (r *clusterResource) schema() schema.Schema {
134136
},
135137
PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()},
136138
},
139+
"agent_deployed": schema.BoolAttribute{
140+
Description: "Whether the agent was deployed to the cluster.",
141+
MarkdownDescription: "Whether the agent was deployed to the cluster.",
142+
Computed: true,
143+
PlanModifiers: []planmodifier.Bool{resource.EnsureAgent()},
144+
},
137145
},
138146
}
139147
}

0 commit comments

Comments
 (0)