Skip to content

Commit 0064690

Browse files
heroku-johnnymars
andauthored
Add Fir generation support for Heroku Apps (#402)
* Add foundational feature matrix system for generation support - Implement IsFeatureSupported() helper function for Cedar/Fir generation differences - Add feature matrix tracking space capabilities across generations - Include comprehensive test coverage with 14 test cases - Cedar generation: supports all space features including shield spaces - Fir generation: supports private spaces only, shield spaces unsupported - Foundation for graceful handling of generation-specific feature differences * Add generation support to apps with CNB validation and acceptance tests * Refactor app generation to be computed from space Major UX improvement: generation is now automatically determined from the space the app is deployed to, rather than user-configured. Changes: - App 'generation' field is now computed-only (was optional) - Apps in Fir spaces automatically get generation = 'fir' - Apps in Cedar spaces (or no space) get generation = 'cedar' Core Improvements: - Fix CNB buildpack errors by conditionally querying based on generation - Smart buildpack handling: skip traditional queries for Fir apps - Automatic space generation detection via SpaceInfo API - Better error prevention and more intuitive configuration Test Improvements: - Consolidate acceptance tests with single space approach - Remove invalid validation tests (no longer user-configurable) - Update test expectations for computed generation behavior - Maintain comprehensive coverage for both generations Documentation Updates: - Update examples to show space-based generation approach - Clarify that generation is computed from space deployment - Improve migration guide with proper space-first workflow - Update argument/attribute references for computed field * Update docs/resources/app.md Co-authored-by: Mars Hall <[email protected]> Signed-off-by: Johnny Winn <[email protected]> * Optimize app generation detection by using AppInfo response - Use app.Generation.Name directly from AppInfo response instead of making separate SpaceInfo API call - Remove unnecessary getSpaceGeneration() function and SpaceInfo API request - Maintains same functionality with better performance and fewer API calls - Addresses PR feedback about unnecessary API requests --------- Signed-off-by: Johnny Winn <[email protected]> Co-authored-by: Mars Hall <[email protected]>
1 parent 13f75a0 commit 0064690

File tree

6 files changed

+351
-15
lines changed

6 files changed

+351
-15
lines changed

docs/resources/app.md

Lines changed: 110 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,37 @@ resource "heroku_app" "default" {
2732
buildpacks = [
2833
"heroku/go"
2934
]
35+
36+
stack = "heroku-22"
37+
}
38+
```
39+
40+
### Fir Generation (via Fir Space)
41+
```hcl-terraform
42+
# Create a Fir generation space first
43+
resource "heroku_space" "fir_space" {
44+
name = "my-fir-space"
45+
organization = "my-org"
46+
region = "virginia"
47+
generation = "fir"
48+
}
49+
50+
# Apps deployed to Fir spaces automatically use Fir generation
51+
resource "heroku_app" "fir_app" {
52+
name = "my-fir-app"
53+
region = "virginia"
54+
space = heroku_space.fir_space.name
55+
56+
organization {
57+
name = "my-org"
58+
}
59+
60+
config_vars = {
61+
FOOBAR = "baz"
62+
}
63+
64+
# Note: buildpacks and stack are not supported for Fir generation
65+
# Use project.toml in your application code instead
3066
}
3167
```
3268

@@ -52,9 +88,12 @@ The following arguments are supported:
5288
* `name` - (Required) The name of the application. In Heroku, this is also the
5389
unique ID, so it must be unique and have a minimum of 3 characters.
5490
* `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.
91+
* `generation` - (Computed) Generation of the app platform. Automatically determined based on the space the app is deployed to. Apps in Fir generation spaces will be `fir`, all other apps will be `cedar`.
92+
- `cedar`: Traditional platform supporting buildpacks, stack configuration, and internal routing
93+
- `fir`: Next-generation platform with Cloud Native Buildpacks (CNB). Does not support `buildpacks`, `stack`, or `internal_routing` fields
94+
* `stack` - (Optional) The application stack is what platform to run the application in. **Note**: Not supported for `fir` generation apps.
5695
* `buildpacks` - (Optional) Buildpack names or URLs for the application.
57-
Buildpacks configured externally won't be altered if this is not present.
96+
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.
5897
* `config_vars`<sup>[1](#deleting-vars)</sup> - (Optional) Configuration variables for the application.
5998
The config variables in this map are not the final set of configuration
6099
variables, but rather variables you want present. That is, other
@@ -68,7 +107,7 @@ The following arguments are supported:
68107
* `space` - (Optional) The name of a private space to create the app in.
69108
* `internal_routing` - (Optional) If true, the application will be routable
70109
only internally in a private space. This option is only available for apps
71-
that also specify `space`.
110+
that also specify `space`. **Note**: Not supported for `fir` generation apps.
72111
* `organization` - (Optional) A block that can be specified once to define
73112
Heroku Team settings for this app. The fields for this block are
74113
documented below.
@@ -96,6 +135,7 @@ The following attributes are exported:
96135

97136
* `id` - The ID (UUID) of the app.
98137
* `name` - The name of the app.
138+
* `generation` - Generation of the app platform (cedar or fir). Automatically determined from the space the app is deployed to.
99139
* `stack` - The application stack is what platform to run the application in.
100140
* `space` - The private space the app should run in.
101141
* `internal_routing` - Whether internal routing is enabled the private space app.
@@ -113,6 +153,71 @@ The following attributes are exported:
113153
attribute `set_app_all_config_vars_in_state` is `false`.
114154
* `uuid` - The unique UUID of the Heroku app. **NOTE:** Use this for `null_resource` triggers.
115155

156+
## Cloud Native Buildpacks (Fir Generation)
157+
158+
When apps are deployed to Fir generation spaces, they automatically use Cloud Native Buildpacks (CNB) instead of traditional Heroku buildpacks. This requires different configuration approaches:
159+
160+
### project.toml Configuration
161+
162+
Instead of specifying `buildpacks` in Terraform, create a `project.toml` file in your application root:
163+
164+
```toml
165+
[build]
166+
[[build.buildpacks]]
167+
id = "heroku/nodejs"
168+
169+
[[build.buildpacks]]
170+
id = "heroku/procfile"
171+
172+
[build.env]
173+
BP_NODE_VERSION = "18.*"
174+
```
175+
176+
### Migration from Cedar to Fir
177+
178+
When migrating from Cedar to Fir generation:
179+
180+
1. **Create Fir space**: Create a new space with `generation = "fir"`
181+
2. **Remove incompatible fields**: Remove `buildpacks`, `stack`, and `internal_routing` from your Terraform configuration
182+
3. **Add project.toml**: Create a `project.toml` file in your application code with buildpack configuration
183+
4. **Update app space**: Change your app's `space` to use the Fir space
184+
5. **Redeploy**: Deploy your application with the new configuration
185+
186+
```hcl-terraform
187+
# Before (Cedar)
188+
resource "heroku_space" "cedar_space" {
189+
name = "my-space"
190+
organization = "my-org"
191+
region = "virginia"
192+
}
193+
194+
resource "heroku_app" "example" {
195+
name = "my-app"
196+
region = "virginia"
197+
space = heroku_space.cedar_space.name
198+
199+
buildpacks = ["heroku/nodejs"]
200+
stack = "heroku-22"
201+
}
202+
203+
# After (Fir)
204+
resource "heroku_space" "fir_space" {
205+
name = "my-space-fir"
206+
organization = "my-org"
207+
region = "virginia"
208+
generation = "fir"
209+
}
210+
211+
resource "heroku_app" "example" {
212+
name = "my-app"
213+
region = "virginia"
214+
space = heroku_space.fir_space.name
215+
216+
# buildpacks and stack removed - configured via project.toml
217+
# generation is automatically "fir" from the space
218+
}
219+
```
220+
116221
## Import
117222

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

heroku/heroku_supported_features.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ 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, // Cedar supports traditional buildpacks
25+
"stack": true, // Cedar supports stack configuration
26+
"internal_routing": true, // Cedar supports internal routing
27+
"cloud_native_buildpacks": false, // Cedar doesn't use CNB by default
28+
},
29+
"drain": {
30+
"app_log_drains": true, // Cedar supports traditional log drains
31+
},
2332
},
2433
"fir": {
2534
"space": {
@@ -34,6 +43,15 @@ var featureMatrix = map[string]map[string]map[string]bool{
3443
"inbound_ruleset": false, // Inbound rulesets not supported
3544
"peering_connection": false, // IPv4 peering not supported
3645
},
46+
"app": {
47+
"buildpacks": false, // Fir doesn't support traditional buildpacks
48+
"stack": false, // Fir doesn't use traditional stack config
49+
"internal_routing": false, // Fir doesn't support internal routing
50+
"cloud_native_buildpacks": true, // Fir uses CNB exclusively
51+
},
52+
"drain": {
53+
"app_log_drains": false, // Fir apps don't support traditional log drains
54+
},
3755
},
3856
}
3957

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: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,17 @@ type application struct {
4545
Vars map[string]string // Represents all vars on a heroku app.
4646
Buildpacks []string // The application's buildpack names or URLs
4747
IsTeamApp bool // Is the application a team (organization) app
48+
Generation string // The generation of the app platform (cedar/fir)
4849
}
4950

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

5860
Importer: &schema.ResourceImporter{
5961
State: resourceHerokuAppImport,
@@ -77,6 +79,12 @@ func resourceHerokuApp() *schema.Resource {
7779
ForceNew: true,
7880
},
7981

82+
"generation": {
83+
Type: schema.TypeString,
84+
Computed: true,
85+
Description: "Generation of the app platform. Determined by the space the app is deployed to.",
86+
},
87+
8088
"stack": {
8189
Type: schema.TypeString,
8290
Optional: true,
@@ -441,6 +449,9 @@ func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error {
441449
return buildpacksErr
442450
}
443451

452+
// Set generation based on app characteristics
453+
d.Set("generation", app.Generation)
454+
444455
if app.IsTeamApp {
445456
orgErr := setTeamDetails(d, app)
446457
if orgErr != nil {
@@ -630,6 +641,14 @@ func (a *application) Update() error {
630641
a.App.Space = app.Space.Name
631642
}
632643

644+
// Determine generation from app's generation field (available in AppInfo response)
645+
if app.Generation.Name != "" {
646+
a.Generation = app.Generation.Name
647+
} else {
648+
// Default to cedar if generation is not specified
649+
a.Generation = "cedar"
650+
}
651+
633652
// If app is a team/org app, define additional values.
634653
if app.Organization != nil && app.Team != nil {
635654
// Set to true to control additional state actions downstream
@@ -648,9 +667,17 @@ func (a *application) Update() error {
648667

649668
var errs []error
650669
var err error
651-
a.Buildpacks, err = retrieveBuildpacks(a.Id, a.Client)
652-
if err != nil {
653-
errs = append(errs, err)
670+
671+
// Only retrieve buildpacks for apps that support traditional buildpacks
672+
if IsFeatureSupported(a.Generation, "app", "buildpacks") {
673+
a.Buildpacks, err = retrieveBuildpacks(a.Id, a.Client)
674+
if err != nil {
675+
errs = append(errs, err)
676+
}
677+
} else {
678+
// CNB apps don't have traditional buildpacks
679+
log.Printf("[DEBUG] App %s uses generation %s which doesn't support traditional buildpacks", a.Id, a.Generation)
680+
a.Buildpacks = []string{}
654681
}
655682

656683
a.Vars, err = retrieveConfigVars(a.Id, a.Client)
@@ -685,6 +712,16 @@ func retrieveBuildpacks(id string, client *heroku.Service) ([]string, error) {
685712
return buildpacks, nil
686713
}
687714

715+
// isCNBError checks if an error is related to Cloud Native Buildpacks
716+
func isCNBError(err error) bool {
717+
if err == nil {
718+
return false
719+
}
720+
errorMessage := err.Error()
721+
return strings.Contains(errorMessage, "Cloud Native Buildpacks") ||
722+
strings.Contains(errorMessage, "project.toml")
723+
}
724+
688725
func retrieveAcm(id string, client *heroku.Service) (bool, error) {
689726
result, err := client.AppInfo(context.TODO(), id)
690727
if err != nil {
@@ -1034,3 +1071,9 @@ func resourceHerokuAppStateUpgradeV0(ctx context.Context, rawState map[string]in
10341071

10351072
return rawState, nil
10361073
}
1074+
1075+
func resourceHerokuAppCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error {
1076+
// Note: Generation is now computed based on the space, not user-configurable.
1077+
// Validation will happen during the apply phase when we can determine the actual generation.
1078+
return nil
1079+
}

0 commit comments

Comments
 (0)