Repository: buildpacks/lifecycle (go)

Issue:
Target distro name/version not derived from `/etc/os-release` when Docker labels aren't set
### Summary

The Platform API 0.12 spec says:

> - If target distribution data is missing, it will be inferred from `/etc/os-release` for Linux images

(https://github.com/buildpacks/spec/blob/platform/0.12/platform.md#analyzedtoml-toml)

As such `lifecycle` has an implementation for this:
https://github.com/buildpacks/lifecycle/blob/feb0d757c9bc16b85f19cb07c80dc28f0bdd8504/platform/target_data.go#L66-L81

However, it seems this implementation never actually gets used in practice since it's behind a `if tm.OS == ""` conditional, and `OS` is always available, so the `/etc/os-release` fallback isn't used even if other properties about the target (such as the distro name and version) are not available:
https://github.com/buildpacks/lifecycle/blob/feb0d757c9bc16b85f19cb07c80dc28f0bdd8504/platform/target_data.go#L15-L19
(Also, why does that code comment say build image - surely it should be analysing the run image?)

This means that for any run images where the `io.buildpacks.base.distro.*` Docker labels haven't been set (which is any run image that hasn't yet made the transition from stacks to targets), the `CNB_TARGET_DISTRO_NAME` and `CNB_TARGET_DISTRO_VERSION` env vars are correctly set in the buildpack environment.

This can be seen by using one of our older run images from before we added the relevant Docker labels - by passing `--run-image heroku/heroku:22-cnb.v122` to `pack build`.

---

### Reproduction

##### Steps

1. `pack buildpack new testcase-targets --api 0.10 --targets "linux/amd64"`
1. `sed -i 's/exit 0/printenv | grep CNB_TARGET | sort/' testcase-targets/bin/build`
1. `pack build --builder heroku/builder:22 --run-image heroku/heroku:22-cnb.v122 --buildpack testcase-targets/ --path testcase-targets/ testcase-targets --verbose`

##### Current behavior

The `CNB_TARGET_DISTRO_NAME` and `CNB_TARGET_DISTRO_VERSION` env vars are not set correctly in the buildpack env:

```
$ pack build --builder heroku/builder:22 --run-image heroku/heroku:22-cnb.v122 --buildpack testcase-targets/ --path testcase-targets/ testcase-targets --verbose
...
===> ANALYZING
...
Run image info in analyzed metadata is: 
{"Reference":"6d5f868e91eba6a6ec22a9a9b9b31a2fd486a0f0f57082fd0864e0bf0b3e4395","Image":"heroku/heroku:22-cnb.v122","Extend":false,"target":{"os":"linux","arch":"amd64"}}
...
===> BUILDING
...
CNB_TARGET_ARCH=amd64
CNB_TARGET_ARCH_VARIANT=
CNB_TARGET_DISTRO_NAME=
CNB_TARGET_DISTRO_VERSION=
CNB_TARGET_OS=linux
...
```

##### Expected behavior

For the `CNB_TARGET_DISTRO_*` env vars to be set based on the contents of `/etc/os-release`, ie:

```
CNB_TARGET_DISTRO_NAME=ubuntu
CNB_TARGET_DISTRO_VERSION=22.04
```

...given that `/etc/os-release` exists and has the necessary `ID` and `VERSION_ID` values:

```
$ docker run --rm heroku/heroku:22-cnb.v122 cat /etc/os-release | rg ID= | sort
ID=ubuntu
VERSION_ID="22.04"
```

---

### Context

##### lifecycle version
`0.19.3`

##### platform version(s)

```
$ pack report
Pack:
  Version:  0.33.2+git-f2cffc4.build-5562
  OS/Arch:  darwin/arm64
```

The build is using Platform API 0.12 and Buildpack API 0.10.

Pull request:
Read `/etc/os-release` file when distro information is not present in labels
<!-- 🎉🎉🎉 Thank you for the PR!!! 🎉🎉🎉 -->

### Summary
<!-- Please describe your changes at a high level. -->

Related issue: https://github.com/buildpacks/lifecycle/issues/1336

#### Release notes
<!-- Please provide 1-2 sentences for release notes. -->
<!-- Example: When using platform API `0.7` or greater, the `creator` logs the expected phase header for the analyze phase -->



---

### Related
<!-- If this PR addresses an issue, please provide the issue number below. -->

Resolves #1336

---

### Context
<!-- Add any other context that may help reviewers (e.g., code that requires special attention, etc.). -->



Patch:
diff --git a/phase/builder.go b/phase/builder.go
index eae12dec..397b0378 100644
--- a/phase/builder.go
+++ b/phase/builder.go
@@ -149,7 +149,7 @@ func (b *Builder) getBuildInputs() buildpack.BuildInputs {
 		LayersDir:      b.LayersDir,
 		PlatformDir:    b.PlatformDir,
 		Env:            env.NewBuildEnv(os.Environ()),
-		TargetEnv:      platform.EnvVarsFor(b.AnalyzeMD.RunImageTarget(), b.Logger),
+		TargetEnv:      platform.EnvVarsFor(&fsutil.Detect{}, b.AnalyzeMD.RunImageTarget(), b.Logger),
 		Out:            b.Out,
 		Err:            b.Err,
 	}
diff --git a/phase/detector.go b/phase/detector.go
index 4324fdc6..b370251f 100644
--- a/phase/detector.go
+++ b/phase/detector.go
@@ -13,6 +13,7 @@ import (
 	"github.com/buildpacks/lifecycle/buildpack"
 	"github.com/buildpacks/lifecycle/env"
 	"github.com/buildpacks/lifecycle/internal/encoding"
+	"github.com/buildpacks/lifecycle/internal/fsutil"
 	"github.com/buildpacks/lifecycle/log"
 	"github.com/buildpacks/lifecycle/platform"
 	"github.com/buildpacks/lifecycle/platform/files"
@@ -239,7 +240,7 @@ func (d *Detector) detectGroup(group buildpack.Group, done []buildpack.GroupElem
 					BuildConfigDir: d.BuildConfigDir,
 					PlatformDir:    d.PlatformDir,
 					Env:            env.NewBuildEnv(os.Environ()),
-					TargetEnv:      platform.EnvVarsFor(d.AnalyzeMD.RunImageTarget(), d.Logger),
+					TargetEnv:      platform.EnvVarsFor(&fsutil.Detect{}, d.AnalyzeMD.RunImageTarget(), d.Logger),
 				}
 				d.Runs.Store(key, d.Executor.Detect(descriptor, inputs, d.Logger)) // this is where we finally invoke bin/detect
 			}
diff --git a/phase/generator.go b/phase/generator.go
index 7f6e5410..8a2106e8 100644
--- a/phase/generator.go
+++ b/phase/generator.go
@@ -151,7 +151,7 @@ func (g *Generator) getGenerateInputs() buildpack.GenerateInputs {
 		BuildConfigDir: g.BuildConfigDir,
 		PlatformDir:    g.PlatformDir,
 		Env:            env.NewBuildEnv(os.Environ()),
-		TargetEnv:      platform.EnvVarsFor(g.AnalyzedMD.RunImageTarget(), g.Logger),
+		TargetEnv:      platform.EnvVarsFor(&fsutil.Detect{}, g.AnalyzedMD.RunImageTarget(), g.Logger),
 		Out:            g.Out,
 		Err:            g.Err,
 	}
diff --git a/platform/target_data.go b/platform/target_data.go
index e88954db..82fba40c 100644
--- a/platform/target_data.go
+++ b/platform/target_data.go
@@ -11,11 +11,12 @@ import (
 
 // EnvVarsFor fulfills the prophecy set forth in https://github.com/buildpacks/rfcs/blob/b8abe33f2bdc58792acf0bd094dc4ce3c8a54dbb/text/0096-remove-stacks-mixins.md?plain=1#L97
 // by returning an array of "VARIABLE=value" strings suitable for inclusion in your environment or complete breakfast.
-func EnvVarsFor(tm files.TargetMetadata, logger log.Logger) []string {
+func EnvVarsFor(d fsutil.Detector, tm files.TargetMetadata, logger log.Logger) []string {
 	// we should always have os & arch,
 	// if they are not populated try to get target information from the build-time base image
-	if tm.OS == "" {
-		GetTargetOSFromFileSystem(&fsutil.Detect{}, &tm, logger)
+	if tm.OS == "" || tm.Distro == nil {
+		logger.Info("target distro name/version labels not found, reading /etc/os-release file")
+		GetTargetOSFromFileSystem(d, &tm, logger)
 	}
 	ret := []string{
 		"CNB_TARGET_OS=" + tm.OS,


Test patch:
diff --git a/phase/builder_test.go b/phase/builder_test.go
index 04507307..e4f4fa1e 100644
--- a/phase/builder_test.go
+++ b/phase/builder_test.go
@@ -191,8 +191,6 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) {
 					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH=amd64")
 					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH_VARIANT=")
 					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_OS=linux")
-					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_NAME=")
-					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_VERSION=")
 					return buildpack.BuildOutputs{}, nil
 				},
 			)
@@ -205,8 +203,6 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) {
 					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH=amd64")
 					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH_VARIANT=")
 					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_OS=linux")
-					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_NAME=")
-					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_VERSION=")
 					return buildpack.BuildOutputs{}, nil
 				})
 
diff --git a/phase/detector_test.go b/phase/detector_test.go
index 6820ef37..1c0e3be0 100644
--- a/phase/detector_test.go
+++ b/phase/detector_test.go
@@ -223,8 +223,6 @@ func testDetector(t *testing.T, when spec.G, it spec.S) {
 					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH=amd64")
 					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH_VARIANT=")
 					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_OS=linux")
-					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_NAME=")
-					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_VERSION=")
 					return buildpack.DetectOutputs{}
 				})
 
diff --git a/phase/generator_test.go b/phase/generator_test.go
index cd6895bf..aefc13e1 100644
--- a/phase/generator_test.go
+++ b/phase/generator_test.go
@@ -284,8 +284,6 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) {
 							h.AssertContains(t, inputs.TargetEnv,
 								"CNB_TARGET_ARCH=amd64",
 								"CNB_TARGET_ARCH_VARIANT=",
-								"CNB_TARGET_DISTRO_NAME=",
-								"CNB_TARGET_DISTRO_VERSION=",
 								"CNB_TARGET_OS=linux",
 							)
 							return buildpack.GenerateOutputs{Dockerfiles: []buildpack.DockerfileInfo{{ExtensionID: d.Extension.ID,
@@ -296,8 +294,6 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) {
 							h.AssertContains(t, inputs.TargetEnv,
 								"CNB_TARGET_ARCH=amd64",
 								"CNB_TARGET_ARCH_VARIANT=",
-								"CNB_TARGET_DISTRO_NAME=",
-								"CNB_TARGET_DISTRO_VERSION=",
 								"CNB_TARGET_OS=linux",
 							)
 							return buildpack.GenerateOutputs{Dockerfiles: []buildpack.DockerfileInfo{{ExtensionID: d.Extension.ID,
diff --git a/platform/run_image_test.go b/platform/run_image_test.go
index 7a14e684..29cc1a54 100644
--- a/platform/run_image_test.go
+++ b/platform/run_image_test.go
@@ -4,6 +4,8 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/apex/log"
+	"github.com/apex/log/handlers/memory"
 	"github.com/google/go-containerregistry/pkg/authn"
 
 	"github.com/buildpacks/lifecycle/platform"
@@ -204,7 +206,12 @@ func testRunImage(t *testing.T, when spec.G, it spec.S) {
 	when(".EnvVarsFor", func() {
 		it("returns the right thing", func() {
 			tm := files.TargetMetadata{Arch: "pentium", ArchVariant: "mmx", ID: "my-id", OS: "linux", Distro: &files.OSDistro{Name: "nix", Version: "22.11"}}
-			observed := platform.EnvVarsFor(tm, nil)
+			d := &mockDetector{
+				contents: "this is just test contents really",
+				t:        t,
+				HasFile:  false,
+			}
+			observed := platform.EnvVarsFor(d, tm, &log.Logger{Handler: memory.New()})
 			h.AssertContains(t, observed, "CNB_TARGET_ARCH="+tm.Arch)
 			h.AssertContains(t, observed, "CNB_TARGET_ARCH_VARIANT="+tm.ArchVariant)
 			h.AssertContains(t, observed, "CNB_TARGET_DISTRO_NAME="+tm.Distro.Name)
@@ -213,9 +220,30 @@ func testRunImage(t *testing.T, when spec.G, it spec.S) {
 			h.AssertEq(t, len(observed), 5)
 		})
 
+		it("returns the right thing from /etc/os-release", func() {
+			d := &mockDetector{
+				contents: "this is just test contents really",
+				t:        t,
+				HasFile:  true,
+			}
+			tm := files.TargetMetadata{Arch: "pentium", ArchVariant: "mmx", ID: "my-id", OS: "linux", Distro: nil}
+			observed := platform.EnvVarsFor(d, tm, &log.Logger{Handler: memory.New()})
+			h.AssertContains(t, observed, "CNB_TARGET_ARCH="+tm.Arch)
+			h.AssertContains(t, observed, "CNB_TARGET_ARCH_VARIANT="+tm.ArchVariant)
+			h.AssertContains(t, observed, "CNB_TARGET_DISTRO_NAME=opensesame")
+			h.AssertContains(t, observed, "CNB_TARGET_DISTRO_VERSION=3.14")
+			h.AssertContains(t, observed, "CNB_TARGET_OS="+tm.OS)
+			h.AssertEq(t, len(observed), 5)
+		})
+
 		it("does not return the wrong thing", func() {
 			tm := files.TargetMetadata{Arch: "pentium", OS: "linux"}
-			observed := platform.EnvVarsFor(tm, nil)
+			d := &mockDetector{
+				contents: "this is just test contents really",
+				t:        t,
+				HasFile:  false,
+			}
+			observed := platform.EnvVarsFor(d, tm, &log.Logger{Handler: memory.New()})
 			h.AssertContains(t, observed, "CNB_TARGET_ARCH="+tm.Arch)
 			h.AssertContains(t, observed, "CNB_TARGET_OS="+tm.OS)
 			// note: per the spec only the ID field is optional, so I guess the others should always be set: https://github.com/buildpacks/rfcs/blob/main/text/0096-remove-stacks-mixins.md#runtime-metadata