Skip to content

Commit 8b7033d

Browse files
committed
Add generation support to apps with CNB validation and acceptance tests
1 parent 65df003 commit 8b7033d

File tree

6 files changed

+348
-12
lines changed

6 files changed

+348
-12
lines changed

docs/resources/app.md

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@ description: |-
1010

1111
Provides a Heroku App resource. This can be used to create and manage applications on Heroku.
1212

13+
The Heroku platform supports two generations:
14+
- **Cedar** (default): Traditional platform with support for buildpacks, stack configuration, and internal routing
15+
- **Fir**: Next-generation platform with Cloud Native Buildpacks (CNB), enhanced security, and modern containerization
16+
1317
-> **Always reference apps by ID (UUID) in Terraform configuration**
1418
Starting with v5.0 of this provider, all HCL app references are by ID. Read more details in [Upgrading](guides/upgrading.html).
1519

1620
## Example Usage
1721

22+
### Cedar Generation (Default)
1823
```hcl-terraform
19-
resource "heroku_app" "default" {
20-
name = "my-cool-app"
24+
resource "heroku_app" "cedar_app" {
25+
name = "my-cedar-app"
2126
region = "us"
2227
2328
config_vars = {
@@ -27,6 +32,24 @@ resource "heroku_app" "default" {
2732
buildpacks = [
2833
"heroku/go"
2934
]
35+
36+
stack = "heroku-22"
37+
}
38+
```
39+
40+
### Fir Generation (Cloud Native Buildpacks)
41+
```hcl-terraform
42+
resource "heroku_app" "fir_app" {
43+
name = "my-fir-app"
44+
region = "us"
45+
generation = "fir"
46+
47+
config_vars = {
48+
FOOBAR = "baz"
49+
}
50+
51+
# Note: buildpacks and stack are not supported for Fir generation
52+
# Use project.toml in your application code instead
3053
}
3154
```
3255

@@ -52,9 +75,12 @@ The following arguments are supported:
5275
* `name` - (Required) The name of the application. In Heroku, this is also the
5376
unique ID, so it must be unique and have a minimum of 3 characters.
5477
* `region` - (Required) The region that the app should be deployed in.
55-
* `stack` - (Optional) The application stack is what platform to run the application in.
78+
* `generation` - (Optional) Generation of the app platform. Valid values are `cedar` and `fir`. Defaults to `cedar` for backward compatibility. **ForceNew**.
79+
- **Cedar**: Traditional platform supporting buildpacks, stack configuration, and internal routing
80+
- **Fir**: Next-generation platform with Cloud Native Buildpacks (CNB). Does not support `buildpacks`, `stack`, or `internal_routing` fields
81+
* `stack` - (Optional) The application stack is what platform to run the application in. **Note**: Not supported for `fir` generation apps.
5682
* `buildpacks` - (Optional) Buildpack names or URLs for the application.
57-
Buildpacks configured externally won't be altered if this is not present.
83+
Buildpacks configured externally won't be altered if this is not present. **Note**: Not supported for `fir` generation apps. Use project.toml for Cloud Native Buildpacks configuration instead.
5884
* `config_vars`<sup>[1](#deleting-vars)</sup> - (Optional) Configuration variables for the application.
5985
The config variables in this map are not the final set of configuration
6086
variables, but rather variables you want present. That is, other
@@ -68,7 +94,7 @@ The following arguments are supported:
6894
* `space` - (Optional) The name of a private space to create the app in.
6995
* `internal_routing` - (Optional) If true, the application will be routable
7096
only internally in a private space. This option is only available for apps
71-
that also specify `space`.
97+
that also specify `space`. **Note**: Not supported for `fir` generation apps.
7298
* `organization` - (Optional) A block that can be specified once to define
7399
Heroku Team settings for this app. The fields for this block are
74100
documented below.
@@ -96,6 +122,7 @@ The following attributes are exported:
96122

97123
* `id` - The ID (UUID) of the app.
98124
* `name` - The name of the app.
125+
* `generation` - Generation of the app platform (cedar or fir). Defaults to cedar.
99126
* `stack` - The application stack is what platform to run the application in.
100127
* `space` - The private space the app should run in.
101128
* `internal_routing` - Whether internal routing is enabled the private space app.
@@ -113,6 +140,55 @@ The following attributes are exported:
113140
attribute `set_app_all_config_vars_in_state` is `false`.
114141
* `uuid` - The unique UUID of the Heroku app. **NOTE:** Use this for `null_resource` triggers.
115142

143+
## Cloud Native Buildpacks (Fir Generation)
144+
145+
When using the Fir generation (`generation = "fir"`), applications use Cloud Native Buildpacks (CNB) instead of traditional Heroku buildpacks. This requires different configuration approaches:
146+
147+
### project.toml Configuration
148+
149+
Instead of specifying `buildpacks` in Terraform, create a `project.toml` file in your application root:
150+
151+
```toml
152+
[build]
153+
[[build.buildpacks]]
154+
id = "heroku/nodejs"
155+
156+
[[build.buildpacks]]
157+
id = "heroku/procfile"
158+
159+
[build.env]
160+
BP_NODE_VERSION = "18.*"
161+
```
162+
163+
### Migration from Cedar to Fir
164+
165+
When migrating from Cedar to Fir generation:
166+
167+
1. **Remove incompatible fields**: Remove `buildpacks`, `stack`, and `internal_routing` from your Terraform configuration
168+
2. **Add project.toml**: Create a `project.toml` file in your application code with buildpack configuration
169+
3. **Update generation**: Set `generation = "fir"` in your app resource
170+
4. **Redeploy**: Deploy your application with the new configuration
171+
172+
```hcl-terraform
173+
# Before (Cedar)
174+
resource "heroku_app" "example" {
175+
name = "my-app"
176+
region = "us"
177+
178+
buildpacks = ["heroku/nodejs"]
179+
stack = "heroku-22"
180+
}
181+
182+
# After (Fir)
183+
resource "heroku_app" "example" {
184+
name = "my-app"
185+
region = "us"
186+
generation = "fir"
187+
188+
# buildpacks and stack removed - configured via project.toml
189+
}
190+
```
191+
116192
## Import
117193

118194
Apps can be imported using an existing app's `UUID` or name.

heroku/heroku_supported_features.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ var featureMatrix = map[string]map[string]map[string]bool{
2020
"inbound_ruleset": true, // Cedar supports inbound rulesets
2121
"peering_connection": true, // Cedar supports IPv4 peering
2222
},
23+
"app": {
24+
"buildpacks": true, // Traditional buildpacks supported
25+
"stack": true, // Stack setting supported
26+
"internal_routing": true, // Internal routing supported
27+
"cloud_native_buildpacks": false, // CNB not supported on Cedar
28+
},
2329
},
2430
"fir": {
2531
"space": {
@@ -34,6 +40,12 @@ var featureMatrix = map[string]map[string]map[string]bool{
3440
"inbound_ruleset": false, // Inbound rulesets not supported
3541
"peering_connection": false, // IPv4 peering not supported
3642
},
43+
"app": {
44+
"buildpacks": false, // Traditional buildpacks not supported
45+
"stack": false, // Stack setting not supported
46+
"internal_routing": false, // Internal routing not supported
47+
"cloud_native_buildpacks": true, // CNB supported on Fir
48+
},
3749
},
3850
}
3951

heroku/heroku_supported_features_test.go

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,63 @@ func TestIsFeatureSupported(t *testing.T) {
7575
},
7676

7777
// Unknown resource type tests
78+
// App feature tests
7879
{
79-
name: "Cedar app features should be unsupported (not implemented yet)",
80+
name: "Cedar app buildpacks should be supported",
8081
generation: "cedar",
8182
resourceType: "app",
82-
feature: "some_feature",
83+
feature: "buildpacks",
84+
expected: true,
85+
},
86+
{
87+
name: "Cedar app stack should be supported",
88+
generation: "cedar",
89+
resourceType: "app",
90+
feature: "stack",
91+
expected: true,
92+
},
93+
{
94+
name: "Cedar app internal_routing should be supported",
95+
generation: "cedar",
96+
resourceType: "app",
97+
feature: "internal_routing",
98+
expected: true,
99+
},
100+
{
101+
name: "Cedar app cloud_native_buildpacks should be unsupported",
102+
generation: "cedar",
103+
resourceType: "app",
104+
feature: "cloud_native_buildpacks",
105+
expected: false,
106+
},
107+
{
108+
name: "Fir app buildpacks should be unsupported",
109+
generation: "fir",
110+
resourceType: "app",
111+
feature: "buildpacks",
112+
expected: false,
113+
},
114+
{
115+
name: "Fir app stack should be unsupported",
116+
generation: "fir",
117+
resourceType: "app",
118+
feature: "stack",
119+
expected: false,
120+
},
121+
{
122+
name: "Fir app internal_routing should be unsupported",
123+
generation: "fir",
124+
resourceType: "app",
125+
feature: "internal_routing",
83126
expected: false,
84127
},
128+
{
129+
name: "Fir app cloud_native_buildpacks should be supported",
130+
generation: "fir",
131+
resourceType: "app",
132+
feature: "cloud_native_buildpacks",
133+
expected: true,
134+
},
85135
{
86136
name: "Fir build features should be unsupported (not implemented yet)",
87137
generation: "fir",

heroku/resource_heroku_app.go

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,12 @@ type application struct {
4949

5050
func resourceHerokuApp() *schema.Resource {
5151
return &schema.Resource{
52-
Create: switchHerokuAppCreate,
53-
Read: resourceHerokuAppRead,
54-
Update: resourceHerokuAppUpdate,
55-
Delete: resourceHerokuAppDelete,
56-
Exists: resourceHerokuAppExists,
52+
Create: switchHerokuAppCreate,
53+
Read: resourceHerokuAppRead,
54+
Update: resourceHerokuAppUpdate,
55+
Delete: resourceHerokuAppDelete,
56+
Exists: resourceHerokuAppExists,
57+
CustomizeDiff: resourceHerokuAppCustomizeDiff,
5758

5859
Importer: &schema.ResourceImporter{
5960
State: resourceHerokuAppImport,
@@ -77,6 +78,15 @@ func resourceHerokuApp() *schema.Resource {
7778
ForceNew: true,
7879
},
7980

81+
"generation": {
82+
Type: schema.TypeString,
83+
Optional: true,
84+
Default: "cedar",
85+
ForceNew: true,
86+
ValidateFunc: validation.StringInSlice([]string{"cedar", "fir"}, false),
87+
Description: "Generation of the app platform. Defaults to cedar for backward compatibility.",
88+
},
89+
8090
"stack": {
8191
Type: schema.TypeString,
8292
Optional: true,
@@ -1034,3 +1044,34 @@ func resourceHerokuAppStateUpgradeV0(ctx context.Context, rawState map[string]in
10341044

10351045
return rawState, nil
10361046
}
1047+
1048+
func resourceHerokuAppCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error {
1049+
generation, generationExists := diff.GetOk("generation")
1050+
1051+
if generationExists {
1052+
generationStr := generation.(string)
1053+
1054+
// Validate buildpacks field
1055+
if buildpacks, buildpacksExists := diff.GetOk("buildpacks"); buildpacksExists {
1056+
buildpacksList := buildpacks.([]interface{})
1057+
if len(buildpacksList) > 0 && !IsFeatureSupported(generationStr, "app", "buildpacks") {
1058+
return fmt.Errorf("buildpacks are not supported for %s generation apps. Use Cloud Native Buildpacks and configure via project.toml instead", generationStr)
1059+
}
1060+
}
1061+
1062+
// Validate stack field
1063+
if stack, stackExists := diff.GetOk("stack"); stackExists {
1064+
if stack.(string) != "" && !IsFeatureSupported(generationStr, "app", "stack") {
1065+
return fmt.Errorf("stack configuration is not supported for %s generation apps. Remove the 'stack' field", generationStr)
1066+
}
1067+
}
1068+
1069+
// Validate internal_routing field
1070+
if internalRouting, internalRoutingExists := diff.GetOk("internal_routing"); internalRoutingExists {
1071+
if internalRouting.(bool) && !IsFeatureSupported(generationStr, "app", "internal_routing") {
1072+
return fmt.Errorf("internal routing is not supported for %s generation apps", generationStr)
1073+
}
1074+
}
1075+
}
1076+
return nil
1077+
}

0 commit comments

Comments
 (0)