Skip to content

Commit 74d6424

Browse files
committed
Add Fir generation validation for heroku_build resource
Validates that buildpacks cannot be specified for Fir generation apps, which use Cloud Native Buildpacks configured via project.toml instead. - Add plan-time and apply-time validation for buildpack compatibility - Add comprehensive unit and acceptance tests - Update documentation with generation-specific examples - Add build features to generation feature matrix
1 parent 6bbb2ca commit 74d6424

File tree

3 files changed

+399
-15
lines changed

3 files changed

+399
-15
lines changed

docs/resources/build.md

Lines changed: 129 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ If the build fails, the build log will be output in the error message.
2222

2323
To start the app from a successful build, use a [Formation resource](formation.html) to specify the process, dyno size, and dyno quantity.
2424

25+
## Generation Compatibility
26+
27+
Build configuration varies between Cedar (traditional) and Fir (next-generation) apps:
28+
29+
- **Cedar apps**: Support traditional `buildpacks` configuration for specifying buildpack URLs or names
30+
- **Fir apps**: Use [Cloud Native Buildpacks](https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app) configured via `project.toml` in the source code. The `buildpacks` argument cannot be used with Fir generation apps.
31+
32+
Apps inherit their generation from their space (Fir) or default to Cedar when not in a space.
33+
2534
## Source code layout
2635

2736
The code contained in the source directory or tarball must follow the layout required by the [buildpack](https://devcenter.heroku.com/articles/buildpacks)
@@ -46,6 +55,25 @@ A [`heroku.yml` manifest](https://devcenter.heroku.com/articles/build-docker-ima
4655
file is required to declare which `Dockerfile` to build for each process. Be careful not to create conflicting configuration
4756
between `heroku.yml` and Terraform, such as addons or config vars.
4857

58+
### Building with Cloud Native Buildpacks (Fir Generation)
59+
60+
Fir generation apps use [Cloud Native Buildpacks](https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app) instead of traditional buildpacks. Buildpack configuration must be specified in a `project.toml` file in the source code rather than the Terraform configuration.
61+
62+
The `buildpacks` argument cannot be used with Fir generation apps. Attempting to do so will result in an error during `terraform apply`.
63+
64+
Example `project.toml` for a Node.js app:
65+
66+
```toml
67+
[build]
68+
buildpacks = ["heroku/nodejs"]
69+
70+
[[build.env]]
71+
name = "NODE_ENV"
72+
value = "production"
73+
```
74+
75+
For more information, see [Using Multiple Buildpacks for an App](https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app).
76+
4977
## Source URLs
5078
A `source.url` may point to any `https://` URL that responds to a `GET` with a tarball source code. When running `terraform apply`,
5179
the source code will only be fetched once for a successful build. Change the URL to force a new resource.
@@ -68,14 +96,16 @@ of the Terraform configuration.
6896

6997
### Example Usage with Source URL
7098

99+
#### Cedar Generation (Traditional)
100+
71101
```hcl-terraform
72-
resource "heroku_app" "foobar" {
73-
name = "foobar"
102+
resource "heroku_app" "cedar_app" {
103+
name = "my-cedar-app"
74104
region = "us"
75105
}
76106
77-
resource "heroku_build" "foobar" {
78-
app_id = heroku_app.foobar.id
107+
resource "heroku_build" "cedar_build" {
108+
app_id = heroku_app.cedar_app.id
79109
buildpacks = ["https://github.com/mars/create-react-app-buildpack"]
80110
81111
source {
@@ -85,12 +115,53 @@ resource "heroku_build" "foobar" {
85115
}
86116
}
87117
88-
resource "heroku_formation" "foobar" {
89-
app_id = heroku_app.foobar.id
118+
resource "heroku_formation" "cedar_formation" {
119+
app_id = heroku_app.cedar_app.id
120+
type = "web"
121+
quantity = 1
122+
size = "Standard-1x"
123+
depends_on = ["heroku_build.cedar_build"]
124+
}
125+
```
126+
127+
#### Fir Generation (Cloud Native Buildpacks)
128+
129+
```hcl-terraform
130+
resource "heroku_space" "fir_space" {
131+
name = "my-fir-space"
132+
organization = "my-organization"
133+
region = "virginia"
134+
generation = "fir"
135+
}
136+
137+
resource "heroku_app" "fir_app" {
138+
name = "my-fir-app"
139+
region = heroku_space.fir_space.region
140+
space = heroku_space.fir_space.id
141+
142+
organization {
143+
name = "my-organization"
144+
}
145+
}
146+
147+
resource "heroku_build" "fir_build" {
148+
app_id = heroku_app.fir_app.id
149+
# Note: Do not specify buildpacks for Fir apps
150+
# Buildpacks are configured via project.toml in the source code
151+
152+
source {
153+
# Source must include project.toml for CNB configuration
154+
url = "https://github.com/username/my-cnb-app/archive/v1.0.0.tar.gz"
155+
version = "v1.0.0"
156+
}
157+
}
158+
159+
resource "heroku_formation" "fir_formation" {
160+
app_id = heroku_app.fir_app.id
90161
type = "web"
91162
quantity = 1
92163
size = "Standard-1x"
93-
depends_on = ["heroku_build.foobar"]
164+
depends_on = ["heroku_build.fir_build"]
94165
}
95166
```
96167

@@ -113,28 +184,71 @@ When running `terraform apply`, if the contents (SHA256) of the source path chan
113184

114185
### Example Usage with Local Source Directory
115186

187+
#### Cedar Generation (Traditional)
188+
116189
```hcl-terraform
117-
resource "heroku_app" "foobar" {
118-
name = "foobar"
190+
resource "heroku_app" "cedar_app" {
191+
name = "my-cedar-app"
119192
region = "us"
120193
}
121194
122-
resource "heroku_build" "foobar" {
123-
app_id = heroku_app.foobar.id
195+
resource "heroku_build" "cedar_build" {
196+
app_id = heroku_app.cedar_app.id
197+
buildpacks = ["heroku/nodejs"]
124198
125199
source {
126200
# A local directory, changing its contents will
127201
# force a new build during `terraform apply`
128-
path = "src/example-app"
202+
path = "src/my-cedar-app"
203+
}
204+
}
205+
206+
resource "heroku_formation" "cedar_formation" {
207+
app_id = heroku_app.cedar_app.id
208+
type = "web"
209+
quantity = 1
210+
size = "Standard-1x"
211+
depends_on = ["heroku_build.cedar_build"]
212+
}
213+
```
214+
215+
#### Fir Generation (Cloud Native Buildpacks)
216+
217+
```hcl-terraform
218+
resource "heroku_space" "fir_space" {
219+
name = "my-fir-space"
220+
organization = "my-organization"
221+
region = "virginia"
222+
generation = "fir"
223+
}
224+
225+
resource "heroku_app" "fir_app" {
226+
name = "my-fir-app"
227+
region = heroku_space.fir_space.region
228+
space = heroku_space.fir_space.id
229+
230+
organization {
231+
name = "my-organization"
232+
}
233+
}
234+
235+
resource "heroku_build" "fir_build" {
236+
app_id = heroku_app.fir_app.id
237+
# Note: Do not specify buildpacks for Fir apps
238+
239+
source {
240+
# Local directory must contain project.toml
241+
# for Cloud Native Buildpack configuration
242+
path = "src/my-cnb-app"
129243
}
130244
}
131245
132-
resource "heroku_formation" "foobar" {
133-
app_id = heroku_app.foobar.id
246+
resource "heroku_formation" "fir_formation" {
247+
app_id = heroku_app.fir_app.id
134248
type = "web"
135249
quantity = 1
136250
size = "Standard-1x"
137-
depends_on = ["heroku_build.foobar"]
251+
depends_on = ["heroku_build.fir_build"]
138252
}
139253
```
140254

heroku/resource_heroku_build.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@ func resourceHerokuBuildCreate(d *schema.ResourceData, meta interface{}) error {
178178

179179
appID := getAppId(d)
180180

181+
// Apply-time validation: ensure buildpacks are not specified for Fir apps
182+
if err := validateBuildpacksForApp(client, appID, d); err != nil {
183+
return err
184+
}
185+
181186
// Build up our creation options
182187
opts := heroku.BuildCreateOpts{}
183188

@@ -333,6 +338,11 @@ func resourceHerokuBuildDelete(d *schema.ResourceData, meta interface{}) error {
333338
}
334339

335340
func resourceHerokuBuildCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error {
341+
// Validate buildpack compatibility with app generation during plan phase
342+
if err := validateBuildpacksForAppGeneration(ctx, diff, v); err != nil {
343+
return err
344+
}
345+
336346
// Detect changes to the content of local source archive.
337347
if v, ok := diff.GetOk("source"); ok {
338348
vL := v.([]interface{})
@@ -376,6 +386,67 @@ func resourceHerokuBuildCustomizeDiff(ctx context.Context, diff *schema.Resource
376386
return nil
377387
}
378388

389+
// validateBuildpacksForAppGeneration validates buildpack configuration against the target app's generation
390+
func validateBuildpacksForAppGeneration(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error {
391+
// Only validate if buildpacks are specified
392+
if _, ok := diff.GetOk("buildpacks"); !ok {
393+
return nil // No buildpacks specified, nothing to validate
394+
}
395+
396+
// Get the app ID to check its generation
397+
appID := diff.Get("app_id").(string)
398+
if appID == "" {
399+
// During plan phase, the app_id might not be available yet if the app is being created
400+
// in the same configuration. Skip validation for now - it will be caught at apply time.
401+
return nil
402+
}
403+
404+
config := v.(*Config)
405+
client := config.Api
406+
407+
// Fetch app info to determine its generation
408+
app, err := client.AppInfo(ctx, appID)
409+
if err != nil {
410+
// If we can't fetch the app (it might not exist yet during plan), skip validation
411+
// It will be caught at apply time
412+
return nil
413+
}
414+
415+
// Validate buildpacks against app generation
416+
return validateBuildpacksForGeneration(app.Generation.Name)
417+
}
418+
419+
// validateBuildpacksForApp validates buildpack configuration at apply-time
420+
func validateBuildpacksForApp(client *heroku.Service, appID string, d *schema.ResourceData) error {
421+
// Only validate if buildpacks are specified
422+
if _, ok := d.GetOk("buildpacks"); !ok {
423+
return nil // No buildpacks specified, nothing to validate
424+
}
425+
426+
// Fetch app info to determine its generation
427+
app, err := client.AppInfo(context.TODO(), appID)
428+
if err != nil {
429+
return fmt.Errorf("failed to get app info for build validation: %w", err)
430+
}
431+
432+
// Validate buildpacks against app generation
433+
return validateBuildpacksForGeneration(app.Generation.Name)
434+
}
435+
436+
// validateBuildpacksForGeneration validates buildpacks against a given generation
437+
func validateBuildpacksForGeneration(generationName string) error {
438+
appGeneration := generationName
439+
if appGeneration == "" {
440+
appGeneration = "cedar" // Default to cedar if generation is not specified
441+
}
442+
443+
if !IsFeatureSupported(appGeneration, "app", "buildpacks") {
444+
return fmt.Errorf("buildpacks cannot be specified for %s generation apps. Use project.toml to configure Cloud Native Buildpacks instead. See: https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app", appGeneration)
445+
}
446+
447+
return nil
448+
}
449+
379450
func uploadSource(filePath, httpMethod, httpUrl string) error {
380451
method := strings.ToUpper(httpMethod)
381452
log.Printf("[DEBUG] Uploading source '%s' to %s %s", filePath, method, httpUrl)

0 commit comments

Comments
 (0)