Skip to content

Commit 5431e7c

Browse files
committed
feat(update-app-base): add support to update base for application charms
This PR adds support to update the base in application charms by requiring a replace in case of a machine charm, and perform the upgrade in case of a k8s charm. re #635
1 parent fbd1ccc commit 5431e7c

File tree

5 files changed

+141
-7
lines changed

5 files changed

+141
-7
lines changed

docs/resources/application.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Notes:
7474
### Read-Only
7575

7676
- `id` (String) The ID of this resource.
77+
- `model_type` (String) The type of the model where the application is to be deployed. It is a computed field and is needed to determine if the application should be replaced or updated in case of base updates.
7778
- `principal` (Boolean, Deprecated) Whether this is a Principal application
7879

7980
<a id="nestedblock--charm"></a>
@@ -85,7 +86,7 @@ Required:
8586

8687
Optional:
8788

88-
- `base` (String) The operating system on which to deploy. E.g. ubuntu@22.04.
89+
- `base` (String) The operating system on which to deploy. E.g. ubuntu@22.04. Changing this value for machine charms will trigger a replace by terraform.
8990
- `channel` (String) The channel to use when deploying a charm. Specified as \<track>/\<risk>/\<branch>.
9091
- `revision` (Number) The revision of the charm to deploy. During the update phase, the charm revision should be update before config update, to avoid issues with config parameters parsing.
9192
- `series` (String, Deprecated) The series on which to deploy.

internal/juju/applications.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ type UpdateApplicationInput struct {
310310
Unexpose []string
311311
Config map[string]string
312312
//Series string // Unsupported today
313+
Base string
313314
Placement map[string]interface{}
314315
Constraints *constraints.Value
315316
EndpointBindings map[string]string
@@ -1194,7 +1195,7 @@ func (c applicationsClient) UpdateApplication(input *UpdateApplicationInput) err
11941195
// before the operations with config. Because the config params
11951196
// can be changed from one revision to another. So "Revision-Config"
11961197
// ordering will help to prevent issues with the configuration parsing.
1197-
if input.Revision != nil || input.Channel != "" || len(input.Resources) != 0 {
1198+
if input.Revision != nil || input.Channel != "" || len(input.Resources) != 0 || input.Base != "" {
11981199
setCharmConfig, err := c.computeSetCharmConfig(input, applicationAPIClient, charmsAPIClient, resourcesAPIClient)
11991200
if err != nil {
12001201
return err
@@ -1378,6 +1379,12 @@ func (c applicationsClient) computeSetCharmConfig(
13781379
if parsedChannel.Branch != "" {
13791380
newOrigin.Branch = strPtr(parsedChannel.Branch)
13801381
}
1382+
} else if input.Base != "" {
1383+
base, err := corebase.ParseBaseFromString(input.Base)
1384+
if err != nil {
1385+
return nil, err
1386+
}
1387+
newOrigin.Base = base
13811388
}
13821389

13831390
resolvedURL, resolvedOrigin, supportedBases, err := resolveCharm(charmsAPIClient, newURL, newOrigin)
@@ -1405,6 +1412,8 @@ func (c applicationsClient) computeSetCharmConfig(
14051412
oldOrigin.Track = newOrigin.Track
14061413
oldOrigin.Risk = newOrigin.Risk
14071414
oldOrigin.Branch = newOrigin.Branch
1415+
} else if input.Base != "" {
1416+
oldOrigin.Base = newOrigin.Base
14081417
}
14091418

14101419
resultOrigin, err := charmsAPIClient.AddCharm(resolvedURL, oldOrigin, false)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2024 Canonical Ltd.
2+
// Licensed under the AGPLv3, see LICENCE file for details.
3+
4+
package provider
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
11+
"github.com/juju/juju/core/model"
12+
)
13+
14+
// baseApplicationRequiresReplaceIf is a plan modifier that sets the RequiresReplace field if the
15+
// model type is IAAS. The reason is that with CAAS the application can be updated in place.
16+
// With IAAS the application needs to be replaced. To make this decision the model type is needed.
17+
// Since you can't access the juju client in the plan modifiers we've added a computed field `model_type`.
18+
// This is set in the state by means of the `stringplanmodifier.UseStateForUnknown()`, so when we update the base
19+
// is always guaranteed to be set.
20+
func baseApplicationRequiresReplaceIf(ctx context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) {
21+
if req.State.Raw.IsKnown() {
22+
var state applicationResourceModel
23+
diags := req.State.Get(ctx, &state)
24+
if diags.HasError() {
25+
resp.Diagnostics.Append(diags...)
26+
return
27+
}
28+
modelType := state.ModelType.ValueString()
29+
resp.RequiresReplace = modelType == model.IAAS.String()
30+
}
31+
}

internal/provider/resource_application.go

+32-5
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ type applicationResourceModel struct {
8282
Constraints types.String `tfsdk:"constraints"`
8383
Expose types.List `tfsdk:"expose"`
8484
ModelName types.String `tfsdk:"model"`
85+
ModelType types.String `tfsdk:"model_type"`
8586
Placement types.String `tfsdk:"placement"`
8687
EndpointBindings types.Set `tfsdk:"endpoint_bindings"`
8788
Resources types.Map `tfsdk:"resources"`
@@ -147,6 +148,14 @@ func (r *applicationResource) Schema(_ context.Context, _ resource.SchemaRequest
147148
stringplanmodifier.RequiresReplaceIfConfigured(),
148149
},
149150
},
151+
"model_type": schema.StringAttribute{
152+
Description: "The type of the model where the application is to be deployed. It is a computed field and " +
153+
"is needed to determine if the application should be replaced or updated in case of base updates.",
154+
Computed: true,
155+
PlanModifiers: []planmodifier.String{
156+
stringplanmodifier.UseStateForUnknown(),
157+
},
158+
},
150159
"units": schema.Int64Attribute{
151160
Description: "The number of application units to deploy for the charm.",
152161
Optional: true,
@@ -316,11 +325,12 @@ func (r *applicationResource) Schema(_ context.Context, _ resource.SchemaRequest
316325
DeprecationMessage: "Configure base instead. This attribute will be removed in the next major version of the provider.",
317326
},
318327
BaseKey: schema.StringAttribute{
319-
Description: "The operating system on which to deploy. E.g. ubuntu@22.04.",
328+
Description: "The operating system on which to deploy. E.g. ubuntu@22.04. Changing this value for machine charms will trigger a replace by terraform.",
320329
Optional: true,
321330
Computed: true,
322331
PlanModifiers: []planmodifier.String{
323332
stringplanmodifier.UseStateForUnknown(),
333+
stringplanmodifier.RequiresReplaceIf(baseApplicationRequiresReplaceIf, "", ""),
324334
},
325335
Validators: []validator.String{
326336
stringvalidator.ConflictsWith(path.Expressions{
@@ -582,6 +592,11 @@ func (r *applicationResource) Create(ctx context.Context, req resource.CreateReq
582592
}
583593
r.trace(fmt.Sprintf("read application resource %q", createResp.AppName))
584594

595+
modelType, err := r.client.Applications.ModelType(modelName)
596+
if err != nil {
597+
resp.Diagnostics.Append(handleApplicationNotFoundError(ctx, err, &resp.State)...)
598+
return
599+
}
585600
// Save plan into Terraform state
586601

587602
// Constraints do not apply to subordinate applications. If the application
@@ -590,6 +605,7 @@ func (r *applicationResource) Create(ctx context.Context, req resource.CreateReq
590605
plan.Placement = types.StringValue(readResp.Placement)
591606
plan.Principal = types.BoolNull()
592607
plan.ApplicationName = types.StringValue(createResp.AppName)
608+
plan.ModelType = types.StringValue(modelType.String())
593609
planCharm.Revision = types.Int64Value(int64(readResp.Revision))
594610
planCharm.Base = types.StringValue(readResp.Base)
595611
planCharm.Series = types.StringValue(readResp.Series)
@@ -692,6 +708,12 @@ func (r *applicationResource) Read(ctx context.Context, req resource.ReadRequest
692708
}
693709
r.trace("read application", map[string]interface{}{"resource": appName, "response": response})
694710

711+
modelType, err := r.client.Applications.ModelType(modelName)
712+
if err != nil {
713+
resp.Diagnostics.Append(handleApplicationNotFoundError(ctx, err, &resp.State)...)
714+
return
715+
}
716+
695717
state.ApplicationName = types.StringValue(appName)
696718
state.ModelName = types.StringValue(modelName)
697719

@@ -700,6 +722,7 @@ func (r *applicationResource) Read(ctx context.Context, req resource.ReadRequest
700722
state.Placement = types.StringValue(response.Placement)
701723
state.Principal = types.BoolNull()
702724
state.UnitCount = types.Int64Value(int64(response.Units))
725+
state.ModelType = types.StringValue(modelType.String())
703726
state.Trust = types.BoolValue(response.Trust)
704727

705728
// state requiring transformation
@@ -919,16 +942,18 @@ func (r *applicationResource) Update(ctx context.Context, req resource.UpdateReq
919942
} else if !planCharm.Revision.Equal(stateCharm.Revision) {
920943
updateApplicationInput.Revision = intPtr(planCharm.Revision)
921944
}
922-
923-
if !planCharm.Series.Equal(stateCharm.Series) || !planCharm.Base.Equal(stateCharm.Base) {
945+
if !planCharm.Base.Equal(stateCharm.Base) {
946+
updateApplicationInput.Base = planCharm.Base.ValueString()
947+
}
948+
if !planCharm.Series.Equal(stateCharm.Series) {
924949
// This violates Terraform's declarative model. We could implement
925950
// `juju set-application-base`, usually used after `upgrade-machine`,
926951
// which would change the operating system used for future units of
927952
// the application provided the charm supported it, but not change
928953
// the current. This provider does not implement an equivalent to
929954
// `upgrade-machine`. There is also a question of how to handle a
930955
// change to series, revision and channel at the same time.
931-
resp.Diagnostics.AddWarning("Not Supported", "Changing an application's operating system after deploy.")
956+
resp.Diagnostics.AddWarning("Not Supported", "Changing operating system's series after deploy.")
932957
}
933958
}
934959

@@ -1051,7 +1076,8 @@ func (r *applicationResource) Update(ctx context.Context, req resource.UpdateReq
10511076
if updateApplicationInput.Channel != "" ||
10521077
updateApplicationInput.Revision != nil ||
10531078
updateApplicationInput.Placement != nil ||
1054-
updateApplicationInput.Units != nil {
1079+
updateApplicationInput.Units != nil ||
1080+
updateApplicationInput.Base != "" {
10551081
readResp, err := r.client.Applications.ReadApplicationWithRetryOnNotFound(ctx, &juju.ReadApplicationInput{
10561082
ModelName: updateApplicationInput.ModelName,
10571083
AppName: updateApplicationInput.AppName,
@@ -1090,6 +1116,7 @@ func (r *applicationResource) Update(ctx context.Context, req resource.UpdateReq
10901116
}
10911117
}
10921118

1119+
plan.ModelType = state.ModelType
10931120
plan.ID = types.StringValue(newAppID(plan.ModelName.ValueString(), plan.ApplicationName.ValueString()))
10941121
plan.Principal = types.BoolNull()
10951122
r.trace("Updated", applicationResourceModelForLogging(ctx, &plan))

internal/provider/resource_application_test.go

+66
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,37 @@ func TestAcc_CharmUpdates(t *testing.T) {
276276
})
277277
}
278278

279+
func TestAcc_CharmUpdateBase(t *testing.T) {
280+
modelName := acctest.RandomWithPrefix("tf-test-charmbaseupdates")
281+
282+
resource.ParallelTest(t, resource.TestCase{
283+
PreCheck: func() { testAccPreCheck(t) },
284+
ProtoV6ProviderFactories: frameworkProviderFactories,
285+
Steps: []resource.TestStep{
286+
{
287+
Config: testAccApplicationUpdateBaseCharm(modelName, "ubuntu@22.04"),
288+
Check: resource.ComposeTestCheckFunc(
289+
resource.TestCheckResourceAttr("juju_application.this", "charm.0.base", "ubuntu@22.04"),
290+
),
291+
},
292+
{
293+
// move to base ubuntu 20.04
294+
Config: testAccApplicationUpdateBaseCharm(modelName, "ubuntu@20.04"),
295+
Check: resource.ComposeTestCheckFunc(
296+
resource.TestCheckResourceAttr("juju_application.this", "charm.0.base", "ubuntu@20.04"),
297+
),
298+
},
299+
{
300+
// move back to ubuntu 22.04
301+
Config: testAccApplicationUpdateBaseCharm(modelName, "ubuntu@22.04"),
302+
Check: resource.ComposeTestCheckFunc(
303+
resource.TestCheckResourceAttr("juju_application.this", "charm.0.base", "ubuntu@22.04"),
304+
),
305+
},
306+
},
307+
})
308+
}
309+
279310
func TestAcc_ResourceRevisionUpdatesLXD(t *testing.T) {
280311
if testingCloud != LXDCloudTesting {
281312
t.Skip(t.Name() + " only runs with LXD")
@@ -1021,6 +1052,41 @@ func testAccResourceApplicationUpdatesCharm(modelName string, channel string) st
10211052
}
10221053
}
10231054

1055+
func testAccApplicationUpdateBaseCharm(modelName string, base string) string {
1056+
if testingCloud == LXDCloudTesting {
1057+
return fmt.Sprintf(`
1058+
resource "juju_model" "this" {
1059+
name = %q
1060+
}
1061+
1062+
resource "juju_application" "this" {
1063+
model = juju_model.this.name
1064+
name = "test-app"
1065+
charm {
1066+
name = "ubuntu"
1067+
base = %q
1068+
}
1069+
}
1070+
`, modelName, base)
1071+
} else {
1072+
return fmt.Sprintf(`
1073+
resource "juju_model" "this" {
1074+
name = %q
1075+
}
1076+
1077+
resource "juju_application" "this" {
1078+
model = juju_model.this.name
1079+
name = "test-app"
1080+
charm {
1081+
name = "coredns"
1082+
channel = "1.25/stable"
1083+
base = %q
1084+
}
1085+
}
1086+
`, modelName, base)
1087+
}
1088+
}
1089+
10241090
// testAccResourceApplicationConstraints will return two set for constraint
10251091
// applications. The version to be used in K8s sets the juju-external-hostname
10261092
// because we set the expose parameter.

0 commit comments

Comments
 (0)