Skip to content

Commit 19c361e

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 19c361e

File tree

4 files changed

+120
-5
lines changed

4 files changed

+120
-5
lines changed

docs/resources/application.md

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ resource "juju_application" "this" {
5656
- `constraints` (String) Constraints imposed on this application. Changing this value will cause the application to be destroyed and recreated by terraform.
5757
- `endpoint_bindings` (Attributes Set) Configure endpoint bindings (see [below for nested schema](#nestedatt--endpoint_bindings))
5858
- `expose` (Block List) Makes an application publicly available over the network (see [below for nested schema](#nestedblock--expose))
59+
- `model_type` (String)
5960
- `name` (String) A custom name for the application deployment. If empty, uses the charm's name.Changing this value will cause the application to be destroyed and recreated by terraform.
6061
- `placement` (String) Specify the target location for the application's units. Changing this value will cause the application to be destroyed and recreated by terraform.
6162
- `resources` (Map of String) Charm resources. Must evaluate to a string. A resource could be a resource revision number from CharmHub or a custom OCI image resource.

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)

internal/provider/resource_application.go

+43-4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/hashicorp/terraform-plugin-log/tflog"
3030
"github.com/juju/errors"
3131
"github.com/juju/juju/core/constraints"
32+
"github.com/juju/juju/core/model"
3233
jujustorage "github.com/juju/juju/storage"
3334

3435
"github.com/juju/terraform-provider-juju/internal/juju"
@@ -82,6 +83,7 @@ type applicationResourceModel struct {
8283
Constraints types.String `tfsdk:"constraints"`
8384
Expose types.List `tfsdk:"expose"`
8485
ModelName types.String `tfsdk:"model"`
86+
ModelType types.String `tfsdk:"model_type"`
8587
Placement types.String `tfsdk:"placement"`
8688
EndpointBindings types.Set `tfsdk:"endpoint_bindings"`
8789
Resources types.Map `tfsdk:"resources"`
@@ -147,6 +149,14 @@ func (r *applicationResource) Schema(_ context.Context, _ resource.SchemaRequest
147149
stringplanmodifier.RequiresReplaceIfConfigured(),
148150
},
149151
},
152+
"model_type": schema.StringAttribute{
153+
Description: "",
154+
Computed: true,
155+
Optional: true,
156+
PlanModifiers: []planmodifier.String{
157+
stringplanmodifier.UseStateForUnknown(),
158+
},
159+
},
150160
"units": schema.Int64Attribute{
151161
Description: "The number of application units to deploy for the charm.",
152162
Optional: true,
@@ -321,6 +331,18 @@ func (r *applicationResource) Schema(_ context.Context, _ resource.SchemaRequest
321331
Computed: true,
322332
PlanModifiers: []planmodifier.String{
323333
stringplanmodifier.UseStateForUnknown(),
334+
stringplanmodifier.RequiresReplaceIf(func(ctx context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) {
335+
if req.State.Raw.IsKnown() {
336+
var state applicationResourceModel
337+
diags := req.State.Get(ctx, &state)
338+
if diags.HasError() {
339+
resp.Diagnostics.Append(diags...)
340+
return
341+
}
342+
modelType := state.ModelType.ValueString()
343+
resp.RequiresReplace = modelType == model.IAAS.String()
344+
}
345+
}, "", ""),
324346
},
325347
Validators: []validator.String{
326348
stringvalidator.ConflictsWith(path.Expressions{
@@ -582,6 +604,11 @@ func (r *applicationResource) Create(ctx context.Context, req resource.CreateReq
582604
}
583605
r.trace(fmt.Sprintf("read application resource %q", createResp.AppName))
584606

607+
modelType, err := r.client.Applications.ModelType(modelName)
608+
if err != nil {
609+
resp.Diagnostics.Append(handleApplicationNotFoundError(ctx, err, &resp.State)...)
610+
return
611+
}
585612
// Save plan into Terraform state
586613

587614
// Constraints do not apply to subordinate applications. If the application
@@ -590,6 +617,7 @@ func (r *applicationResource) Create(ctx context.Context, req resource.CreateReq
590617
plan.Placement = types.StringValue(readResp.Placement)
591618
plan.Principal = types.BoolNull()
592619
plan.ApplicationName = types.StringValue(createResp.AppName)
620+
plan.ModelType = types.StringValue(modelType.String())
593621
planCharm.Revision = types.Int64Value(int64(readResp.Revision))
594622
planCharm.Base = types.StringValue(readResp.Base)
595623
planCharm.Series = types.StringValue(readResp.Series)
@@ -692,6 +720,12 @@ func (r *applicationResource) Read(ctx context.Context, req resource.ReadRequest
692720
}
693721
r.trace("read application", map[string]interface{}{"resource": appName, "response": response})
694722

723+
modelType, err := r.client.Applications.ModelType(modelName)
724+
if err != nil {
725+
resp.Diagnostics.Append(handleApplicationNotFoundError(ctx, err, &resp.State)...)
726+
return
727+
}
728+
695729
state.ApplicationName = types.StringValue(appName)
696730
state.ModelName = types.StringValue(modelName)
697731

@@ -700,6 +734,7 @@ func (r *applicationResource) Read(ctx context.Context, req resource.ReadRequest
700734
state.Placement = types.StringValue(response.Placement)
701735
state.Principal = types.BoolNull()
702736
state.UnitCount = types.Int64Value(int64(response.Units))
737+
state.ModelType = types.StringValue(modelType.String())
703738
state.Trust = types.BoolValue(response.Trust)
704739

705740
// state requiring transformation
@@ -919,16 +954,18 @@ func (r *applicationResource) Update(ctx context.Context, req resource.UpdateReq
919954
} else if !planCharm.Revision.Equal(stateCharm.Revision) {
920955
updateApplicationInput.Revision = intPtr(planCharm.Revision)
921956
}
922-
923-
if !planCharm.Series.Equal(stateCharm.Series) || !planCharm.Base.Equal(stateCharm.Base) {
957+
if !planCharm.Base.Equal(stateCharm.Base) {
958+
updateApplicationInput.Base = planCharm.Base.ValueString()
959+
}
960+
if !planCharm.Series.Equal(stateCharm.Series) {
924961
// This violates Terraform's declarative model. We could implement
925962
// `juju set-application-base`, usually used after `upgrade-machine`,
926963
// which would change the operating system used for future units of
927964
// the application provided the charm supported it, but not change
928965
// the current. This provider does not implement an equivalent to
929966
// `upgrade-machine`. There is also a question of how to handle a
930967
// change to series, revision and channel at the same time.
931-
resp.Diagnostics.AddWarning("Not Supported", "Changing an application's operating system after deploy.")
968+
resp.Diagnostics.AddWarning("Not Supported", "Changing operating system's series after deploy.")
932969
}
933970
}
934971

@@ -1051,7 +1088,8 @@ func (r *applicationResource) Update(ctx context.Context, req resource.UpdateReq
10511088
if updateApplicationInput.Channel != "" ||
10521089
updateApplicationInput.Revision != nil ||
10531090
updateApplicationInput.Placement != nil ||
1054-
updateApplicationInput.Units != nil {
1091+
updateApplicationInput.Units != nil ||
1092+
updateApplicationInput.Base != "" {
10551093
readResp, err := r.client.Applications.ReadApplicationWithRetryOnNotFound(ctx, &juju.ReadApplicationInput{
10561094
ModelName: updateApplicationInput.ModelName,
10571095
AppName: updateApplicationInput.AppName,
@@ -1090,6 +1128,7 @@ func (r *applicationResource) Update(ctx context.Context, req resource.UpdateReq
10901128
}
10911129
}
10921130

1131+
plan.ModelType = state.ModelType
10931132
plan.ID = types.StringValue(newAppID(plan.ModelName.ValueString(), plan.ApplicationName.ValueString()))
10941133
plan.Principal = types.BoolNull()
10951134
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 22
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 latest/stable
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)