Compare commits

..

85 Commits

Author SHA1 Message Date
Buildpacks Robot f521f92a09
Bump pipeline from 1.33.0 to 1.37.5 (#288)
* Bump pipeline from 1.33.0 to 1.37.5

Bumps pipeline from 1.33.0 to 1.37.5.

Signed-off-by: GitHub <noreply@github.com>

* Apply suggestions from code review

Signed-off-by: Daniel Mikusa <dan@mikusa.com>

---------

Signed-off-by: GitHub <noreply@github.com>
Signed-off-by: Daniel Mikusa <dan@mikusa.com>
Co-authored-by: buildpack-bot <buildpack-bot@users.noreply.github.com>
Co-authored-by: Daniel Mikusa <dan@mikusa.com>
2024-04-24 22:51:23 -04:00
Buildpacks Robot e20ec30ee2
Bump Go Modules (#287)
Bumps Go modules used by the project. See the commit for details on what modules were updated.

Signed-off-by: GitHub <noreply@github.com>
Co-authored-by: buildpack-bot <buildpack-bot@users.noreply.github.com>
2024-04-24 22:47:07 -04:00
Ralf Pannemans 18b4f48b23
Do not enforce presence of CNB_STACK_ID (#284)
Signed-off-by: Johannes Dillmann <j.dillmann@sap.com>
Co-authored-by: Johannes Dillmann <j.dillmann@sap.com>
2024-04-19 08:48:41 -04:00
Philipp Stehle 7f587a123f
align toml tag of `working-dir` with spec (#268)
See https://github.com/buildpacks/spec/blob/buildpack/v0.10/buildpack.md#launchtoml-toml

Signed-off-by: Ralf Pannemans <ralf.pannemans@sap.com>
Co-authored-by: Ralf Pannemans <ralf.pannemans@sap.com>
2023-11-28 11:53:48 -05:00
Buildpacks Robot 77105e2978
Bump Go Modules (#265)
Bumps Go modules used by the project. See the commit for details on what modules were updated.

Signed-off-by: GitHub <noreply@github.com>
Co-authored-by: buildpack-bot <buildpack-bot@users.noreply.github.com>
2023-10-22 23:13:19 -04:00
Ralf Pannemans d17194071c
Convert nested binding credentials to JSON (#264)
Convert nested binding credentials to JSON (#256)

Signed-off-by: Johannes Dillmann <j.dillmann@sap.com>
Signed-off-by: Pavel Busko <pavel.busko@sap.com>
Signed-off-by: Ralf Pannemans <ralf.pannemans@sap.com>
Co-authored-by: Johannes Dillmann <modulo11@users.noreply.github.com>
Co-authored-by: Pavel Busko <pavel.busko@sap.com>
Co-authored-by: Johannes Dillmann <j.dillmann@sap.com>
2023-10-22 22:21:05 -04:00
Aidan Delaney 9021dcf494
Add a libcnb tutorial (#242) 2023-10-04 08:43:03 -04:00
Buildpacks Robot 51c0f7c9d8
Bump pipeline from 1.32.0 to 1.33.0 (#246)
* Bump pipeline from 1.32.0 to 1.33.0

Bumps pipeline from 1.32.0 to 1.33.0.

Signed-off-by: GitHub <noreply@github.com>

* Update .github/workflows/pb-synchronize-labels.yml

Signed-off-by: Daniel Mikusa <dan@mikusa.com>

* Update .github/workflows/pb-tests.yml

Signed-off-by: Daniel Mikusa <dan@mikusa.com>

* Update .github/workflows/pb-update-draft-release.yml

Signed-off-by: Daniel Mikusa <dan@mikusa.com>

* Update .github/workflows/pb-update-pipeline.yml

Signed-off-by: Daniel Mikusa <dan@mikusa.com>

---------

Signed-off-by: GitHub <noreply@github.com>
Signed-off-by: Daniel Mikusa <dan@mikusa.com>
Co-authored-by: buildpack-bot <buildpack-bot@users.noreply.github.com>
Co-authored-by: Daniel Mikusa <dan@mikusa.com>
2023-07-07 14:57:14 -04:00
Buildpacks Robot 053fa626f2
Bump Go Modules (#245)
Bumps Go modules used by the project. See the commit for details on what modules were updated.

Signed-off-by: GitHub <noreply@github.com>
Co-authored-by: buildpack-bot <buildpack-bot@users.noreply.github.com>
2023-07-07 14:54:21 -04:00
Pavel Busko e9faf69a99
Distinguish provider and type when reading binding from VCAP_SERVICES (1.x BACKPORT) (#244)
Distinguish provider and type when reading binding from VCAP_SERVICES (#235)

Signed-off-by: Ralf Pannemans <ralf.pannemans@sap.com>
Co-authored-by: Ralf Pannemans <ralf.pannemans@sap.com>
2023-07-07 14:47:14 -04:00
Ralf Pannemans 60df949646
Read bindings from VCAP_SERVICES (#228)
Read bindings from VCAP_SERVICES

Signed-off-by: Ralf Pannemans <ralf.pannemans@sap.com>
2023-05-04 22:12:05 -04:00
Daniel Mikusa 8b7f79763b
Update pipeline-builder (#226)
Signed-off-by: Daniel Mikusa <dan@mikusa.com>
2023-04-12 23:06:47 +01:00
Daniel Mikusa 09cee453ad
Update main dependencies
Signed-off-by: Daniel Mikusa <dan@mikusa.com>
2023-04-12 09:22:32 -04:00
Daniel Mikusa 0f3d43690e
Remove usage of ioutil/*
Signed-off-by: Daniel Mikusa <dan@mikusa.com>
2023-04-12 09:19:56 -04:00
Daniel Mikusa fb77fe5013
Regenerate mocks with latest version of `mockery`
Signed-off-by: Daniel Mikusa <dan@mikusa.com>
2023-04-12 09:00:51 -04:00
Daniel Mikusa 539663a25f
Fix linter errors
Signed-off-by: Daniel Mikusa <dan@mikusa.com>
2023-04-12 09:00:20 -04:00
Daniel Mikusa 8660a07c64
Update lint tools & linter definitions
Signed-off-by: Daniel Mikusa <dan@mikusa.com>
2023-04-12 08:59:36 -04:00
Daniel Mikusa 25ba7f615a
Update pipelines (#168)
- Updates to most recent pipeline-builder version
- Updates pipelines to use Go 1.18
- Updates pipelines to use 'go install' instead of 'go get'
- Adds a pipeline to check for and update Go versions & to check for transitive go.mod updates, which dependabot does not do

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
Co-authored-by: Sambhav Kothari <skothari44@bloomberg.net>
2022-08-19 17:28:46 +00:00
Daniel Mikusa 6305526cf8
Use curl to install richgo (#167)
This PR changes the workflow to use 'curl' to fetch and install richgo instead of 'go get'. We could switch to 'go install', but 'curl' is faster than fetching & building.

This is what the pipeline-builder project has been doing to install richgo for quite a while now.

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
2022-08-11 08:51:16 +01:00
Daniel Mikusa 2e5389a593
Ensure directories are created when process-specific env variables are used (#159)
* Ensure directories are created when process-specific env variables are used
* Add comment to explain the need for extra calls to makedirs

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>

Co-authored-by: Sambhav Kothari <skothari44@bloomberg.net>
2022-07-14 21:45:18 -04:00
Daniel Mikusa 587c59c91d
Bump go module dependencies (#161)
I do not believe that Dependabot is running against this branch, so we'll need to manually update them occasionally.

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
2022-07-14 21:44:07 +01:00
Buildpacks Robot 5f657b5cfc
Bump pipeline from 1.21.1 to 1.21.2 (#138)
Bumps pipeline from 1.21.1 to 1.21.2.

Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: buildpack-bot <buildpack-bot@users.noreply.github.com>
2022-04-20 07:18:12 +02:00
Buildpacks Robot 6e3b073b7b
Bump pipeline from 1.20.0 to 1.21.1 (#137)
Co-authored-by: buildpack-bot <buildpack-bot@users.noreply.github.com>
2022-04-12 10:23:26 +01:00
Sambhav Kothari 52e0183e1d
Add support for buildpack api 0.8 (#131) 2022-04-11 16:13:40 +01:00
dependabot[bot] 2f1a4584c6
Bump github.com/BurntSushi/toml from 1.0.0 to 1.1.0 (#136)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-09 12:44:21 +01:00
Gabe Cemaj 963d4bea64
Use semver to parse and check BP API versions (#134)
Signed-off-by: gcemaj <gcemaj@bloomberg.net>

Co-authored-by: gcemaj <gcemaj@bloomberg.net>
2022-04-07 21:53:45 +01:00
Sambhav Kothari 71c0efab66
Merge pull request #133 from gcemaj/gcemaj-launch-working-dir
Add support for working-directory in launch.toml (API>=0.8)
2022-04-05 22:11:45 +01:00
Sambhav Kothari 3d9ff459f6
Merge branch 'main' into gcemaj-launch-working-dir 2022-04-05 21:22:38 +01:00
Sambhav Kothari abe34281b4
Merge pull request #125 from buildpacks/dependabot/go_modules/github.com/stretchr/testify-1.7.1
Bump github.com/stretchr/testify from 1.7.0 to 1.7.1
2022-04-05 21:20:00 +01:00
Sambhav Kothari 8b98521c54
Merge branch 'main' into dependabot/go_modules/github.com/stretchr/testify-1.7.1 2022-04-05 21:16:53 +01:00
Sambhav Kothari e4b66fbc45
Merge pull request #135 from buildpacks/dependabot/go_modules/github.com/BurntSushi/toml-1.1.0
Bump github.com/BurntSushi/toml from 1.0.0 to 1.1.0
2022-04-05 21:09:25 +01:00
gcemaj 5c5fb9c447 [action] add working-directory to launch.toml (API>=0.8)
Signed-off-by: gcemaj <gcemaj@bloomberg.net>
2022-04-05 15:08:21 -04:00
dependabot[bot] 69cca5e154
Bump github.com/BurntSushi/toml from 1.0.0 to 1.1.0
Bumps [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/BurntSushi/toml/releases)
- [Commits](https://github.com/BurntSushi/toml/compare/v1.0.0...v1.1.0)

---
updated-dependencies:
- dependency-name: github.com/BurntSushi/toml
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-05 19:03:25 +00:00
Sambhav Kothari a9e6d2093f
Merge branch 'main' into dependabot/go_modules/github.com/stretchr/testify-1.7.1 2022-04-02 02:00:39 +01:00
Sambhav Kothari 31fc6e39de
Merge pull request #129 from buildpacks/dependabot/go_modules/github.com/onsi/gomega-1.19.0
Bump github.com/onsi/gomega from 1.18.1 to 1.19.0
2022-04-02 02:00:21 +01:00
dependabot[bot] 8ebd909600
Bump github.com/onsi/gomega from 1.18.1 to 1.19.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.18.1 to 1.19.0.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.18.1...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-28 19:05:28 +00:00
dependabot[bot] fdc1649156
Bump github.com/stretchr/testify from 1.7.0 to 1.7.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 19:05:09 +00:00
Sambhav Kothari e2931f6866
Merge pull request #123 from buildpacks/update/pipeline
Bump pipeline from 1.19.0 to 1.20.0
2022-03-11 05:59:03 +00:00
buildpack-bot 007e6d2e3e Bump pipeline from 1.19.0 to 1.20.0
Bumps pipeline from 1.19.0 to 1.20.0.

Signed-off-by: GitHub <noreply@github.com>
2022-03-11 05:10:39 +00:00
Sambhav Kothari 0df8dedcfe
Merge pull request #118 from dmikusa-pivotal/deps-update 2022-03-03 19:03:31 +00:00
Daniel Mikusa 7ed3dc927c
Updates to go 1.17
Updates go.mod to use go 1.17 to unlock features in newer versions of Go.

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
2022-03-03 12:34:13 -05:00
Emily Casey bc167dd856
Merge pull request #117 from buildpacks/update/pipeline
Bump pipeline from 1.18.0 to 1.19.0
2022-02-25 09:50:17 -05:00
buildpack-bot 714a5dc525 Bump pipeline from 1.18.0 to 1.19.0
Bumps pipeline from 1.18.0 to 1.19.0.

Signed-off-by: GitHub <noreply@github.com>
2022-02-15 05:10:59 +00:00
Sambhav Kothari fdc252a082
Merge pull request #116 from buildpacks/update/pipeline
Bump pipeline from 1.17.0 to 1.18.0
2022-02-10 06:10:28 +00:00
buildpack-bot 62037e0b7a Bump pipeline from 1.17.0 to 1.18.0
Bumps pipeline from 1.17.0 to 1.18.0.

Signed-off-by: GitHub <noreply@github.com>
2022-02-10 05:10:51 +00:00
Sambhav Kothari 6a941db0cd
Merge pull request #115 from dmikusa-pivotal/lifecycle_0_13_3 2022-02-02 19:35:08 +00:00
Daniel Mikusa f496f6e88d
Adds WithBOMLabel Config Option to control writing BOM Labels
In some environments and with some applications, a long enough BOM label may be generated that it will either break Kubernetes or cause your application to fail to start. With the changes in lifecycle 0.13.3, we are enabling the BOM label again, but due to the issue above we also need a way for users to disable it if there are problems.

When running `libcnb.Build` you may now include an `Option` of `WithBOMLabel` that has an argument of either true or false. If not set, it defaults to false. This controls the output of libcnb.BOMEntry items in `launch.toml` and `build.toml`. If true, BOM entries are written. If false, BOM entries are not written, even if they are returned by the buildpack.

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
2022-02-01 14:08:07 -05:00
Daniel Mikusa 86ede05ba0
Permit both BOM formats at the same time
Starting with lifecycle 0.13.3, it is permitted to have both the old style label-based BOM information and the new style layer-based BOM information. If the buildpack API is 0.6 or older, label-based BOMs only is OK. If the buildpack API is 0.7, you may have both label-based BOM and layer-based BOM or just layer-based BOM. It is permitted to have just label-based BOM, however, that will generate a warning from the lifecycle.

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
2022-01-31 09:50:30 -05:00
Sambhav Kothari d3e6e18255
Merge pull request #114 from buildpacks/dependabot/go_modules/github.com/onsi/gomega-1.18.1
Bump github.com/onsi/gomega from 1.18.0 to 1.18.1
2022-01-28 21:35:41 +00:00
dependabot[bot] a0a8708c7a
Bump github.com/onsi/gomega from 1.18.0 to 1.18.1
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.18.0...v1.18.1)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-28 19:02:39 +00:00
Sambhav Kothari 53c63e4082
Merge pull request #112 from buildpacks/dependabot/go_modules/github.com/onsi/gomega-1.18.0
Bump github.com/onsi/gomega from 1.17.0 to 1.18.0
2022-01-24 23:00:38 +00:00
dependabot[bot] df6fd7ca71
Bump github.com/onsi/gomega from 1.17.0 to 1.18.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-24 19:06:33 +00:00
Sambhav Kothari d91d62a519
Merge pull request #111 from buildpacks/update/pipeline
Bump pipeline from 1.16.0 to 1.17.0
2022-01-24 07:42:44 +00:00
buildpack-bot 4d89af3641 Bump pipeline from 1.16.0 to 1.17.0
Bumps pipeline from 1.16.0 to 1.17.0.

Signed-off-by: GitHub <noreply@github.com>
2022-01-24 05:10:12 +00:00
Sambhav Kothari f4abcd0215
Merge pull request #110 from buildpacks/dependabot/go_modules/github.com/BurntSushi/toml-1.0.0 2022-01-12 19:08:04 +00:00
dependabot[bot] f04e4e2cf2
Bump github.com/BurntSushi/toml from 0.4.1 to 1.0.0
Bumps [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) from 0.4.1 to 1.0.0.
- [Release notes](https://github.com/BurntSushi/toml/releases)
- [Commits](https://github.com/BurntSushi/toml/compare/v0.4.1...v1.0.0)

---
updated-dependencies:
- dependency-name: github.com/BurntSushi/toml
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-12 19:03:14 +00:00
Sambhav Kothari 49c96bbc6c
Merge pull request #109 from buildpacks/samj1912-patch-1 2022-01-11 18:40:07 +00:00
Sambhav Kothari 2d891dd194
Only validate for API versions 0.5 and 0.6
libcnb is only supported for versions 0.5+ and we check for it a few lines above. Removing some unnecessary checks

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>
2022-01-11 18:20:04 +00:00
Sambhav Kothari 896a63b4e3
Merge pull request #108 from dmikusa-pivotal/gh_issue_107 2022-01-11 18:15:26 +00:00
Daniel Mikusa 45c858b545
Validation should only occur under API 0.7
- Adds API checks before validating SBOM format, should only happen with API 0.7+
- Adds a test to confirm validation does not run if the API is less than 0.7.

Resolves #107

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
2022-01-10 16:36:14 -05:00
Sambhav Kothari c9ad73fb07
Merge pull request #106 from buildpacks/binding
Use appropriate bindings path for detect
2022-01-06 19:58:38 +00:00
Sambhav Kothari f869c1d937
Use appropriate bindings path for detect
Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>
2022-01-05 22:21:53 +00:00
Sambhav Kothari 4a789fd9d2
Merge pull request #100 from buildpacks/update/pipeline
Bump pipeline from 1.15.1 to 1.16.0
2021-12-06 09:21:08 +00:00
buildpack-bot d67b3ffea7 Bump pipeline from 1.15.1 to 1.16.0
Bumps pipeline from 1.15.1 to 1.16.0.

Signed-off-by: GitHub <noreply@github.com>
2021-12-06 05:11:25 +00:00
Sambhav Kothari 128cdb9dbe
Merge pull request #99 from dmikusa-pivotal/sbom-bug
Fixes bug with Warning message regarding SBOM format
2021-11-29 23:10:52 +00:00
Daniel Mikusa 77cb45d022
Fixes bug with Warning message regarding SBOM format
In the current implementation, it is possible for the `Warning: this buildpack is including both old and new format SBOM...` message to be triggered incorrectly. The warning is displayed if the launch or build object are not empty & if the API is `0.7`. This isn't right though. If you have no SBOM entries, but you have process types or labels, then it would be not empty & you'd see this message incorrectly.

This PR adjust the criteria such that you'll see this warning message if launch or build are not empty, if API is `0.7` and if BOM entries is not empty.

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
2021-11-29 16:35:16 -05:00
Sambhav Kothari a33b3677ad
Merge pull request #98 from buildpacks/polish-pr-97
Polish PR: Additional updates for SBOM Support
2021-11-24 08:34:09 +00:00
Sambhav Kothari 6d86013d6d Make validate SBOM private
Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>
2021-11-23 18:52:42 +00:00
Daniel Mikusa 66f3e9f328
Additional updates for SBOM Support
- Adds SBOMFormats (maps to `sbom-formats` in buildpack.toml) to the BuildpackInfo struct. This makes the information accessible to buildpacks.
- Modifies build such that it only writes the old-style build and launch BOM information if the buildpack API is less than 0.7. If it's 0.7+, it should not write the old style format as that can conflict with the new SBOM format and cause the lifecycle to fail. Omits a warning message if this occurs.
- Adds validation of the new SBOM files that are written by a buildpack. We check that the extension matches up with a valid MIME type that is listed in buildpack.toml's `sbom-formats` field. If it does not match up, then it fails. This should not generally happen with published buildpacks. This check can be helpful while authoring buildpacks, to ensure everything is correctly setup.

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
Co-authored-by: Sambhav Kothari <sambhavs.email@gmail.com>
Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
2021-11-20 21:35:28 -05:00
Sambhav Kothari 6aa81e5081
Merge pull request #95 from dmikusa-pivotal/bom-rfc-95
Adds a convenience method for getting the build, launch and layer BOM file paths
2021-11-18 03:15:25 +00:00
Daniel Mikusa c06fd640c3
Adds a convenience method for getting the build, launch and layer BOM file paths
This PR includes:

- convenience methods on the Layers and Layer object for fetching the SBoM path
- deprecated messages if using old-style BOM functionality

Co-authored-by: Sambhav Kothari <sambhavs.email@gmail.com>
Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
2021-11-17 20:44:18 -05:00
Sambhav Kothari 50e1696404
Merge pull request #92 from buildpacks/update/pipeline
Bump pipeline from 1.13.0 to 1.15.1
2021-11-12 09:18:45 +00:00
buildpack-bot 9cb9a2095d Bump pipeline from 1.13.0 to 1.15.1
Bumps pipeline from 1.13.0 to 1.15.1.

Signed-off-by: GitHub <noreply@github.com>
2021-11-12 05:10:26 +00:00
Sambhav Kothari 0376b8a285
Merge pull request #94 from dmikusa-pivotal/api-07 2021-11-11 21:14:11 +00:00
Daniel Mikusa 9af239f94c
Updates libcnb to support buildpacks API 0.7
Buildpacks API 0.7 brings one new feature, the functionality for SBoM output through [RFC #95](https://github.com/buildpacks/rfcs/blob/main/text/0095-sbom.md).

Without this change, you can write the SBoM information as described in RFC #95, but the lifecycle will ignore it. To make the lifecycle capture your SBoM information you need to:
1. Use a version of libcnb with this PR.
2. Update the `api = "0.8"` line in your buildpack.toml.
3. Write the SBoM files from your buildpack according to the locations in RFC #95. Libcnb does not provide any help with this activity presently, it is up to the buildpack author.
4. Use a lifecycle version with support, 0.13.0+
5. Use a pack version with platform API 0.8+

The lifecycle should then copy your SBoM files and include them into the image.

This PR is only required because the current implementation restricts usage of libcnb to specific buildpack API versions and we needed to add 0.7 to this list.

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
2021-11-11 13:02:54 -05:00
Sambhav Kothari 70403787d2
Merge pull request #91 from buildpacks/update/pipeline
Bump pipeline from 1.12.1 to 1.13.0
2021-11-09 05:54:29 +00:00
buildpack-bot ced0ee6ed8 Bump pipeline from 1.12.1 to 1.13.0
Bumps pipeline from 1.12.1 to 1.13.0.

Signed-off-by: GitHub <noreply@github.com>
2021-11-09 05:10:07 +00:00
Sambhav Kothari c564678891
Merge pull request #90 from buildpacks/dependabot/go_modules/github.com/onsi/gomega-1.17.0
Bump github.com/onsi/gomega from 1.16.0 to 1.17.0
2021-11-08 23:16:52 +00:00
dependabot[bot] 5c9b56b7c6
Bump github.com/onsi/gomega from 1.16.0 to 1.17.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.16.0...v1.17.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-08 19:04:45 +00:00
Sambhav Kothari 8c10662dc7
Merge pull request #86 from jghiloni/issue-85
Add struct tag to Buildpack.Path field
2021-11-04 22:15:53 +00:00
Josh Ghiloni b2b6b4b4c1
Make substring match more explicit in buildpack toml test
Signed-off-by: Josh Ghiloni <jghiloni@vmware.com>
2021-11-04 13:33:50 -04:00
Josh Ghiloni 33e2828ac7
Omit Path field from serialization altogether
Signed-off-by: Josh Ghiloni <jghiloni@vmware.com>
2021-11-04 11:20:26 -04:00
Josh Ghiloni a965403652
Add struct tag to Buildpack.Path field
Most Go applications use github.com/burntsushi/toml as their toml
parsing library. When struct tags are omitted, the library will do
case-insensitive deserialization, so a property called "path" will
successfully unmarshal into the Path struct field. However, if you
marshal a Buildpack instance to toml, the "Path = " field appears
regardless of whether it is set or not, and technically violates the
buildpack toml spec. This PR adds a struct tag to not only marshal the
field to the "path" property in the resultant toml, but also adds the
"omitempty" modifier to ignore the field altogether when not set.

Signed-off-by: Josh Ghiloni <jghiloni@vmware.com>
2021-11-04 10:21:07 -04:00
Sambhav Kothari bc6e6ca9a4
Merge pull request #84 from buildpacks/update/pipeline
Bump pipeline from 1.12.0 to 1.12.1
2021-11-04 07:08:48 +00:00
buildpack-bot 536a77887b Bump pipeline from 1.12.0 to 1.12.1
Bumps pipeline from 1.12.0 to 1.12.1.

Signed-off-by: GitHub <noreply@github.com>
2021-11-04 05:09:37 +00:00
78 changed files with 3977 additions and 3466 deletions

View File

@ -1 +1 @@
1.42.0 1.37.5

View File

@ -2,7 +2,7 @@ name: Synchronize Labels
"on": "on":
push: push:
branches: branches:
- main - release-1.x
paths: paths:
- .github/labels.yml - .github/labels.yml
jobs: jobs:

View File

@ -8,7 +8,7 @@ name: Tests
pull_request: {} pull_request: {}
push: push:
branches: branches:
- main - release-1.x
jobs: jobs:
unit: unit:
name: Unit Test name: Unit Test
@ -23,7 +23,7 @@ jobs:
restore-keys: ${{ runner.os }}-go- restore-keys: ${{ runner.os }}-go-
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: "1.24" go-version: "1.20"
- name: Install richgo - name: Install richgo
run: | run: |
#!/usr/bin/env bash #!/usr/bin/env bash

View File

@ -2,7 +2,7 @@ name: Update Draft Release
"on": "on":
push: push:
branches: branches:
- main - release-1.x
jobs: jobs:
update: update:
name: Update Draft Release name: Update Draft Release

View File

@ -11,7 +11,7 @@ jobs:
steps: steps:
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: "1.24" go-version: "1.20"
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Update Go Version & Modules - name: Update Go Version & Modules
id: update-go id: update-go
@ -25,7 +25,7 @@ jobs:
exit 1 exit 1
fi fi
OLD_GO_VERSION=$(grep -P '^go \d\.\d+' go.mod | cut -d ' ' -f 2 | cut -d '.' -f 1-2) OLD_GO_VERSION=$(grep -P '^go \d\.\d+' go.mod | cut -d ' ' -f 2)
go mod edit -go="$GO_VERSION" go mod edit -go="$GO_VERSION"
go mod tidy go mod tidy
@ -49,7 +49,7 @@ jobs:
echo "commit-body=${COMMIT_BODY}" >> "$GITHUB_OUTPUT" echo "commit-body=${COMMIT_BODY}" >> "$GITHUB_OUTPUT"
echo "commit-semver=${COMMIT_SEMVER}" >> "$GITHUB_OUTPUT" echo "commit-semver=${COMMIT_SEMVER}" >> "$GITHUB_OUTPUT"
env: env:
GO_VERSION: "1.24" GO_VERSION: "1.20"
- uses: peter-evans/create-pull-request@v6 - uses: peter-evans/create-pull-request@v6
with: with:
author: ${{ secrets.IMPLEMENTATION_GITHUB_USERNAME }} <${{ secrets.IMPLEMENTATION_GITHUB_USERNAME }}@users.noreply.github.com> author: ${{ secrets.IMPLEMENTATION_GITHUB_USERNAME }} <${{ secrets.IMPLEMENTATION_GITHUB_USERNAME }}@users.noreply.github.com>

View File

@ -2,7 +2,7 @@ name: Update Pipeline
"on": "on":
push: push:
branches: branches:
- main - release-1.x
paths: paths:
- .github/pipeline-descriptor.yml - .github/pipeline-descriptor.yml
schedule: schedule:
@ -16,7 +16,7 @@ jobs:
steps: steps:
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: "1.24" go-version: "1.20"
- name: Install octo - name: Install octo
run: | run: |
#!/usr/bin/env bash #!/usr/bin/env bash

7
.gitignore vendored
View File

@ -11,10 +11,3 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
bin/
linux/
dependencies/
package/
scratch/

View File

@ -1,45 +0,0 @@
# Migration Guide
This guide highlights the major differences between libcnb v1 and v2 with a focus on what you as an author need to change to upgrade your buildpacks from v1 to v2.
## Buildpack API Support
With libcnb v1, you get support for buildpack API 0.5 to 0.8. With v2, you get support for 0.8 to 0.10. This should provide a seamless transition as you can continue to use buildpack API 0.8 with
## Removal of LayerContributor
The LayerContributor has been removed. Previously, libcnb would take a list of LayerContributors and execute them to retrieve the list of Layers to process. Now it just directly takes the list of Layers to process.
The path forward is to either update your buildpacks to return layers or to implement your own LayerContributor interface as a means to ease the transition to libcnb v2.
## Replace Builder and Detector with Functions
The Builder and Detector interfaces have been removed and replaced with functions, specifically BuildFunc and DetectFunc. They serve the same purpose, but simplify your implementation because you do not need to implement the single method interface, you can only need pass in a function that will be called back.
The path forward is to remove your Builder and Detector structs and pass the method directly into libcnb.
## Rename `poet` to `log`
We have renamed the `poet` module to be called `log`. This is a simple find & replace change in your code. We have also simplified the method names, so instead of needing to say `poet.NewLogger`, you can say `log.New` or `log.NewWithOptions`.
We have also introduced a new `Logger` interface which can be overridden to control how libcnb logs. A basic implementation has been provided, called `PlainLogger`. If you want to supply a custom implementation, it can be passed in through main/build/detect functions.
Lastly, libcnb has been modified such that it will only log at the `Debug` level. Anything problems that arise will instead return an error, which you as the buildpack author should handle.
## Remove Deprecated Pre-Buildpack API 0.7 BOM
The pre-buildpack API 0.7 BOM API was marked for deprecation in libcnb v1. It has been removed in v2. If you are still using it, you will need to migrate to use the new SBOM functionality provided by the [Buildpacks API](https://github.com/buildpacks/rfcs/blob/main/text/0095-sbom.md).
## Path to Source Code Changed
In libcnb v1, `BuildContext.Application.Path` points to the application source code. This was shortened to `BuildContext.ApplicationPath`. The same change was made for `DetectContext.ApplicationPath`.
## Remove Deprecated CNB Binding Support
The CNB Binding specification has long been replaced by the Service Binding Specification for Kubernetes. Support for the CNB Binding Support had remained, but it is removed in v2. This is unlikely to impact anyone.
## Remove Shell-Specific Logic & Overridable Process Arguments
To comply with [RFC #168](https://github.com/buildpacks/rfcs/pull/168), we remove the `Direct` field from the `Process` struct. We also change `Command` from a `string` to `string[]` on the `Process` struct, to support overridable process arguments. Both of these require at leats API 0.9.
In conjunction with this, we have also removed Profile & `profile.d` support. These should be replaced with the [Profile Buildpack](https://github.com/buildpacks/profile) and `exec.d`.

View File

@ -1,7 +1,7 @@
# Go parameters # Go parameters
GOCMD?=go GOCMD?=go
GO_VERSION=$(shell go list -m -f "{{.GoVersion}}") GO_VERSION=$(shell go list -m -f "{{.GoVersion}}")
PACKAGE_BASE=github.com/buildpacks/libcnb/v2 PACKAGE_BASE=github.com/buildpacks/libcnb
all: test all: test

View File

@ -14,16 +14,14 @@
go get github.com/buildpacks/libcnb go get github.com/buildpacks/libcnb
``` ```
or for the v2 alpha
```
go get github.com/buildpacks/libcnb@v2.0.0-alpha.1
```
#### Docs #### Docs
https://pkg.go.dev/github.com/buildpacks/libcnb?tab=doc https://pkg.go.dev/github.com/buildpacks/libcnb?tab=doc
#### Tutorial
[docs/tutorial.md](docs/tutorial.md)
## License ## License
This library is released under version 2.0 of the [Apache License][a]. This library is released under version 2.0 of the [Apache License][a].

128
application.go Normal file
View File

@ -0,0 +1,128 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb
// Application is the user contributed application to build.
type Application struct {
// Path is the path to the application.
Path string
}
// Label represents an image label.
type Label struct {
// Key is the key of the label.
Key string `toml:"key"`
// Value is the value of the label.
Value string `toml:"value"`
}
// Process represents metadata about a type of command that can be run.
type Process struct {
// Type is the type of the process.
Type string `toml:"type"`
// Command is the command of the process.
Command string `toml:"command"`
// Arguments are arguments to the command.
Arguments []string `toml:"args"`
// Command is exec'd directly by the os (no profile.d scripts run)
Direct bool `toml:"direct,omitempty"`
// WorkingDirectory is a directory to execute the command in, removes the need to use a shell environment to CD into working directory
WorkingDirectory string `toml:"working-dir,omitempty"`
// Default can be set to true to indicate that the process
// type being defined should be the default process type for the app image.
Default bool `toml:"default,omitempty"`
}
// Slice represents metadata about a slice.
type Slice struct {
// Paths are the contents of the slice.
Paths []string `toml:"paths"`
}
// LaunchTOML represents the contents of launch.toml.
type LaunchTOML struct {
// Labels is the collection of image labels contributed by the buildpack.
Labels []Label `toml:"labels"`
// Processes is the collection of process types contributed by the buildpack.
Processes []Process `toml:"processes"`
// Slices is the collection of slices contributed by the buildpack.
Slices []Slice `toml:"slices"`
// BOM is a collection of entries for the bill of materials.
//
// Deprecated: as of Buildpack API 0.7, write to `layer.BOMPath()` instead
BOM []BOMEntry `toml:"bom"`
}
func (l LaunchTOML) isEmpty() bool {
return len(l.Labels) == 0 && len(l.Processes) == 0 && len(l.Slices) == 0 && len(l.BOM) == 0
}
// BuildTOML represents the contents of build.toml.
type BuildTOML struct {
// BOM contains the build-time bill of materials.
//
// Deprecated: as of Buildpack API 0.7, write to `layer.BOMPath()` instead
BOM []BOMEntry `toml:"bom"`
// Unmet is a collection of buildpack plan entries that should be passed through to subsequent providers.
Unmet []UnmetPlanEntry
}
func (b BuildTOML) isEmpty() bool {
return len(b.BOM) == 0 && len(b.Unmet) == 0
}
// BOMEntry contains a bill of materials entry.
//
// Deprecated: as of Buildpack API 0.7, BOM should use standard formats like CycloneDX going forward
type BOMEntry struct {
// Name represents the name of the entry.
Name string `toml:"name"`
// Metadata is the metadata of the entry. Optional.
Metadata map[string]interface{} `toml:"metadata,omitempty"`
// Launch indicates whether the given entry is included in app image. If launch is true the entry
// will be added to the app image Bill of Materials. Launch should be true if the entry describes
// the contents of a launch layer or app layer.
Launch bool `toml:"-"`
// Build indicates whether the given entry is available at build time. If build is true the entry
// will be added to the build Bill of Materials.
Build bool `toml:"-"`
}
// Store represents the contents of store.toml
type Store struct {
// Metadata represents the persistent metadata.
Metadata map[string]interface{} `toml:"metadata"`
}

285
build.go
View File

@ -25,17 +25,17 @@ import (
"strings" "strings"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/Masterminds/semver" "github.com/Masterminds/semver/v3"
"github.com/buildpacks/libcnb/v2/internal" "github.com/buildpacks/libcnb/internal"
"github.com/buildpacks/libcnb/v2/log" "github.com/buildpacks/libcnb/poet"
) )
// BuildContext contains the inputs to build. // BuildContext contains the inputs to build.
type BuildContext struct { type BuildContext struct {
// ApplicationPath is the location of the application source code as provided by
// the lifecycle. // Application is application to build.
ApplicationPath string Application Application
// Buildpack is metadata about the buildpack, from buildpack.toml. // Buildpack is metadata about the buildpack, from buildpack.toml.
Buildpack Buildpack Buildpack Buildpack
@ -43,9 +43,6 @@ type BuildContext struct {
// Layers is the layers available to the buildpack. // Layers is the layers available to the buildpack.
Layers Layers Layers Layers
// Logger is the way to write messages to the end user
Logger log.Logger
// PersistentMetadata is metadata that is persisted even across cache cleaning. // PersistentMetadata is metadata that is persisted even across cache cleaning.
PersistentMetadata map[string]interface{} PersistentMetadata map[string]interface{}
@ -55,23 +52,22 @@ type BuildContext struct {
// Platform is the contents of the platform. // Platform is the contents of the platform.
Platform Platform Platform Platform
// Deprecated: StackID is the ID of the stack. // StackID is the ID of the stack.
StackID string StackID string
// TargetInfo contains info of the target (os, arch, ...).
TargetInfo TargetInfo
// TargetDistro is the target distribution (name, version).
TargetDistro TargetDistro
} }
// BuildResult contains the results of detection. // BuildResult contains the results of detection.
type BuildResult struct { type BuildResult struct {
// BOM contains entries to be appended to the app image Bill of Materials and/or build Bill of Materials.
//
// Deprecated: as of Buildpack API 0.7, write to `layer.BOMPath()` instead
BOM *BOM
// Labels are the image labels contributed by the buildpack. // Labels are the image labels contributed by the buildpack.
Labels []Label Labels []Label
// Layers is the collection of LayerCreators contributed by the buildpack. // Layers is the collection of LayerCreators contributed by the buildpack.
Layers []Layer Layers []LayerContributor
// PersistentMetadata is metadata that is persisted even across cache cleaning. // PersistentMetadata is metadata that is persisted even across cache cleaning.
PersistentMetadata map[string]interface{} PersistentMetadata map[string]interface{}
@ -87,19 +83,27 @@ type BuildResult struct {
Unmet []UnmetPlanEntry Unmet []UnmetPlanEntry
} }
// BOM contains all Bill of Materials entries
//
// Deprecated: as of Buildpack API 0.7, write to `layer.BOMPath()` instead
type BOM struct {
Entries []BOMEntry
}
// Constants to track minimum and maximum supported Buildpack API versions // Constants to track minimum and maximum supported Buildpack API versions
const ( const (
// MinSupportedBPVersion indicates the minium supported version of the Buildpacks API // MinSupportedBPVersion indicates the minium supported version of the Buildpacks API
MinSupportedBPVersion = "0.8" MinSupportedBPVersion = "0.5"
// MaxSupportedBPVersion indicates the maximum supported version of the Buildpacks API // MaxSupportedBPVersion indicates the maximum supported version of the Buildpacks API
MaxSupportedBPVersion = "0.10" MaxSupportedBPVersion = "0.8"
) )
// NewBuildResult creates a new BuildResult instance, initializing empty fields. // NewBuildResult creates a new BuildResult instance, initializing empty fields.
func NewBuildResult() BuildResult { func NewBuildResult() BuildResult {
return BuildResult{ return BuildResult{
PersistentMetadata: make(map[string]interface{}), PersistentMetadata: make(map[string]interface{}),
BOM: &BOM{},
} }
} }
@ -110,46 +114,58 @@ func (b BuildResult) String() string {
} }
return fmt.Sprintf( return fmt.Sprintf(
"{Labels:%+v Layers:%s PersistentMetadata:%+v Processes:%+v Slices:%+v, Unmet:%+v}", "{BOM: %+v, Labels:%+v Layers:%s PersistentMetadata:%+v Processes:%+v Slices:%+v, Unmet:%+v}",
b.Labels, l, b.PersistentMetadata, b.PersistentMetadata, b.Slices, b.Unmet, b.BOM, b.Labels, l, b.PersistentMetadata, b.PersistentMetadata, b.Slices, b.Unmet,
) )
} }
// BuildFunc takes a context and returns a result, performing buildpack build behaviors. //go:generate mockery --name=Builder --case=underscore
type BuildFunc func(context BuildContext) (BuildResult, error)
// Builder describes an interface for types that can be used by the Build function.
type Builder interface {
// Build takes a context and returns a result, performing buildpack build behaviors.
Build(context BuildContext) (BuildResult, error)
}
// Build is called by the main function of a buildpack, for build. // Build is called by the main function of a buildpack, for build.
func Build(build BuildFunc, config Config) { func Build(builder Builder, options ...Option) {
config := Config{
arguments: os.Args,
bomLabel: false,
environmentWriter: internal.EnvironmentWriter{},
exitHandler: internal.NewExitHandler(),
tomlWriter: internal.TOMLWriter{},
}
for _, option := range options {
config = option(config)
}
var ( var (
err error err error
file string file string
ok bool ok bool
) )
ctx := BuildContext{Logger: config.logger} ctx := BuildContext{}
logger := poet.NewLogger(os.Stdout)
ctx.ApplicationPath, err = os.Getwd() ctx.Application.Path, err = os.Getwd()
if err != nil { if err != nil {
config.exitHandler.Error(fmt.Errorf("unable to get working directory\n%w", err)) config.exitHandler.Error(fmt.Errorf("unable to get working directory\n%w", err))
return return
} }
if logger.IsDebugEnabled() {
if config.logger.IsDebugEnabled() { logger.Debug(ApplicationPathFormatter(ctx.Application.Path))
if err := config.contentWriter.Write("Application contents", ctx.ApplicationPath); err != nil {
config.logger.Debugf("unable to write application contents\n%w", err)
}
} }
if s, ok := os.LookupEnv(EnvBuildpackDirectory); ok { if s, ok := os.LookupEnv("CNB_BUILDPACK_DIR"); ok {
ctx.Buildpack.Path = filepath.Clean(s) ctx.Buildpack.Path = filepath.Clean(s)
} else { } else { // TODO: Remove branch once lifecycle has been updated to support this
config.exitHandler.Error(fmt.Errorf("unable to get CNB_BUILDPACK_DIR, not found")) ctx.Buildpack.Path = filepath.Clean(strings.TrimSuffix(config.arguments[0], filepath.Join("bin", "build")))
return
} }
if logger.IsDebugEnabled() {
if config.logger.IsDebugEnabled() { logger.Debug(BuildpackPathFormatter(ctx.Buildpack.Path))
if err := config.contentWriter.Write("Buildpack contents", ctx.Buildpack.Path); err != nil {
config.logger.Debugf("unable to write buildpack contents\n%w", err)
}
} }
file = filepath.Join(ctx.Buildpack.Path, "buildpack.toml") file = filepath.Join(ctx.Buildpack.Path, "buildpack.toml")
@ -157,7 +173,7 @@ func Build(build BuildFunc, config Config) {
config.exitHandler.Error(fmt.Errorf("unable to decode buildpack %s\n%w", file, err)) config.exitHandler.Error(fmt.Errorf("unable to decode buildpack %s\n%w", file, err))
return return
} }
config.logger.Debugf("Buildpack: %+v", ctx.Buildpack) logger.Debugf("Buildpack: %+v", ctx.Buildpack)
API, err := semver.NewVersion(ctx.Buildpack.API) API, err := semver.NewVersion(ctx.Buildpack.API)
if err != nil { if err != nil {
@ -167,54 +183,55 @@ func Build(build BuildFunc, config Config) {
compatVersionCheck, _ := semver.NewConstraint(fmt.Sprintf(">= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion)) compatVersionCheck, _ := semver.NewConstraint(fmt.Sprintf(">= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
if !compatVersionCheck.Check(API) { if !compatVersionCheck.Check(API) {
if MinSupportedBPVersion == MaxSupportedBPVersion {
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack API == %s", MinSupportedBPVersion))
return
}
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion)) config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
return return
} }
var buildpackPlanPath string
layersDir, ok := os.LookupEnv(EnvLayersDirectory) if API.LessThan(semver.MustParse("0.8")) {
if !ok { if len(config.arguments) != 4 {
config.exitHandler.Error(fmt.Errorf("expected CNB_LAYERS_DIR to be set")) config.exitHandler.Error(fmt.Errorf("expected 3 arguments and received %d", len(config.arguments)-1))
return return
} }
ctx.Layers = Layers{layersDir} ctx.Layers = Layers{config.arguments[1]}
ctx.Platform.Path = config.arguments[2]
ctx.Platform.Path, ok = os.LookupEnv(EnvPlatformDirectory) buildpackPlanPath = config.arguments[3]
if !ok { } else {
config.exitHandler.Error(fmt.Errorf("expected CNB_PLATFORM_DIR to be set")) layersDir, ok := os.LookupEnv("CNB_LAYERS_DIR")
return if !ok {
} config.exitHandler.Error(fmt.Errorf("expected CNB_LAYERS_DIR to be set"))
return
buildpackPlanPath, ok := os.LookupEnv(EnvBuildPlanPath) }
if !ok { ctx.Layers = Layers{layersDir}
config.exitHandler.Error(fmt.Errorf("expected CNB_BP_PLAN_PATH to be set")) ctx.Platform.Path, ok = os.LookupEnv("CNB_PLATFORM_DIR")
return if !ok {
} config.exitHandler.Error(fmt.Errorf("expected CNB_PLATFORM_DIR to be set"))
return
config.logger.Debugf("Layers: %+v", ctx.Layers) }
buildpackPlanPath, ok = os.LookupEnv("CNB_BP_PLAN_PATH")
if config.logger.IsDebugEnabled() { if !ok {
if err := config.contentWriter.Write("Platform contents", ctx.Platform.Path); err != nil { config.exitHandler.Error(fmt.Errorf("expected CNB_BP_PLAN_PATH to be set"))
config.logger.Debugf("unable to write platform contents\n%w", err) return
} }
} }
if ctx.Platform.Bindings, err = NewBindings(ctx.Platform.Path); err != nil { logger.Debugf("Layers: %+v", ctx.Layers)
if logger.IsDebugEnabled() {
logger.Debug(PlatformFormatter(ctx.Platform))
}
if ctx.Platform.Bindings, err = NewBindingsForBuild(ctx.Platform.Path); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform bindings %s\n%w", ctx.Platform.Path, err)) config.exitHandler.Error(fmt.Errorf("unable to read platform bindings %s\n%w", ctx.Platform.Path, err))
return return
} }
config.logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings) logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings)
file = filepath.Join(ctx.Platform.Path, "env") file = filepath.Join(ctx.Platform.Path, "env")
if ctx.Platform.Environment, err = internal.NewConfigMapFromPath(file); err != nil { if ctx.Platform.Environment, err = internal.NewConfigMapFromPath(file); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform environment %s\n%w", file, err)) config.exitHandler.Error(fmt.Errorf("unable to read platform environment %s\n%w", file, err))
return return
} }
config.logger.Debugf("Platform Environment: %s", ctx.Platform.Environment) logger.Debugf("Platform Environment: %s", ctx.Platform.Environment)
var store Store var store Store
file = filepath.Join(ctx.Layers.Path, "store.toml") file = filepath.Join(ctx.Layers.Path, "store.toml")
@ -223,39 +240,26 @@ func Build(build BuildFunc, config Config) {
return return
} }
ctx.PersistentMetadata = store.Metadata ctx.PersistentMetadata = store.Metadata
config.logger.Debugf("Persistent Metadata: %+v", ctx.PersistentMetadata) logger.Debugf("Persistent Metadata: %+v", ctx.PersistentMetadata)
if _, err = toml.DecodeFile(buildpackPlanPath, &ctx.Plan); err != nil && !os.IsNotExist(err) { if _, err = toml.DecodeFile(buildpackPlanPath, &ctx.Plan); err != nil && !os.IsNotExist(err) {
config.exitHandler.Error(fmt.Errorf("unable to decode buildpack plan %s\n%w", buildpackPlanPath, err)) config.exitHandler.Error(fmt.Errorf("unable to decode buildpack plan %s\n%w", buildpackPlanPath, err))
return return
} }
config.logger.Debugf("Buildpack Plan: %+v", ctx.Plan) logger.Debugf("Buildpack Plan: %+v", ctx.Plan)
if ctx.StackID, ok = os.LookupEnv(EnvStackID); !ok { if ctx.StackID, ok = os.LookupEnv("CNB_STACK_ID"); !ok {
config.logger.Debug("CNB_STACK_ID not set") logger.Debug("CNB_STACK_ID not set")
} else { } else {
config.logger.Debugf("Stack: %s", ctx.StackID) logger.Debugf("Stack: %s", ctx.StackID)
} }
if API.GreaterThan(semver.MustParse("0.9")) { result, err := builder.Build(ctx)
ctx.TargetInfo = TargetInfo{}
ctx.TargetInfo.OS, _ = os.LookupEnv(EnvTargetOS)
ctx.TargetInfo.Arch, _ = os.LookupEnv(EnvTargetArch)
ctx.TargetInfo.Variant, _ = os.LookupEnv(EnvTargetArchVariant)
config.logger.Debugf("System: %+v", ctx.TargetInfo)
ctx.TargetDistro = TargetDistro{}
ctx.TargetDistro.Name, _ = os.LookupEnv(EnvTargetDistroName)
ctx.TargetDistro.Version, _ = os.LookupEnv(EnvTargetDistroVersion)
config.logger.Debugf("Distro: %+v", ctx.TargetDistro)
}
result, err := build(ctx)
if err != nil { if err != nil {
config.exitHandler.Error(err) config.exitHandler.Error(err)
return return
} }
config.logger.Debugf("Result: %+v", result) logger.Debugf("Result: %+v", result)
file = filepath.Join(ctx.Layers.Path, "*.toml") file = filepath.Join(ctx.Layers.Path, "*.toml")
existing, err := filepath.Glob(file) existing, err := filepath.Glob(file)
@ -265,31 +269,60 @@ func Build(build BuildFunc, config Config) {
} }
var contributed []string var contributed []string
for _, layer := range result.Layers { for _, creator := range result.Layers {
name := creator.Name()
layer, err := ctx.Layers.Layer(name)
if err != nil {
config.exitHandler.Error(fmt.Errorf("unable to create layer %s\n%w", name, err))
return
}
layer, err = creator.Contribute(layer)
if err != nil {
config.exitHandler.Error(fmt.Errorf("unable to invoke layer creator\n%w", err))
return
}
file = filepath.Join(layer.Path, "env.build") file = filepath.Join(layer.Path, "env.build")
config.logger.Debugf("Writing layer env.build: %s <= %+v", file, layer.BuildEnvironment) logger.Debugf("Writing layer env.build: %s <= %+v", file, layer.BuildEnvironment)
if err = config.environmentWriter.Write(file, layer.BuildEnvironment); err != nil { if err = config.environmentWriter.Write(file, layer.BuildEnvironment); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write layer env.build %s\n%w", file, err)) config.exitHandler.Error(fmt.Errorf("unable to write layer env.build %s\n%w", file, err))
return return
} }
file = filepath.Join(layer.Path, "env.launch") file = filepath.Join(layer.Path, "env.launch")
config.logger.Debugf("Writing layer env.launch: %s <= %+v", file, layer.LaunchEnvironment) logger.Debugf("Writing layer env.launch: %s <= %+v", file, layer.LaunchEnvironment)
if err = config.environmentWriter.Write(file, layer.LaunchEnvironment); err != nil { if err = config.environmentWriter.Write(file, layer.LaunchEnvironment); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write layer env.launch %s\n%w", file, err)) config.exitHandler.Error(fmt.Errorf("unable to write layer env.launch %s\n%w", file, err))
return return
} }
file = filepath.Join(layer.Path, "env") file = filepath.Join(layer.Path, "env")
config.logger.Debugf("Writing layer env: %s <= %+v", file, layer.SharedEnvironment) logger.Debugf("Writing layer env: %s <= %+v", file, layer.SharedEnvironment)
if err = config.environmentWriter.Write(file, layer.SharedEnvironment); err != nil { if err = config.environmentWriter.Write(file, layer.SharedEnvironment); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write layer env %s\n%w", file, err)) config.exitHandler.Error(fmt.Errorf("unable to write layer env %s\n%w", file, err))
return return
} }
file = filepath.Join(layer.Path, "profile.d")
logger.Debugf("Writing layer profile.d: %s <= %+v", file, layer.Profile)
if err = config.environmentWriter.Write(file, layer.Profile); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write layer profile.d %s\n%w", file, err))
return
}
file = filepath.Join(ctx.Layers.Path, fmt.Sprintf("%s.toml", layer.Name)) file = filepath.Join(ctx.Layers.Path, fmt.Sprintf("%s.toml", layer.Name))
config.logger.Debugf("Writing layer metadata: %s <= %+v", file, layer) logger.Debugf("Writing layer metadata: %s <= %+v", file, layer)
if err = config.tomlWriter.Write(file, layer); err != nil { var toWrite interface{} = layer
if API.Equal(semver.MustParse("0.5")) {
toWrite = internal.LayerAPI5{
Build: layer.LayerTypes.Build,
Cache: layer.LayerTypes.Cache,
Launch: layer.LayerTypes.Launch,
Metadata: layer.Metadata,
}
}
if err = config.tomlWriter.Write(file, toWrite); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write layer metadata %s\n%w", file, err)) config.exitHandler.Error(fmt.Errorf("unable to write layer metadata %s\n%w", file, err))
return return
} }
@ -301,7 +334,7 @@ func Build(build BuildFunc, config Config) {
continue continue
} }
config.logger.Debugf("Removing %s", e) logger.Debugf("Removing %s", e)
if err := os.RemoveAll(e); err != nil { if err := os.RemoveAll(e); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to remove %s\n%w", e, err)) config.exitHandler.Error(fmt.Errorf("unable to remove %s\n%w", e, err))
@ -309,20 +342,53 @@ func Build(build BuildFunc, config Config) {
} }
} }
if err := validateSBOMFormats(ctx.Layers.Path, ctx.Buildpack.Info.SBOMFormats); err != nil { if API.GreaterThan(semver.MustParse("0.7")) || API.Equal(semver.MustParse("0.7")) {
config.exitHandler.Error(fmt.Errorf("unable to validate SBOM\n%w", err)) if err := validateSBOMFormats(ctx.Layers.Path, ctx.Buildpack.Info.SBOMFormats); err != nil {
return config.exitHandler.Error(fmt.Errorf("unable to validate SBOM\n%w", err))
return
}
}
// Deprecated: as of Buildpack API 0.7, to be removed in a future version
var launchBOM, buildBOM []BOMEntry
if result.BOM != nil && config.bomLabel {
for _, entry := range result.BOM.Entries {
if entry.Launch {
launchBOM = append(launchBOM, entry)
}
if entry.Build {
buildBOM = append(buildBOM, entry)
}
}
} }
launch := LaunchTOML{ launch := LaunchTOML{
Labels: result.Labels, Labels: result.Labels,
Processes: result.Processes, Processes: result.Processes,
Slices: result.Slices, Slices: result.Slices,
BOM: launchBOM,
} }
if !launch.isEmpty() { if !launch.isEmpty() {
file = filepath.Join(ctx.Layers.Path, "launch.toml") file = filepath.Join(ctx.Layers.Path, "launch.toml")
config.logger.Debugf("Writing application metadata: %s <= %+v", file, launch) logger.Debugf("Writing application metadata: %s <= %+v", file, launch)
if API.LessThan(semver.MustParse("0.6")) {
for _, process := range launch.Processes {
if process.Default {
logger.Info("WARNING: Launch layer is setting default=true, but that is not supported until API version 0.6. This setting will be ignored.")
}
}
}
if API.LessThan(semver.MustParse("0.8")) {
for i, process := range launch.Processes {
if process.WorkingDirectory != "" {
logger.Infof("WARNING: Launch layer is setting working-directory=%s, but that is not supported until API version 0.8. This setting will be ignored.", process.WorkingDirectory)
launch.Processes[i].WorkingDirectory = ""
}
}
}
if err = config.tomlWriter.Write(file, launch); err != nil { if err = config.tomlWriter.Write(file, launch); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write application metadata %s\n%w", file, err)) config.exitHandler.Error(fmt.Errorf("unable to write application metadata %s\n%w", file, err))
@ -330,15 +396,16 @@ func Build(build BuildFunc, config Config) {
} }
} }
buildTOML := BuildTOML{ build := BuildTOML{
Unmet: result.Unmet, Unmet: result.Unmet,
BOM: buildBOM,
} }
if !buildTOML.isEmpty() { if !build.isEmpty() {
file = filepath.Join(ctx.Layers.Path, "build.toml") file = filepath.Join(ctx.Layers.Path, "build.toml")
config.logger.Debugf("Writing build metadata: %s <= %+v", file, build) logger.Debugf("Writing build metadata: %s <= %+v", file, build)
if err = config.tomlWriter.Write(file, buildTOML); err != nil { if err = config.tomlWriter.Write(file, build); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write build metadata %s\n%w", file, err)) config.exitHandler.Error(fmt.Errorf("unable to write build metadata %s\n%w", file, err))
return return
} }
@ -349,7 +416,7 @@ func Build(build BuildFunc, config Config) {
Metadata: result.PersistentMetadata, Metadata: result.PersistentMetadata,
} }
file = filepath.Join(ctx.Layers.Path, "store.toml") file = filepath.Join(ctx.Layers.Path, "store.toml")
config.logger.Debugf("Writing persistent metadata: %s <= %+v", file, store) logger.Debugf("Writing persistent metadata: %s <= %+v", file, store)
if err = config.tomlWriter.Write(file, store); err != nil { if err = config.tomlWriter.Write(file, store); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write persistent metadata %s\n%w", file, err)) config.exitHandler.Error(fmt.Errorf("unable to write persistent metadata %s\n%w", file, err))
return return

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +0,0 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb
// BuildTOML represents the contents of build.toml.
type BuildTOML struct {
// Unmet is a collection of buildpack plan entries that should be passed through to subsequent providers.
Unmet []UnmetPlanEntry
}
func (b BuildTOML) isEmpty() bool {
return len(b.Unmet) == 0
}

View File

@ -76,39 +76,13 @@ type BuildpackOrder struct {
Groups []BuildpackOrderBuildpack `toml:"group"` Groups []BuildpackOrderBuildpack `toml:"group"`
} }
// Deprecated: BuildpackStack is a stack supported by the buildpack. // BuildpackStack is a stack supported by the buildpack.
type BuildpackStack struct { type BuildpackStack struct {
// ID is the id of the stack. // ID is the id of the stack.
ID string `toml:"id"` ID string `toml:"id"`
}
// TargetDistro is the supported target distro // Mixins is the collection of mixins associated with the stack.
type TargetDistro struct { Mixins []string `toml:"mixins"`
// Name is the name of the supported distro.
Name string `toml:"name"`
// Version is the version of the supported distro.
Version string `toml:"version"`
}
// TargetInfo is the supported target
type TargetInfo struct {
// OS is the supported os.
OS string `toml:"os"`
// Arch is the supported architecture.
Arch string `toml:"arch"`
// Variant is the supported variant of the architecture.
Variant string `toml:"variant"`
}
// Target is a target supported by the buildpack.
type Target struct {
TargetInfo
// Distros is the collection of distros associated with the target.
Distros []TargetDistro `toml:"distros"`
} }
// Buildpack is the contents of the buildpack.toml file. // Buildpack is the contents of the buildpack.toml file.
@ -122,12 +96,9 @@ type Buildpack struct {
// Path is the path to the buildpack. // Path is the path to the buildpack.
Path string `toml:"-"` Path string `toml:"-"`
// Deprecated: Stacks is the collection of stacks supported by the buildpack. // Stacks is the collection of stacks supported by the buildpack.
Stacks []BuildpackStack `toml:"stacks"` Stacks []BuildpackStack `toml:"stacks"`
// Targets is the collection of targets supported by the buildpack.
Targets []Target `toml:"targets"`
// Metadata is arbitrary metadata attached to the buildpack. // Metadata is arbitrary metadata attached to the buildpack.
Metadata map[string]interface{} `toml:"metadata"` Metadata map[string]interface{} `toml:"metadata"`
} }

View File

@ -23,7 +23,7 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2" "github.com/buildpacks/libcnb"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
@ -35,7 +35,7 @@ func testBuildpackTOML(t *testing.T, _ spec.G, it spec.S) {
it("does not serialize the Path field", func() { it("does not serialize the Path field", func() {
bp := libcnb.Buildpack{ bp := libcnb.Buildpack{
API: "0.8", API: "0.6",
Info: libcnb.BuildpackInfo{ Info: libcnb.BuildpackInfo{
ID: "test-buildpack/sample", ID: "test-buildpack/sample",
Name: "sample", Name: "sample",

View File

@ -16,13 +16,6 @@
package libcnb package libcnb
import (
"os"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/v2/log"
)
//go:generate mockery --name EnvironmentWriter --case=underscore //go:generate mockery --name EnvironmentWriter --case=underscore
// EnvironmentWriter is the interface implemented by a type that wants to serialize a map of environment variables to // EnvironmentWriter is the interface implemented by a type that wants to serialize a map of environment variables to
@ -71,43 +64,17 @@ type ExecDWriter interface {
// Config is an object that contains configurable properties for execution. // Config is an object that contains configurable properties for execution.
type Config struct { type Config struct {
arguments []string arguments []string
dirContentFormatter log.DirectoryContentFormatter bomLabel bool
environmentWriter EnvironmentWriter environmentWriter EnvironmentWriter
execdWriter ExecDWriter exitHandler ExitHandler
exitHandler ExitHandler tomlWriter TOMLWriter
logger log.Logger execdWriter ExecDWriter
tomlWriter TOMLWriter
contentWriter internal.DirectoryContentsWriter
extension bool
} }
// Option is a function for configuring a Config instance. // Option is a function for configuring a Config instance.
type Option func(config Config) Config type Option func(config Config) Config
// NewConfig will generate a config from the given set of options
func NewConfig(options ...Option) Config {
config := Config{}
// apply defaults
options = append([]Option{
WithArguments(os.Args),
WithEnvironmentWriter(internal.EnvironmentWriter{}),
WithExitHandler(internal.NewExitHandler()),
WithLogger(log.New(os.Stdout)),
WithTOMLWriter(internal.TOMLWriter{}),
WithDirectoryContentFormatter(internal.NewPlainDirectoryContentFormatter()),
}, options...)
for _, opt := range options {
config = opt(config)
}
config.contentWriter = internal.NewDirectoryContentsWriter(config.dirContentFormatter, config.logger.DebugWriter())
return config
}
// WithArguments creates an Option that sets a collection of arguments. // WithArguments creates an Option that sets a collection of arguments.
func WithArguments(arguments []string) Option { func WithArguments(arguments []string) Option {
return func(config Config) Config { return func(config Config) Config {
@ -148,18 +115,11 @@ func WithExecDWriter(execdWriter ExecDWriter) Option {
} }
} }
// WithLogger creates an Option that sets a ExecDWriter implementation. // WithBOMLabel creates an Option that enables/disables writing the BOM Label
func WithLogger(logger log.Logger) Option { // Deprecated: as of Buildpack API 0.7, to be removed in a future version
func WithBOMLabel(bomLabel bool) Option {
return func(config Config) Config { return func(config Config) Config {
config.logger = logger config.bomLabel = bomLabel
return config
}
}
// WithDirectoryContentFormatter creates an Option that sets a ExecDWriter implementation.
func WithDirectoryContentFormatter(formatter log.DirectoryContentFormatter) Option {
return func(config Config) Config {
config.dirContentFormatter = formatter
return config return config
} }
} }

180
detect.go
View File

@ -21,30 +21,24 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/Masterminds/semver" "github.com/Masterminds/semver/v3"
"github.com/buildpacks/libcnb/v2/internal" "github.com/buildpacks/libcnb/internal"
"github.com/buildpacks/libcnb/v2/log" "github.com/buildpacks/libcnb/poet"
) )
// DetectContext contains the inputs to detection. // DetectContext contains the inputs to detection.
type DetectContext struct { type DetectContext struct {
// ApplicationPath is the location of the application source code as provided by // Application is the application to build.
// the lifecycle. Application Application
ApplicationPath string
// Buildpack is metadata about the buildpack from buildpack.toml (empty when processing an extension) // Buildpack is metadata about the buildpack, from buildpack.toml.
Buildpack Buildpack Buildpack Buildpack
// Extension is metadata about the extension from extension.toml (empty when processing a buildpack)
Extension Extension
// Logger is the way to write messages to the end user
Logger log.Logger
// Platform is the contents of the platform. // Platform is the contents of the platform.
Platform Platform Platform Platform
@ -62,78 +56,62 @@ type DetectResult struct {
Plans []BuildPlan Plans []BuildPlan
} }
// DetectFunc takes a context and returns a result, performing buildpack detect behaviors. //go:generate mockery --name Detector --case=underscore
type DetectFunc func(context DetectContext) (DetectResult, error)
// Detector describes an interface for types that can be used by the Detect function.
type Detector interface {
// Detect takes a context and returns a result, performing buildpack detect behaviors.
Detect(context DetectContext) (DetectResult, error)
}
// Detect is called by the main function of a buildpack, for detection. // Detect is called by the main function of a buildpack, for detection.
func Detect(detect DetectFunc, config Config) { func Detect(detector Detector, options ...Option) {
var ( config := Config{
err error arguments: os.Args,
file string environmentWriter: internal.EnvironmentWriter{},
ok bool exitHandler: internal.NewExitHandler(),
api string tomlWriter: internal.TOMLWriter{},
path string
destination interface{}
)
ctx := DetectContext{Logger: config.logger}
var moduletype = "buildpack"
if config.extension {
moduletype = "extension"
} }
ctx.ApplicationPath, err = os.Getwd() for _, option := range options {
config = option(config)
}
var (
err error
file string
ok bool
)
ctx := DetectContext{}
logger := poet.NewLogger(os.Stdout)
ctx.Application.Path, err = os.Getwd()
if err != nil { if err != nil {
config.exitHandler.Error(fmt.Errorf("unable to get working directory\n%w", err)) config.exitHandler.Error(fmt.Errorf("unable to get working directory\n%w", err))
return return
} }
if logger.IsDebugEnabled() {
if config.logger.IsDebugEnabled() { logger.Debug(ApplicationPathFormatter(ctx.Application.Path))
if err := config.contentWriter.Write("Application contents", ctx.ApplicationPath); err != nil {
config.logger.Debugf("unable to write application contents\n%w", err)
}
} }
if !config.extension { if s, ok := os.LookupEnv("CNB_BUILDPACK_DIR"); ok {
if s, ok := os.LookupEnv(EnvBuildpackDirectory); ok { ctx.Buildpack.Path = filepath.Clean(s)
path = filepath.Clean(s) } else { // TODO: Remove branch once lifecycle has been updated to support this
} else { ctx.Buildpack.Path = filepath.Clean(strings.TrimSuffix(config.arguments[0], filepath.Join("bin", "detect")))
config.exitHandler.Error(fmt.Errorf("unable to get CNB_BUILDPACK_DIR, not found")) }
return if logger.IsDebugEnabled() {
} logger.Debug(BuildpackPathFormatter(ctx.Buildpack.Path))
ctx.Buildpack.Path = path
destination = &ctx.Buildpack
file = filepath.Join(ctx.Buildpack.Path, "buildpack.toml")
} else {
if s, ok := os.LookupEnv(EnvExtensionDirectory); ok {
path = filepath.Clean(s)
} else {
config.exitHandler.Error(fmt.Errorf("unable to get CNB_EXTENSION_DIR, not found"))
return
}
ctx.Extension.Path = path
destination = &ctx.Extension
file = filepath.Join(ctx.Extension.Path, "extension.toml")
} }
if _, err = toml.DecodeFile(file, destination); err != nil && !os.IsNotExist(err) { file = filepath.Join(ctx.Buildpack.Path, "buildpack.toml")
config.exitHandler.Error(fmt.Errorf("unable to decode %s %s\n%w", moduletype, file, err)) if _, err = toml.DecodeFile(file, &ctx.Buildpack); err != nil && !os.IsNotExist(err) {
config.exitHandler.Error(fmt.Errorf("unable to decode buildpack %s\n%w", file, err))
return return
} }
config.logger.Debugf("%s: %+v", moduletype, ctx.Buildpack) logger.Debugf("Buildpack: %+v", ctx.Buildpack)
if config.logger.IsDebugEnabled() { API, err := semver.NewVersion(ctx.Buildpack.API)
if err := config.contentWriter.Write(moduletype+" contents", path); err != nil {
config.logger.Debugf("unable to write %s contents\n%w", moduletype, err)
}
}
if config.extension {
api = ctx.Extension.API
} else {
api = ctx.Buildpack.API
}
API, err := semver.NewVersion(api)
if err != nil { if err != nil {
config.exitHandler.Error(errors.New("version cannot be parsed")) config.exitHandler.Error(errors.New("version cannot be parsed"))
return return
@ -141,61 +119,61 @@ func Detect(detect DetectFunc, config Config) {
compatVersionCheck, _ := semver.NewConstraint(fmt.Sprintf(">= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion)) compatVersionCheck, _ := semver.NewConstraint(fmt.Sprintf(">= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
if !compatVersionCheck.Check(API) { if !compatVersionCheck.Check(API) {
if MinSupportedBPVersion == MaxSupportedBPVersion {
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack API == %s", MinSupportedBPVersion))
return
}
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion)) config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
return return
} }
var buildPlanPath string var buildPlanPath string
ctx.Platform.Path, ok = os.LookupEnv(EnvPlatformDirectory) if API.LessThan(semver.MustParse("0.8")) {
if !ok { if len(config.arguments) != 3 {
config.exitHandler.Error(fmt.Errorf("expected CNB_PLATFORM_DIR to be set")) config.exitHandler.Error(fmt.Errorf("expected 2 arguments and received %d", len(config.arguments)-1))
return return
} }
ctx.Platform.Path = config.arguments[1]
buildPlanPath, ok = os.LookupEnv(EnvDetectPlanPath) buildPlanPath = config.arguments[2]
if !ok { } else {
config.exitHandler.Error(fmt.Errorf("expected CNB_BUILD_PLAN_PATH to be set")) ctx.Platform.Path, ok = os.LookupEnv("CNB_PLATFORM_DIR")
return if !ok {
} config.exitHandler.Error(fmt.Errorf("expected CNB_PLATFORM_DIR to be set"))
return
if config.logger.IsDebugEnabled() { }
if err := config.contentWriter.Write("Platform contents", ctx.Platform.Path); err != nil { buildPlanPath, ok = os.LookupEnv("CNB_BUILD_PLAN_PATH")
config.logger.Debugf("unable to write platform contents\n%w", err) if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_BUILD_PLAN_PATH to be set"))
return
} }
} }
file = filepath.Join(ctx.Platform.Path, "bindings") if logger.IsDebugEnabled() {
if ctx.Platform.Bindings, err = NewBindings(ctx.Platform.Path); err != nil { logger.Debug(PlatformFormatter(ctx.Platform))
config.exitHandler.Error(fmt.Errorf("unable to read platform bindings %s\n%w", file, err)) }
if ctx.Platform.Bindings, err = NewBindingsForBuild(ctx.Platform.Path); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform bindings %s\n%w", ctx.Platform.Path, err))
return return
} }
config.logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings) logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings)
file = filepath.Join(ctx.Platform.Path, "env") file = filepath.Join(ctx.Platform.Path, "env")
if ctx.Platform.Environment, err = internal.NewConfigMapFromPath(file); err != nil { if ctx.Platform.Environment, err = internal.NewConfigMapFromPath(file); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform environment %s\n%w", file, err)) config.exitHandler.Error(fmt.Errorf("unable to read platform environment %s\n%w", file, err))
return return
} }
config.logger.Debugf("Platform Environment: %s", ctx.Platform.Environment) logger.Debugf("Platform Environment: %s", ctx.Platform.Environment)
if ctx.StackID, ok = os.LookupEnv(EnvStackID); !ok { if ctx.StackID, ok = os.LookupEnv("CNB_STACK_ID"); !ok {
config.logger.Debug("CNB_STACK_ID not set") logger.Debug("CNB_STACK_ID not set")
} else { } else {
config.logger.Debugf("Stack: %s", ctx.StackID) logger.Debugf("Stack: %s", ctx.StackID)
} }
result, err := detect(ctx) result, err := detector.Detect(ctx)
if err != nil { if err != nil {
config.exitHandler.Error(err) config.exitHandler.Error(err)
return return
} }
config.logger.Debugf("Result: %+v", result) logger.Debugf("Result: %+v", result)
if !result.Pass { if !result.Pass {
config.exitHandler.Fail() config.exitHandler.Fail()
@ -211,7 +189,7 @@ func Detect(detect DetectFunc, config Config) {
plans.Or = result.Plans[1:] plans.Or = result.Plans[1:]
} }
config.logger.Debugf("Writing build plans: %s <= %+v", buildPlanPath, plans) logger.Debugf("Writing build plans: %s <= %+v", buildPlanPath, plans)
if err := config.tomlWriter.Write(buildPlanPath, plans); err != nil { if err := config.tomlWriter.Write(buildPlanPath, plans); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write buildplan %s\n%w", buildPlanPath, err)) config.exitHandler.Error(fmt.Errorf("unable to write buildplan %s\n%w", buildPlanPath, err))
return return

View File

@ -26,9 +26,8 @@ import (
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/buildpacks/libcnb/v2" "github.com/buildpacks/libcnb"
"github.com/buildpacks/libcnb/v2/log" "github.com/buildpacks/libcnb/mocks"
"github.com/buildpacks/libcnb/v2/mocks"
) )
func testDetect(t *testing.T, context spec.G, it spec.S) { func testDetect(t *testing.T, context spec.G, it spec.S) {
@ -39,7 +38,7 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
buildpackPath string buildpackPath string
buildPlanPath string buildPlanPath string
commandPath string commandPath string
detectFunc libcnb.DetectFunc detector *mocks.Detector
exitHandler *mocks.ExitHandler exitHandler *mocks.ExitHandler
platformPath string platformPath string
tomlWriter *mocks.TOMLWriter tomlWriter *mocks.TOMLWriter
@ -50,18 +49,17 @@ func testDetect(t *testing.T, context spec.G, it spec.S) {
it.Before(func() { it.Before(func() {
var err error var err error
applicationPath, err = os.MkdirTemp("", "detect-application-path") applicationPath = t.TempDir()
Expect(err).NotTo(HaveOccurred())
applicationPath, err = filepath.EvalSymlinks(applicationPath) applicationPath, err = filepath.EvalSymlinks(applicationPath)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
buildpackPath, err = os.MkdirTemp("", "detect-buildpack-path") buildpackPath = t.TempDir()
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(os.Setenv("CNB_BUILDPACK_DIR", buildpackPath)).To(Succeed()) Expect(os.Setenv("CNB_BUILDPACK_DIR", buildpackPath)).To(Succeed())
Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
[]byte(` []byte(`
api = "0.8" api = "0.6"
[buildpack] [buildpack]
id = "test-id" id = "test-id"
@ -81,6 +79,7 @@ uri = "https://spdx.org/licenses/Apache-1.1.html"
[[stacks]] [[stacks]]
id = "test-id" id = "test-id"
mixins = ["test-name"]
[metadata] [metadata]
test-key = "test-value" test-key = "test-value"
@ -95,17 +94,14 @@ test-key = "test-value"
commandPath = filepath.Join("bin", "detect") commandPath = filepath.Join("bin", "detect")
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { detector = &mocks.Detector{}
return libcnb.DetectResult{}, nil
}
exitHandler = &mocks.ExitHandler{} exitHandler = &mocks.ExitHandler{}
exitHandler.On("Error", mock.Anything) exitHandler.On("Error", mock.Anything)
exitHandler.On("Fail") exitHandler.On("Fail")
exitHandler.On("Pass") exitHandler.On("Pass")
platformPath, err = os.MkdirTemp("", "detect-platform-path") platformPath = t.TempDir()
Expect(err).NotTo(HaveOccurred())
Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha"), 0755)).To(Succeed()) Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(platformPath, "bindings", "alpha", "test-secret-key"), Expect(os.WriteFile(filepath.Join(platformPath, "bindings", "alpha", "test-secret-key"),
@ -144,7 +140,7 @@ test-key = "test-value"
it.Before(func() { it.Before(func() {
Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
[]byte(` []byte(`
api = "0.7" api = "0.4"
[buildpack] [buildpack]
id = "test-id" id = "test-id"
@ -156,25 +152,29 @@ version = "1.1.1"
}) })
it("fails", func() { it("fails", func() {
libcnb.Detect(detectFunc, libcnb.Detect(detector,
libcnb.NewConfig( libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}), libcnb.WithExitHandler(exitHandler),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard())),
) )
if libcnb.MinSupportedBPVersion == libcnb.MaxSupportedBPVersion { Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError( fmt.Sprintf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", libcnb.MinSupportedBPVersion, libcnb.MaxSupportedBPVersion),
fmt.Sprintf("this version of libcnb is only compatible with buildpack API == %s", libcnb.MinSupportedBPVersion))) ))
} else {
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
fmt.Sprintf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", libcnb.MinSupportedBPVersion, libcnb.MaxSupportedBPVersion),
))
}
}) })
}) })
context("errors if required env vars are not set", func() { it("encounters the wrong number of Arguments", func() {
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{}, nil)
libcnb.Detect(detector,
libcnb.WithArguments([]string{commandPath}),
libcnb.WithExitHandler(exitHandler),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("expected 2 arguments and received 0"))
})
context("errors if required env vars are not set for buildpack API >=0.8", func() {
for _, e := range []string{"CNB_PLATFORM_DIR", "CNB_BUILD_PLAN_PATH"} { for _, e := range []string{"CNB_PLATFORM_DIR", "CNB_BUILD_PLAN_PATH"} {
// We need to do this assignment because of the way that spec binds variables // We need to do this assignment because of the way that spec binds variables
envVar := e envVar := e
@ -195,10 +195,9 @@ version = "1.1.1"
}) })
it("fails", func() { it("fails", func() {
libcnb.Detect(detectFunc, libcnb.Detect(detector,
libcnb.NewConfig( libcnb.WithArguments([]string{commandPath}),
libcnb.WithArguments([]string{commandPath}), libcnb.WithExitHandler(exitHandler),
libcnb.WithExitHandler(exitHandler)),
) )
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError( Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
fmt.Sprintf("expected %s to be set", envVar), fmt.Sprintf("expected %s to be set", envVar),
@ -208,9 +207,7 @@ version = "1.1.1"
} }
}) })
context("has a detect environment", func() { context("when BP API >= 0.8", func() {
var ctx libcnb.DetectContext
it.Before(func() { it.Before(func() {
Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
[]byte(` []byte(`
@ -223,21 +220,18 @@ version = "1.1.1"
`), `),
0600), 0600),
).To(Succeed()) ).To(Succeed())
detectFunc = func(context libcnb.DetectContext) (libcnb.DetectResult, error) {
ctx = context
return libcnb.DetectResult{}, nil
}
}) })
it("creates context", func() { it("creates context", func() {
libcnb.Detect(detectFunc, detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: true}, nil)
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath}), libcnb.Detect(detector,
libcnb.WithExitHandler(exitHandler)), libcnb.WithArguments([]string{commandPath}),
libcnb.WithExitHandler(exitHandler),
) )
Expect(ctx.ApplicationPath).To(Equal(applicationPath)) ctx := detector.Calls[0].Arguments[0].(libcnb.DetectContext)
Expect(ctx.Application).To(Equal(libcnb.Application{Path: applicationPath}))
Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{ Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{
API: "0.8", API: "0.8",
Info: libcnb.BuildpackInfo{ Info: libcnb.BuildpackInfo{
@ -264,76 +258,108 @@ version = "1.1.1"
}) })
}) })
it("fails if CNB_BUILDPACK_DIR is not set", func() { context("when BP API < 0.8", func() {
it.Before(func() {
Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_BUILD_PLAN_PATH")).To(Succeed())
})
it("creates context", func() {
detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: true}, nil)
libcnb.Detect(detector,
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler),
)
ctx := detector.Calls[0].Arguments[0].(libcnb.DetectContext)
Expect(ctx.Application).To(Equal(libcnb.Application{Path: applicationPath}))
Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{
API: "0.6",
Info: libcnb.BuildpackInfo{
ID: "test-id",
Name: "test-name",
Version: "1.1.1",
ClearEnvironment: true,
Description: "A test buildpack",
Keywords: []string{"test", "buildpack"},
Licenses: []libcnb.License{
{Type: "Apache-2.0", URI: "https://spdx.org/licenses/Apache-2.0.html"},
{Type: "Apache-1.1", URI: "https://spdx.org/licenses/Apache-1.1.html"},
},
},
Path: buildpackPath,
Stacks: []libcnb.BuildpackStack{
{
ID: "test-id",
Mixins: []string{"test-name"},
},
},
Metadata: map[string]interface{}{"test-key": "test-value"},
}))
})
})
it("extracts buildpack path from command path if CNB_BUILDPACK_PATH is not set", func() {
Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed()) Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed())
libcnb.Detect(detectFunc, detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: true}, nil)
libcnb.NewConfig(
libcnb.WithArguments([]string{filepath.Join(buildpackPath, commandPath), platformPath, buildPlanPath}), libcnb.Detect(detector,
libcnb.WithExitHandler(exitHandler), libcnb.WithArguments([]string{filepath.Join(buildpackPath, commandPath), platformPath, buildPlanPath}),
libcnb.WithLogger(log.NewDiscard())), libcnb.WithExitHandler(exitHandler),
) )
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to get CNB_BUILDPACK_DIR, not found")) ctx := detector.Calls[0].Arguments[0].(libcnb.DetectContext)
Expect(ctx.Buildpack.Path).To(Equal(buildpackPath))
}) })
it("handles error from DetectFunc", func() { it("handles error from DetectFunc", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{}, fmt.Errorf("test-error"))
return libcnb.DetectResult{}, fmt.Errorf("test-error")
}
libcnb.Detect(detectFunc, libcnb.Detect(detector,
libcnb.NewConfig( libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}), libcnb.WithExitHandler(exitHandler),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard())),
) )
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("test-error")) Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("test-error"))
}) })
it("does not write empty files", func() { it("does not write empty files", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: true}, nil)
return libcnb.DetectResult{Pass: true}, nil
}
libcnb.Detect(detectFunc, libcnb.Detect(detector,
libcnb.NewConfig( libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}), libcnb.WithExitHandler(exitHandler),
libcnb.WithExitHandler(exitHandler), libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithLogger(log.NewDiscard())),
) )
Expect(tomlWriter.Calls).To(HaveLen(0)) Expect(tomlWriter.Calls).To(HaveLen(0))
}) })
it("writes one build plan", func() { it("writes one build plan", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{
return libcnb.DetectResult{ Pass: true,
Pass: true, Plans: []libcnb.BuildPlan{
Plans: []libcnb.BuildPlan{ {
{ Provides: []libcnb.BuildPlanProvide{
Provides: []libcnb.BuildPlanProvide{ {Name: "test-name"},
{Name: "test-name"}, },
}, Requires: []libcnb.BuildPlanRequire{
Requires: []libcnb.BuildPlanRequire{ {
{ Name: "test-name",
Name: "test-name", Metadata: map[string]interface{}{"test-key": "test-value"},
Metadata: map[string]interface{}{"test-key": "test-value"},
},
}, },
}, },
}, },
}, nil },
} }, nil)
libcnb.Detect(detectFunc, libcnb.Detect(detector,
libcnb.NewConfig( libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}), libcnb.WithExitHandler(exitHandler),
libcnb.WithExitHandler(exitHandler), libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithLogger(log.NewDiscard())),
) )
Expect(tomlWriter.Calls[0].Arguments.Get(0)).To(Equal(buildPlanPath)) Expect(tomlWriter.Calls[0].Arguments.Get(0)).To(Equal(buildPlanPath))
@ -353,42 +379,38 @@ version = "1.1.1"
}) })
it("writes two build plans", func() { it("writes two build plans", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{
return libcnb.DetectResult{ Pass: true,
Pass: true, Plans: []libcnb.BuildPlan{
Plans: []libcnb.BuildPlan{ {
{ Provides: []libcnb.BuildPlanProvide{
Provides: []libcnb.BuildPlanProvide{ {Name: "test-name-1"},
{Name: "test-name-1"},
},
Requires: []libcnb.BuildPlanRequire{
{
Name: "test-name-1",
Metadata: map[string]interface{}{"test-key-1": "test-value-1"},
},
},
}, },
{ Requires: []libcnb.BuildPlanRequire{
Provides: []libcnb.BuildPlanProvide{ {
{Name: "test-name-2"}, Name: "test-name-1",
}, Metadata: map[string]interface{}{"test-key-1": "test-value-1"},
Requires: []libcnb.BuildPlanRequire{
{
Name: "test-name-2",
Metadata: map[string]interface{}{"test-key-2": "test-value-2"},
},
}, },
}, },
}, },
}, nil {
} Provides: []libcnb.BuildPlanProvide{
{Name: "test-name-2"},
},
Requires: []libcnb.BuildPlanRequire{
{
Name: "test-name-2",
Metadata: map[string]interface{}{"test-key-2": "test-value-2"},
},
},
},
},
}, nil)
libcnb.Detect(detectFunc, libcnb.Detect(detector,
libcnb.NewConfig( libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}), libcnb.WithExitHandler(exitHandler),
libcnb.WithExitHandler(exitHandler), libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithLogger(log.NewDiscard())),
) )
Expect(tomlWriter.Calls[0].Arguments.Get(0)).To(Equal(buildPlanPath)) Expect(tomlWriter.Calls[0].Arguments.Get(0)).To(Equal(buildPlanPath))

219
docs/build.md Normal file
View File

@ -0,0 +1,219 @@
# Building
A buildpack is suitably packaged `detect` binary, a `build` binary and a `buildpack.toml`. Here we look at the implementation logic of the build binary for our buildpack. This starts by declaring a `struct` that we call `Builder`. The purpose of our struct is to satisfy `libcnb`s [`Builder`](https://pkg.go.dev/github.com/buildpacks/libcnb?utm_source=gopls#Builder) interface providing a receiver with signature `func Build(context BuildContext) (BuildResult, error)`. An implementation of `Builder` is passed to `libcnb.Main` as the main entry point to the `build` binary.
In our `Detector` we use a [`poet`](https://pkg.go.dev/github.com/buildpacks/libcnb/poet) logger instance. The logger instance will be used to inform the user of the progress of our `detect` phase.
```go
type Builder struct {
Logger poet.Logger
}
```
## Implementing Build
The most simple implementation of `func (context BuildContext) (BuildResult, error)` returns a `BuildResult` containing an ordered list `LayerContributor`s which will each contribute layers to the image.
```go
func (d Builder) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
return libcnb.NewBuildResult(), nil
}
```
The trivial passing implementation of `Build` contributes no layers to the result.
This trivial case ignores the `Plans` field of the `DetectResult` struct. In order to understand the `Plans` field we look at the concept of provides and requires.
### Builders and Contributors
It is recommended that a single `Builder` provides each layer using a `Contributor`. In this example we are contributing a single layer containing our extracted archive. Therefore, our `Builder` will orchestrate a single `Contributor`.
### Accessing `BuildPlan` Metadata
The `detect` phases passes metadata to the `build` phase in the [`BuildpackPlan`](https://pkg.go.dev/github.com/buildpacks/libcnb#BuildpackPlan) (note: not to be confused with the more general [`BuildPlan`](https://pkg.go.dev/github.com/buildpacks/libcnb#BuildPlan)). Multiple buildpack `Detect` executions can require that the `Build` of our example buildpack executes. As there may be requirements from multiple `detect` binaries, we must merge all the entries in the buildplan that correspond to our "example" buildpack.
```go
func (b Builder) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
b.Logger.Title(context.Buildpack)
// Resolve the "version" that the detect phase adds to the BuildPlan metadata
version, err := resolveVersion(context.Plan)
if err != nil {
return libcnb.NewBuildResult(), err
}
// If "version" was not supplied, use a default
if version == "" {
version = "0.0.1"
}
return libcnb.NewBuildResult(), nil
}
// resolveVersion takes the buildpack plan and resolves the version metadata
// It then returrns the "version" information extracted from the metadata.
func resolveVersion(plan libcnb.BuildpackPlan) (string, error) {
exampleBuildPlan := libcnb.BuildpackPlanEntry{}
// Find the buildpack plan entry that releates to our "example" buildpack
for _, e := range plan.Entries {
if e.Name == "example" {
exampleBuildPlan = e
}
}
// Extract the "version" metadata if present
if version, ok := exampleBuildPlan.Metadata["version"].(string); ok {
return version, nil
}
return "", fmt.Errorf("unable to resolve version from metadata.")
}
```
In general, multiple buildpacks could `Require` our example buildpack. In that general case there may be multiple `BuildpackPlan` entries referring to our `example` buildpack. A production-level utility function to resolve the build plan can be found in [`libpak`](https://github.com/paketo-buildpacks/libpak/blob/f24422191fc6a2a02178337d96dda1210faaae9f/buildpack_plan.go#L69).
### Parsing the Buildpack Metadata
We download the files to extract into a layer from an archive on the Internet. To make this easier to maintain, we embed the download URL as a string template in the `builpack.toml` metadata. Our download URL is of the form `"https://source.fake/releases/v{{ .version }}/example_{{ .version }}_linux_amd64.tar.gz"`. We accept the version parameter as detected in the `detect` phase, and replace the version into our download URL.
```go
func (b Builder) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
b.Logger.Title(context.Buildpack)
version, err := resolveVersion(context.Plan)
if err != nil {
return libcnb.NewBuildResult(), err
}
if version == "" {
version = "0.0.1"
}
// Use the standard Go text/template to compute the download URL from the
// version resolved from the buildpack plan.
urlTemplateMetadata, found := context.Buildpack.Metadata["url"]
if !found {
return libcnb.NewBuildResult(), fmt.Errorf("no url template in buildpack.toml metadata")
}
urlTemplate, ok := urlTemplateMetadata.(string)
if !ok {
return libcnb.NewBuildResult(), fmt.Errorf("unable to parse buildpack.toml metadata")
}
archiveURL, err := template.New("ArchiveVersion").Parse(urlTemplate)
if err != nil {
return libcnb.NewBuildResult(), fmt.Errorf("unable buildpack.toml url metadata is not a valid template")
}
var url bytes.Buffer
archiveURL.Execute(&url, struct{ version string }{version: version})
return libcnb.NewBuildResult(), nil
}
```
Now that we know our download URL, we are in a position to contribute a layer containing the extracted files.
### Contributing a Layer
To contribute a layer we implement the [`LayerContributor`](https://pkg.go.dev/github.com/buildpacks/libcnb#LayerContributor) interface. Our `Contributor` will download an archive from the provided URL. Again, we borrow a utility function from `libpak` to extract the archive to an output layer. You could use standard Go archive functions to do this, but it's more work.
```go
type Contributor struct {
Logger log.Logger
Archive string
}
func (Contributor) Name() string {
return "example"
}
func (c Contributor) Contribute(layer libcnb.Layer) (libcnb.Layer, error) {
// the values for caching layers are documented at
// https://buildpacks.io/docs/buildpack-author-guide/caching-strategies/
// Here we make the layer available for caching, as a build layer and as a
// launch layer on the output image
layer.Launch = true
layer.Cache = true
layer.Build = true
if err := os.MkdirAll(layer.Exec.Path, 0755); err != nil {
return libcnb.Layer{}, err
}
// Fetch the archive from the Internet
resp, err := http.Get(c.Archive)
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to download archive\n%w", err)
}
defer resp.Body.Close()
// Extract the archive into the output layer
if err := crush.Extract(resp.Body, layer.Path, 0); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to expand archive\n%w", err)
}
return layer, nil
}
```
We add our contributor to the `libcnb.BuildContext` layers. `libcnb` will ensure each `LayerContributor` is executed in the order that they are appended to the `BuildResult.Layers` list.
```go
func (b Builder) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
b.Logger.Title(context.Buildpack)
...
contributor := Contributor{
Logger: b.Logger,
Archive: url.String(),
}
result := libcnb.NewBuildResult()
result.Layers = append(result.Layers, contributor)
return result, nil
}
```
### Contributing a Startup Script and Environmental Variable
Finally, we assume that our run image contains a `bash` shell. An [`exec.d` style](https://buildpacks.io/docs/reference/spec/migration/buildpack-api-0.4-0.5/#execd) executable is contributed as to run on container startup.
An environment variable `EXAMPLE` is contributed to the image with the default value of `1.2.3`. It is left as an exercise to the reader to pass the `version` information from `Build` through the instance of `type Contributor struct` to the `EXAMPLE` environmental variable.
```go
const ExampleExecD string = `
#!/bin/bash
echo Executed ${EXAMPLE} on Startup
`
func (c Contributor) Contribute(layer libcnb.Layer) (libcnb.Layer, error) {
layer.Launch = true
layer.Cache = true
layer.Build = true
if err := os.MkdirAll(layer.Exec.Path, 0755); err != nil {
return libcnb.Layer{}, err
}
// fetch and extract tar.gz
resp, err := http.Get(c.Archive)
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to download archive\n%w", err)
}
defer resp.Body.Close()
if err := crush.Extract(resp.Body, layer.Path, 0); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to expand archive\n%w", err)
}
// We are using the exec.d interface here per - https://github.com/buildpacks/spec/blob/main/buildpack.md#execd
// #nosec G306
if err := os.WriteFile(layer.Exec.FilePath("example"), []byte(ExampleExecD), 0755); err != nil {
return libcnb.Layer{}, err
}
// Set environment variable
layer.LaunchEnvironment.Default("EXAMPLE", "1.2.3")
return layer, nil
}
```
## Summary
We have taken a quick tour of implementing a `build` binary using `libcnb`. We have seen how `libcnb` offers a straightforward `BuildResult` that can be used to return a number of `LayerContributor`s. In turn, each `LayerContributor` can contribute files to the output image, scripts executed on startup and environmental variable on the output image. Our tour of the `libcnb` API included a look at `libcnb.BuildContext` through which we have access to the `BuildPlan` computed after the `detect` phase and buildpack metadata.

224
docs/detect.md Normal file
View File

@ -0,0 +1,224 @@
# Detection
A buildpack is suitably packaged `detect` binary, a `build` binary and a `buildpack.toml`. Here we look at the implementation logic of the detect binary for our example buildpack. This starts by declaring a `struct` that we call `Detector`. The purpose of our struct is to satisfy `libcnb`s [`Detector`](https://pkg.go.dev/github.com/buildpacks/libcnb?utm_source=gopls#Detector) interface providing a receiver with signature `func Detect(context DetectContext) (DetectResult, error)`. An implementation of `Detector` is passed to `libcnb.Main` as the main entry point to the `detect` binary.
In our `Detector` we use a [`poet`](https://pkg.go.dev/github.com/buildpacks/libcnb/poet) logger instance. The logger instance will be used to inform the user of the progress of our `detect` phase.
```go
type Detector struct {
Logger poet.Logger
}
```
## Implementing Detect
The most simple implementation of `func (context DetectContext) (DetectResult, error)` returns a `DetectResult` ensuring that the this buildpack does not contribute to the build phase.
```go
func (d Detector) Detect(context DetectContext) (DetectResult, error) {
return DetectResult{}, nil
}
```
The trivial passing implementation of `Detect` ensures that the `build` binary of this buildpack will run during the build phase.
```go
func (d Detector) Detect(context DetectContext) (DetectResult, error) {
return DetectResult{Pass: true}, nil
}
```
These two trivial cases ignore the `Plans` field of the `DetectResult` struct. In order to understand the `Plans` field we look at the concept of provides and requires.
### Provides and Requires
Buildpacks may depend on other buildpacks. The dependencies are computed at runtime. Buildpacks can **provide** functionality that other buildpacks use. For example, a buildpack may provide a runtime such as the python runtime or a Java runtime environment; other buildpacks may detect that an application is a python or Java application and require the provided runtime. Buildpacks may also **require** functionality provided by other buildpacks.
A buildpack can offer multiple provides. However, it is common for a buildpack to offer only a single provide. Each provision is identified by a string. Here we declare `Example` as an identifier.
```go
const (
Example = "example"
)
```
Extending a trivial passing implementation with `Provides` allows another buildpack to require this buildpack.
```go
func (d Detector) Detect(context DetectContext) (DetectResult, error) {
return DetectResult{
Pass: true,
Plans: []libcnb.BuildPlan{
{
// Let the system know that if other buildpacks Require "example"
// then this buildpack Provides the implementation logic
Provides: []libcnb.BuildPlanProvide{
{Name: Example},
},
}
}
}, nil
}
```
The detect phase of another buildpack may require this buildpack. This is commonly used when the detect phase gathers information that is passed as metadata to the build phase. Similarly, it is common for a buildpack to require itself. This will allow the detect phase of a buildpack to pass information to the build phase.
```go
func (d Detector) Detect(context DetectContext) (DetectResult, error) {
return DetectResult{
Pass: true,
Plans: []libcnb.BuildPlan{
{
// Let the system know that if other buildpacks Require "example"
// then this buildpack Provides the implementation logic
Provides: []libcnb.BuildPlanProvide{
{Name: Example},
},
Requires: []libcmb.BuildPlanRequire{
{Name: Example},
}
}
}
}, nil
}
```
Here we pass the key-value pair `(version, 1.0)` as metadata from the detect phase of the example buildpack to the build phase. Metadata can be arbitrarily nested, but it must be textual as it is serialized to TOML at the end of the detect phase.
```go
func (d Detector) Detect(context DetectContext) (DetectResult, error) {
return DetectResult{
Pass: true,
Plans: []libcnb.BuildPlan{
{
// Let the system know that if other buildpacks Require "example"
// then this buildpack Provides the implementation logic
Provides: []libcnb.BuildPlanProvide{
{Name: Example},
},
Requires: []libcmb.BuildPlanRequire{
{
Name: Example,
// Pass arbitrary metadata to the Example build phase
Metadata: {
"version": "1.0",
}
},
}
}
}
}, nil
}
```
The `detect` binary is provided with read-only access to the application source code. As an example we can detect the presence of a file called `example.txt`. If this file does not exist, then we can fail at the detect phase.
```go
func (d Detector) Detect(context DetectContext) (DetectResult, error) {
const exampleFile := "example.txt"
// Construct a path to exampleFile in the Application source project
requirementsFile := filepath.Join(context.Application.Path, exampleFile)
// Test for the existance of exampleFile
if _, err := os.Stat(exampleFile); err != nil && os.IsNotExist(err) {
return libcnb.DetectResult{}, err
}
d.Logger.Debugf("Found %s example file -> %s", exampleFile)
...
}
```
We have defined a `detect` binary that passes if `example.txt` is present in the source application and fails otherwise.
During the detect phase the buildpack has access to static metadata declared in the `buildpack.toml` file.
## Accessing buildpack metadata
It is common for static metadata to be declared in a `buildpack.toml`. This is commonly used to pass URLs to the buildpack that are used to download artefacts. As an example, a `buildpack.toml` may declare the download location for the `example` artefact:
```toml
[metadata]
[[metadata.dependencies]]
url = "https://source.fake/releases/v{{ .version }}/example_{{ .version }}_linux_amd64.tar.gz"
```
In our running example the detect phase of our buildpack detects the presence of `example.txt`. We may want a buildpack definition to change the name of the file for detection.
```toml
[metadata]
[[metadata.dependencies]]
example-file = "custom-example.txt"
```
The `Detect(context)` function can access the metadata in the buildpack definition.
```go
func (d Detector) Detect(context DetectContext) (DetectResult, error) {
var exampleFile := "example.txt"
if configuredExampleFile, ok; context.Buildpack.Metadata["example-file"]; ok {
exampleFile = configuredExampleFile.(string)
}
// Construct a path to exampleFile in the Application source project
requirementsFile := filepath.Join(context.Application.Path, exampleFile)
// Test for the existance of exampleFile
if _, err := os.Stat(exampleFile); err != nil && os.IsNotExist(err) {
return libcnb.DetectResult{Pass: true}, err
}
d.Logger.Debugf("Found %s example file -> %s", exampleFile)
...
}
```
If `example.txt` can be overridden it is common to also provide an environment variable to allow overriding. Here we allow `example.txt` to be overridden using either a static definition in the `buildpack.toml` file or by the presence of a `BP_EXAMPLE_FILE` environment variable.
```go
const (
BpExampleFile = "BP_EXAMPLE_FILE"
)
func (d Detector) Detect(context DetectContext) (DetectResult, error) {
var exampleFile := "example.txt"
if configuredExampleFile, ok; context.Buildpack.Metadata["example-file"]; ok {
exampleFile = configuredExampleFile.(string)
}
if file, ok := context.Platform.Environment(BpExamplefile); ok {
exampleFile = file
}
// Construct a path to exampleFile in the Application source project
requirementsFile := filepath.Join(context.Application.Path, exampleFile)
// Test for the existance of exampleFile
if _, err := os.Stat(exampleFile); err != nil && os.IsNotExist(err) {
return libcnb.DetectResult{Pass: true}, err
}
d.Logger.Debugf("Found %s example file -> %s", exampleFile)
...
}
```
Buildpack metadata and environment variables are useful when passing configuration to a buildpack `detect` binary. Passing sensitive data, such as secrets, requires a different approach.
## Secrets and other bindings
Secrets are available through the `DetectContext`. Here we look for a binding named "GIT_TOKEN" which we expect to define a secret called "TOKEN". We assume the existence of a function with signature `findBinding([]libcnb.Bindings, string) (libcnb.Binding, error)`.
```go
const (
GitTokenBinding = "GIT_TOKEN"
GitToken = "TOKEN"
)
func (d Detector) Detect(context DetectContext) (DetectResult, error) {
bindings := context.Platform.Bindings
gitTokenBinding, err := findBinding(bindings, GitTokenBinding)
if err != nil {
return DetectResult{}, fmt.errorf("unable to access binding %s", GitTokenBinding)
}
if gitToken, exists := gitTokenBinding.Secret[GitToken]; !exists {
return DetectResult{}, fmt.errorf("unable to find secret %s in binding %s", GitToken, GitTokenBinding)
}
...
}
```
## Summary
We have taken a quick tour of implementing a `detect` binary using `libcnb`. We have seen how `libcnb` offers a straightforward `DetectResult` that can be used to signal success or failure of the detect phase. In addition the `DetectResult` can offer `Provides` and specify `Requires`. Our tour of the `libcnb` API included a look at `libcnb.DetectContext` through which we have access to environment variables, buildpack metadata and bindings.

44
docs/tutorial.md Normal file
View File

@ -0,0 +1,44 @@
# `libcnb` tutorial
[https://pkg.go.dev/github.com/buildpacks/libcnb?tab=doc](https://pkg.go.dev/github.com/buildpacks/libcnb?tab=doc) provides the reference documentation for `libcnb`. In this tutorial we walk through an example implementation using data structures and functions described in the reference documentation.
In general, buildpacks are responsible for
* installing runtimes eg: a `python` interpreter,
* installing dependency managers eg: Python's `pip` dependency manager,
* resolving application dependencies, and
* building/packaging the application code.
Commonly each of these responsibilities is factored out into a separate buildpacks.
In this tutorial we take a simplified look at a generic buildpack and implement both the `detect` and `build` binaries for an example buildpack.
## Scenario
Our buildpack will have the following properties:
* if the source project contains `example.txt` or `BP_EXAMPLE_FILE=some-file.txt` then the buildpack `detect` process will succeed.
* if `detect` passes, then our `build` binary will contribute a single layer to the output image. The layer will contain the contents of a gzipped archive fetched from an Internet location.
We first [`detect`](detect.md) whether this buildpack should contribute to the build. If detection passes we then run [`build`](build.md) to extract our example gzipped archive.
The main application of our buildpack will combine our implementation of `Detector` and `Builder` to provide the `detect` and `build` binaries:
```go
func main() {
detector := Detector{Logger: log.New(os.Stdout)}
builder := Builder{Logger: log.New(os.Stdout)}
libcnb.Main(detector, builder)
}
```
## Learning Goals
At the end of this tutorial you will have
* examined the separation of `detect` responsibilities from `build` responsibilities,
* introspected a source application to satisfy `detect`,
* passed metadata between the `detect` and `build` phases using the build plan,
* contributed a layer to an output image that contains the contents of an archive,
* set a default environment variable `EXAMPLE` on the output image, and
* defined an exec.d startup script on the output image.

View File

@ -23,7 +23,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2" "github.com/buildpacks/libcnb"
) )
func testEnvironment(t *testing.T, _ spec.G, it spec.S) { func testEnvironment(t *testing.T, _ spec.G, it spec.S) {

View File

@ -1,103 +0,0 @@
package examples
import (
"fmt"
"os"
"path/filepath"
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb/v2/log"
)
const (
DefaultVersion = "0.1"
)
type Builder struct {
Logger log.Logger
}
// BuildpackPlan may contain multiple entries for a single buildpack, resolve
// into a single entry.
func resolve(plan libcnb.BuildpackPlan, name string) libcnb.BuildpackPlanEntry {
entry := libcnb.BuildpackPlanEntry{
Name: name,
Metadata: map[string]interface{}{},
}
for _, e := range plan.Entries {
for k, v := range e.Metadata {
entry.Metadata[k] = v
}
}
return entry
}
func populateLayer(layer libcnb.Layer, version string) (libcnb.Layer, error) {
exampleFile := filepath.Join(layer.Path, "example.txt")
if err := os.WriteFile(exampleFile, []byte(version), 0600); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to write example file: %w", err)
}
layer.SharedEnvironment.Default("EXAMPLE_FILE", exampleFile)
// Provide an SBOM
bom := cdx.NewBOM()
bom.Metadata = &cdx.Metadata{
Component: &cdx.Component{
Type: cdx.ComponentTypeFile,
Name: "example",
Version: version,
},
}
sbomPath := layer.SBOMPath(libcnb.CycloneDXJSON)
sbomFile, err := os.OpenFile(sbomPath, os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return layer, err
}
defer sbomFile.Close()
encoder := cdx.NewBOMEncoder(sbomFile, cdx.BOMFileFormatJSON)
if err := encoder.Encode(bom); err != nil {
return layer, err
}
return layer, nil
}
func (b Builder) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
// Reduce possible multiple buildpack plan entries to a single entry
entry := resolve(context.Plan, Provides)
result := libcnb.NewBuildResult()
// Read metadata from the buildpack plan, often contributed by libcnb.Requires
// of the Detect phase
version := DefaultVersion
if v, ok := entry.Metadata["version"].(string); ok {
version = v
}
// Create a layer
layer, err := context.Layers.Layer("example")
if err != nil {
return result, err
}
layer.LayerTypes = libcnb.LayerTypes{
Launch: true,
Build: true,
Cache: true,
}
layer, err = populateLayer(layer, version)
if err != nil {
return result, nil
}
result.Layers = append(result.Layers, layer)
return result, nil
}
func ExampleBuild() {
detector := Detector{log.New(os.Stdout)}
builder := Builder{log.New(os.Stdout)}
libcnb.BuildpackMain(detector.Detect, builder.Build)
}

View File

@ -1,67 +0,0 @@
package examples
import (
"errors"
"os"
"path/filepath"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb/v2/log"
)
const (
Provides = "example"
BpExampleVersion = "BP_EXAMPLE_VERSION"
)
type Detector struct {
Logger log.Logger
}
func (Detector) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) {
version := "1.0"
// Scan the application source folder to see if the example buildpack is
// required. If `version.toml` does not exist we return a failed DetectResult
// but no runtime error has occurred, so we return an empty error.
versionPath := filepath.Join(context.ApplicationPath, "version.toml")
if _, err := os.Open(versionPath); errors.Is(err, os.ErrNotExist) {
return libcnb.DetectResult{}, nil
}
// Read the version number from the buildpack definition
if exampleVersion, exists := context.Buildpack.Metadata["version"]; exists {
version = exampleVersion.(string)
}
// Accept version number from the environment if the user provides it
if exampleVersion, exists := context.Platform.Environment[BpExampleVersion]; exists {
version = exampleVersion
}
metadata := map[string]interface{}{
"version": version,
}
return libcnb.DetectResult{
Pass: true,
Plans: []libcnb.BuildPlan{
{
// Let the system know that if other buildpacks Require "example"
// then this buildpack Provides the implementation logic.
Provides: []libcnb.BuildPlanProvide{
{Name: Provides},
},
// It is common for a buildpack to Require itself if the build phase
// needs information from the detect phase. Here we pass the version number
// as metadata to the build phase.
Requires: []libcnb.BuildPlanRequire{
{
Name: Provides,
Metadata: metadata,
},
},
},
},
}, nil
}
func ExampleDetect() {
detector := Detector{log.New(os.Stdout)}
libcnb.BuildpackMain(detector.Detect, nil)
}

View File

@ -1,30 +0,0 @@
package examples
import (
"fmt"
"os"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb/v2/log"
)
type Generator struct {
Logger log.Logger
}
func (Generator) Generate(context libcnb.GenerateContext) (libcnb.GenerateResult, error) {
// here you can read the context.ApplicationPath folder
// and create run.Dockerfile and build.Dockerfile in the context.OutputPath folder
// and read metadata from the context.Extension struct
// Just to use context to keep compiler happy =)
fmt.Println(context.Extension.Info.ID)
result := libcnb.NewGenerateResult()
return result, nil
}
func ExampleGenerate() {
generator := Generator{log.New(os.Stdout)}
libcnb.ExtensionMain(nil, generator.Generate)
}

View File

@ -21,7 +21,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/buildpacks/libcnb/v2/internal" "github.com/buildpacks/libcnb/internal"
) )
//go:generate mockery --name ExecD --case=underscore //go:generate mockery --name ExecD --case=underscore

View File

@ -24,8 +24,8 @@ import (
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/buildpacks/libcnb/v2" "github.com/buildpacks/libcnb"
"github.com/buildpacks/libcnb/v2/mocks" "github.com/buildpacks/libcnb/mocks"
) )
func testExecD(t *testing.T, _ spec.G, it spec.S) { func testExecD(t *testing.T, _ spec.G, it spec.S) {

View File

@ -1,59 +0,0 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb
// ExtensionInfo is information about the extension.
type ExtensionInfo struct {
// ID is the ID of the extension.
ID string `toml:"id"`
// Name is the name of the extension.
Name string `toml:"name"`
// Version is the version of the extension.
Version string `toml:"version"`
// Homepage is the homepage of the extension.
Homepage string `toml:"homepage"`
// Description is a string describing the extension.
Description string `toml:"description"`
// Keywords is a list of words that are associated with the extension.
Keywords []string `toml:"keywords"`
// Licenses a list of extension licenses.
Licenses []License `toml:"licenses"`
}
// Extension is the contents of the extension.toml file.
type Extension struct {
// API is the api version expected by the extension.
API string `toml:"api"`
// Info is information about the extension.
Info ExtensionInfo `toml:"extension"`
// Path is the path to the extension.
Path string `toml:"-"`
// Targets is the collection of targets supported by the buildpack.
Targets []Target `toml:"targets"`
// Metadata is arbitrary metadata attached to the extension.
Metadata map[string]interface{} `toml:"metadata"`
}

View File

@ -1,51 +0,0 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb_test
import (
"bytes"
"testing"
"github.com/BurntSushi/toml"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2"
. "github.com/onsi/gomega"
)
func testExtensionTOML(t *testing.T, _ spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
)
it("does not serialize the Path field", func() {
extn := libcnb.Extension{
API: "0.8",
Info: libcnb.ExtensionInfo{
ID: "test-buildpack/sample",
Name: "sample",
},
Path: "../buildpack",
}
output := &bytes.Buffer{}
Expect(toml.NewEncoder(output).Encode(extn)).To(Succeed())
Expect(output.String()).NotTo(Or(ContainSubstring("Path = "), ContainSubstring("path = ")))
})
}

59
formatter.go Normal file
View File

@ -0,0 +1,59 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb
import (
"fmt"
"github.com/buildpacks/libcnb/internal"
)
// ApplicationPathFormatter is the formatter for an ApplicationPath.
type ApplicationPathFormatter string
func (a ApplicationPathFormatter) String() string {
contents, err := internal.DirectoryContents{Path: string(a)}.Get()
if err != nil {
return fmt.Sprintf("Application contents: %s", err)
}
return fmt.Sprintf("Application contents: %s", contents)
}
// BuildpackPathFormatter is the formatter for a BuildpackPath.
type BuildpackPathFormatter string
func (b BuildpackPathFormatter) String() string {
contents, err := internal.DirectoryContents{Path: string(b)}.Get()
if err != nil {
return fmt.Sprintf("Buildpack contents: %s", err)
}
return fmt.Sprintf("Buildpack contents: %s", contents)
}
// PlatformFormatter is the formatter for a Platform.
type PlatformFormatter Platform
func (p PlatformFormatter) String() string {
contents, err := internal.DirectoryContents{Path: p.Path}.Get()
if err != nil {
return fmt.Sprintf("Platform contents: %s", err)
}
return fmt.Sprintf("Platform contents: %s", contents)
}

108
formatter_test.go Normal file
View File

@ -0,0 +1,108 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb_test
import (
"os"
"path/filepath"
"testing"
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb"
)
func testFormatter(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
)
context("ApplicationPathFormatter", func() {
var (
app string
)
it.Before(func() {
app = t.TempDir()
})
it("lists empty directory contents", func() {
Expect(libcnb.ApplicationPathFormatter(app).String()).To(Equal("Application contents: [.]"))
})
it("lists directory contents", func() {
f, err := os.Create(filepath.Join(app, "test-file"))
Expect(err).NotTo(HaveOccurred())
defer f.Close()
Expect(libcnb.ApplicationPathFormatter(app).String()).To(Equal("Application contents: [. test-file]"))
})
})
context("BuildpackPathFormatter", func() {
var (
bp string
)
it.Before(func() {
bp = t.TempDir()
})
it.After(func() {
Expect(os.RemoveAll(bp)).To(Succeed())
})
it("lists empty directory contents", func() {
Expect(libcnb.BuildpackPathFormatter(bp).String()).To(Equal("Buildpack contents: [.]"))
})
it("lists directory contents", func() {
f, err := os.Create(filepath.Join(bp, "test-file"))
Expect(err).NotTo(HaveOccurred())
defer f.Close()
Expect(libcnb.BuildpackPathFormatter(bp).String()).To(Equal("Buildpack contents: [. test-file]"))
})
})
context("PlatformFormatter", func() {
var (
plat libcnb.Platform
)
it.Before(func() {
plat.Path = t.TempDir()
})
it.After(func() {
Expect(os.RemoveAll(plat.Path)).To(Succeed())
})
it("lists empty directory contents", func() {
Expect(libcnb.PlatformFormatter(plat).String()).To(Equal("Platform contents: [.]"))
})
it("lists directory contents", func() {
f, err := os.Create(filepath.Join(plat.Path, "test-file"))
Expect(err).NotTo(HaveOccurred())
defer f.Close()
Expect(libcnb.PlatformFormatter(plat).String()).To(Equal("Platform contents: [. test-file]"))
})
})
}

View File

@ -1,261 +0,0 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/BurntSushi/toml"
"github.com/Masterminds/semver"
"github.com/buildpacks/libcnb/v2/internal"
"github.com/buildpacks/libcnb/v2/log"
)
// GenerateContext contains the inputs to generate.
type GenerateContext struct {
// ApplicationPath is the location of the application source code as provided by
// the lifecycle.
ApplicationPath string
// Extension is metadata about the extension, from extension.toml.
Extension Extension
// OutputDirectory is the location Dockerfiles should be written to.
OutputDirectory string
// Logger is the way to write messages to the end user
Logger log.Logger
// Plan is the buildpack plan provided to the buildpack.
Plan BuildpackPlan
// Platform is the contents of the platform.
Platform Platform
// TargetInfo contains info of the target (os, arch, ...).
TargetInfo TargetInfo
// TargetDistro is the target distribution (name, version).
TargetDistro TargetDistro
// Deprecated: StackID is the ID of the stack.
StackID string
}
// GenerateResult contains the results of detection.
type GenerateResult struct {
// Unmet contains buildpack plan entries that were not satisfied by the buildpack and therefore should be
// passed to subsequent providers.
Unmet []UnmetPlanEntry
RunDockerfile []byte
BuildDockerfile []byte
Config *ExtendConfig
}
// DockerfileArg is a Dockerfile argument
type DockerfileArg struct {
Name string `toml:"name"`
Value string `toml:"value"`
}
// BuildConfig contains additional arguments passed to the generated Dockerfiles
type BuildConfig struct {
Args []DockerfileArg `toml:"args"`
}
// ExtendConfig contains additional configuration for the Dockerfiles
type ExtendConfig struct {
Build BuildConfig `toml:"build"`
Run BuildConfig `toml:"run"`
}
// NewGenerateResult creates a new BuildResult instance, initializing empty fields.
func NewGenerateResult() GenerateResult {
return GenerateResult{}
}
func (b GenerateResult) String() string {
return fmt.Sprintf(
"{Unmet:%+v}",
b.Unmet,
)
}
// GenerateFunc takes a context and returns a result, performing extension generate behaviors.
type GenerateFunc func(context GenerateContext) (GenerateResult, error)
// Generate is called by the main function of a extension, for generate phase
func Generate(generate GenerateFunc, config Config) {
var (
err error
file string
ok bool
)
ctx := GenerateContext{Logger: config.logger}
ctx.ApplicationPath, err = os.Getwd()
if err != nil {
config.exitHandler.Error(fmt.Errorf("unable to get working directory\n%w", err))
return
}
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write("Application contents", ctx.ApplicationPath); err != nil {
config.logger.Debugf("unable to write application contents\n%w", err)
}
}
if s, ok := os.LookupEnv(EnvExtensionDirectory); ok {
ctx.Extension.Path = filepath.Clean(s)
} else {
config.exitHandler.Error(fmt.Errorf("unable to get CNB_EXTENSION_DIR, not found"))
return
}
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write("Extension contents", ctx.Extension.Path); err != nil {
config.logger.Debugf("unable to write extension contents\n%w", err)
}
}
file = filepath.Join(ctx.Extension.Path, "extension.toml")
if _, err = toml.DecodeFile(file, &ctx.Extension); err != nil && !os.IsNotExist(err) {
config.exitHandler.Error(fmt.Errorf("unable to decode extension %s\n%w", file, err))
return
}
config.logger.Debugf("Extension: %+v", ctx.Extension)
API, err := semver.NewVersion(ctx.Extension.API)
if err != nil {
config.exitHandler.Error(errors.New("version cannot be parsed"))
return
}
compatVersionCheck, _ := semver.NewConstraint(fmt.Sprintf(">= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
if !compatVersionCheck.Check(API) {
if MinSupportedBPVersion == MaxSupportedBPVersion {
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack API == %s", MinSupportedBPVersion))
return
}
config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion))
return
}
outputDir, ok := os.LookupEnv(EnvOutputDirectory)
if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_OUTPUT_DIR to be set"))
return
}
ctx.OutputDirectory = outputDir
ctx.Platform.Path, ok = os.LookupEnv(EnvPlatformDirectory)
if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_PLATFORM_DIR to be set"))
return
}
buildpackPlanPath, ok := os.LookupEnv(EnvBuildPlanPath)
if !ok {
config.exitHandler.Error(fmt.Errorf("expected CNB_BP_PLAN_PATH to be set"))
return
}
if config.logger.IsDebugEnabled() {
if err := config.contentWriter.Write("Platform contents", ctx.Platform.Path); err != nil {
config.logger.Debugf("unable to write platform contents\n%w", err)
}
}
if ctx.Platform.Bindings, err = NewBindings(ctx.Platform.Path); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform bindings %s\n%w", ctx.Platform.Path, err))
return
}
config.logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings)
file = filepath.Join(ctx.Platform.Path, "env")
if ctx.Platform.Environment, err = internal.NewConfigMapFromPath(file); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to read platform environment %s\n%w", file, err))
return
}
config.logger.Debugf("Platform Environment: %s", ctx.Platform.Environment)
if _, err = toml.DecodeFile(buildpackPlanPath, &ctx.Plan); err != nil && !os.IsNotExist(err) {
config.exitHandler.Error(fmt.Errorf("unable to decode buildpack plan %s\n%w", buildpackPlanPath, err))
return
}
config.logger.Debugf("Buildpack Plan: %+v", ctx.Plan)
if ctx.StackID, ok = os.LookupEnv(EnvStackID); !ok {
config.logger.Debug("CNB_STACK_ID not set")
} else {
config.logger.Debugf("Stack: %s", ctx.StackID)
}
if API.GreaterThan(semver.MustParse("0.9")) {
ctx.TargetInfo = TargetInfo{}
ctx.TargetInfo.OS, _ = os.LookupEnv(EnvTargetOS)
ctx.TargetInfo.Arch, _ = os.LookupEnv(EnvTargetArch)
ctx.TargetInfo.Variant, _ = os.LookupEnv(EnvTargetArchVariant)
config.logger.Debugf("System: %+v", ctx.TargetInfo)
ctx.TargetDistro = TargetDistro{}
ctx.TargetDistro.Name, _ = os.LookupEnv(EnvTargetDistroName)
ctx.TargetDistro.Version, _ = os.LookupEnv(EnvTargetDistroVersion)
config.logger.Debugf("Distro: %+v", ctx.TargetDistro)
}
result, err := generate(ctx)
if err != nil {
config.exitHandler.Error(err)
return
}
config.logger.Debugf("Result: %+v", result)
if len(result.RunDockerfile) > 0 {
//nolint:gosec
if err := os.WriteFile(filepath.Join(ctx.OutputDirectory, "run.Dockerfile"), result.RunDockerfile, 0644); err != nil {
config.exitHandler.Error(err)
return
}
}
if len(result.BuildDockerfile) > 0 {
//nolint:gosec
if err := os.WriteFile(filepath.Join(ctx.OutputDirectory, "build.Dockerfile"), result.BuildDockerfile, 0644); err != nil {
config.exitHandler.Error(err)
return
}
}
if result.Config != nil {
configFile, err := os.Create(filepath.Join(ctx.OutputDirectory, "extend-config.toml"))
if err != nil {
config.exitHandler.Error(err)
return
}
if err := toml.NewEncoder(configFile).Encode(result.Config); err != nil {
config.exitHandler.Error(err)
return
}
}
}

View File

@ -1,458 +0,0 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb_test
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"testing"
"text/template"
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/stretchr/testify/mock"
"github.com/buildpacks/libcnb/v2"
"github.com/buildpacks/libcnb/v2/log"
"github.com/buildpacks/libcnb/v2/mocks"
)
func testGenerate(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
generateFunc libcnb.GenerateFunc
applicationPath string
extensionPath string
outputPath string
buildpackPlanPath string
extnTOMLContents string
commandPath string
environmentWriter *mocks.EnvironmentWriter
exitHandler *mocks.ExitHandler
platformPath string
tomlWriter *mocks.TOMLWriter
extensionTOML *template.Template
workingDir string
)
it.Before(func() {
generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) {
return libcnb.NewGenerateResult(), nil
}
var err error
applicationPath, err = os.MkdirTemp("", "generate-application-path")
Expect(err).NotTo(HaveOccurred())
applicationPath, err = filepath.EvalSymlinks(applicationPath)
Expect(err).NotTo(HaveOccurred())
extensionPath, err = os.MkdirTemp("", "generate-extension-path")
Expect(err).NotTo(HaveOccurred())
Expect(os.Setenv("CNB_EXTENSION_DIR", extensionPath)).To(Succeed())
extnTOMLContents = `
api = "{{.APIVersion}}"
[extension]
id = "test-id"
name = "test-name"
version = "1.1.1"
description = "A test extension"
keywords = ["test", "extension"]
[[extension.licenses]]
type = "Apache-2.0"
uri = "https://spdx.org/licenses/Apache-2.0.html"
[[extension.licenses]]
type = "Apache-1.1"
uri = "https://spdx.org/licenses/Apache-1.1.html"
[metadata]
test-key = "test-value"
`
extensionTOML, err = template.New("extension.toml").Parse(extnTOMLContents)
Expect(err).ToNot(HaveOccurred())
var b bytes.Buffer
err = extensionTOML.Execute(&b, map[string]string{"APIVersion": "0.8"})
Expect(err).ToNot(HaveOccurred())
Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"), b.Bytes(), 0600)).To(Succeed())
f, err := os.CreateTemp("", "generate-buildpackplan-path")
Expect(err).NotTo(HaveOccurred())
Expect(f.Close()).NotTo(HaveOccurred())
buildpackPlanPath = f.Name()
Expect(os.WriteFile(buildpackPlanPath,
[]byte(`
[[entries]]
name = "test-name"
version = "test-version"
[entries.metadata]
test-key = "test-value"
`),
0600),
).To(Succeed())
commandPath = filepath.Join("bin", "generate")
environmentWriter = &mocks.EnvironmentWriter{}
environmentWriter.On("Write", mock.Anything, mock.Anything).Return(nil)
exitHandler = &mocks.ExitHandler{}
exitHandler.On("Error", mock.Anything)
platformPath, err = os.MkdirTemp("", "generate-platform-path")
Expect(err).NotTo(HaveOccurred())
Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(platformPath, "bindings", "alpha", "test-secret-key"),
[]byte("test-secret-value"), 0600)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(platformPath, "env"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(platformPath, "env", "TEST_ENV"), []byte("test-value"), 0600)).
To(Succeed())
tomlWriter = &mocks.TOMLWriter{}
tomlWriter.On("Write", mock.Anything, mock.Anything).Return(nil)
outputPath, err = os.MkdirTemp("", "generate-output-path")
Expect(err).NotTo(HaveOccurred())
Expect(os.Setenv("CNB_OUTPUT_DIR", outputPath)).To(Succeed())
Expect(os.Setenv("CNB_STACK_ID", "test-stack-id")).To(Succeed())
Expect(os.Setenv("CNB_PLATFORM_DIR", platformPath)).To(Succeed())
Expect(os.Setenv("CNB_BP_PLAN_PATH", buildpackPlanPath)).To(Succeed())
Expect(os.Setenv("CNB_TARGET_OS", "linux")).To(Succeed())
Expect(os.Setenv("CNB_TARGET_ARCH", "arm")).To(Succeed())
Expect(os.Setenv("CNB_TARGET_ARCH_VARIANT", "v6")).To(Succeed())
Expect(os.Setenv("CNB_TARGET_DISTRO_NAME", "ubuntu")).To(Succeed())
Expect(os.Setenv("CNB_TARGET_DISTRO_VERSION", "24.04")).To(Succeed())
workingDir, err = os.Getwd()
Expect(err).NotTo(HaveOccurred())
Expect(os.Chdir(applicationPath)).To(Succeed())
})
it.After(func() {
Expect(os.Chdir(workingDir)).To(Succeed())
Expect(os.Unsetenv("CNB_EXTENSION_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed())
Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed())
Expect(os.Unsetenv("CNB_OUTPUT_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_TARGET_OS"))
Expect(os.Unsetenv("CNB_TARGET_ARCH"))
Expect(os.Unsetenv("CNB_TARGET_ARCH_VARIANT"))
Expect(os.Unsetenv("CNB_TARGET_DISTRO_NAME"))
Expect(os.Unsetenv("CNB_TARGET_DISTRO_VERSION"))
Expect(os.RemoveAll(applicationPath)).To(Succeed())
Expect(os.RemoveAll(extensionPath)).To(Succeed())
Expect(os.RemoveAll(buildpackPlanPath)).To(Succeed())
Expect(os.RemoveAll(outputPath)).To(Succeed())
Expect(os.RemoveAll(platformPath)).To(Succeed())
})
context("buildpack API is not within the supported range", func() {
it.Before(func() {
Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"),
[]byte(`
api = "0.7"
[extension]
id = "test-id"
name = "test-name"
version = "1.1.1"
`),
0600),
).To(Succeed())
})
it("fails", func() {
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, outputPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard())),
)
if libcnb.MinSupportedBPVersion == libcnb.MaxSupportedBPVersion {
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
fmt.Sprintf("this version of libcnb is only compatible with buildpack API == %s", libcnb.MinSupportedBPVersion)))
} else {
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
fmt.Sprintf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", libcnb.MinSupportedBPVersion, libcnb.MaxSupportedBPVersion),
))
}
})
})
context("errors if required env vars are not set", func() {
for _, e := range []string{"CNB_OUTPUT_DIR", "CNB_PLATFORM_DIR", "CNB_BP_PLAN_PATH"} {
// We need to do this assignment because of the way that spec binds variables
envVar := e
context(fmt.Sprintf("when %s is unset", envVar), func() {
it.Before(func() {
Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"),
[]byte(`
api = "0.8"
[extension]
id = "test-id"
name = "test-name"
version = "1.1.1"
`),
0600),
).To(Succeed())
os.Unsetenv(envVar)
})
it("fails", func() {
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath}),
libcnb.WithExitHandler(exitHandler)),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
fmt.Sprintf("expected %s to be set", envVar),
))
})
})
}
})
context("has a build environment", func() {
var ctx libcnb.GenerateContext
it.Before(func() {
Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"),
[]byte(`
api = "0.8"
[extension]
id = "test-id"
name = "test-name"
version = "1.1.1"
`),
0600),
).To(Succeed())
generateFunc = func(context libcnb.GenerateContext) (libcnb.GenerateResult, error) {
ctx = context
return libcnb.NewGenerateResult(), nil
}
})
it("creates context", func() {
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath})),
)
Expect(ctx.ApplicationPath).To(Equal(applicationPath))
Expect(ctx.Extension).To(Equal(libcnb.Extension{
API: "0.8",
Info: libcnb.ExtensionInfo{
ID: "test-id",
Name: "test-name",
Version: "1.1.1",
},
Path: extensionPath,
}))
Expect(ctx.OutputDirectory).To(Equal(outputPath))
Expect(ctx.Plan).To(Equal(libcnb.BuildpackPlan{
Entries: []libcnb.BuildpackPlanEntry{
{
Name: "test-name",
Metadata: map[string]interface{}{
"test-key": "test-value",
},
},
},
}))
Expect(ctx.Platform).To(Equal(libcnb.Platform{
Bindings: libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(platformPath, "bindings", "alpha"),
Secret: map[string]string{
"test-secret-key": "test-secret-value",
},
},
},
Environment: map[string]string{"TEST_ENV": "test-value"},
Path: platformPath,
}))
Expect(ctx.StackID).To(Equal("test-stack-id"))
})
})
context("has a build environment specifying target metadata", func() {
var ctx libcnb.GenerateContext
it.Before(func() {
Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"),
[]byte(`
api = "0.10"
[extension]
id = "test-id"
name = "test-name"
version = "1.1.1"
[[targets]]
os = "linux"
arch = "amd64"
[[targets.distros]]
name = "ubuntu"
version = "18.04"
[[targets.distros]]
name = "debian"
[[targets]]
os = "linux"
arch = "arm"
variant = "v6"
`), 0600),
).To(Succeed())
generateFunc = func(context libcnb.GenerateContext) (libcnb.GenerateResult, error) {
ctx = context
return libcnb.NewGenerateResult(), nil
}
})
it("provides target information", func() {
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath}),
libcnb.WithLogger(log.New(os.Stdout)),
),
)
Expect(ctx.Extension.Targets).To(HaveLen(2))
Expect(ctx.Extension.Targets[0].OS).To(Equal("linux"))
Expect(ctx.Extension.Targets[0].Arch).To(Equal("amd64"))
Expect(ctx.Extension.Targets[0].Distros).To(HaveLen(2))
Expect(ctx.Extension.Targets[0].Distros[0].Name).To(Equal("ubuntu"))
Expect(ctx.Extension.Targets[0].Distros[0].Version).To(Equal("18.04"))
Expect(ctx.Extension.Targets[0].Distros[1].Name).To(Equal("debian"))
Expect(ctx.Extension.Targets[1].Variant).To(Equal("v6"))
Expect(ctx.TargetInfo.OS).To(Equal("linux"))
Expect(ctx.TargetInfo.Arch).To(Equal("arm"))
Expect(ctx.TargetInfo.Variant).To(Equal("v6"))
Expect(ctx.TargetDistro.Name).To(Equal("ubuntu"))
Expect(ctx.TargetDistro.Version).To(Equal("24.04"))
})
})
it("fails if CNB_EXTENSION_DIR is not set", func() {
Expect(os.Unsetenv("CNB_EXTENSION_DIR")).To(Succeed())
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{filepath.Join(extensionPath, commandPath), outputPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard())),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to get CNB_EXTENSION_DIR, not found"))
})
it("handles error from GenerateFunc", func() {
generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) {
return libcnb.NewGenerateResult(), errors.New("test-error")
}
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, outputPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard())),
)
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("test-error"))
})
it("writes Dockerfiles", func() {
generateFunc = func(_ libcnb.GenerateContext) (libcnb.GenerateResult, error) {
result := libcnb.NewGenerateResult()
result.BuildDockerfile = []byte(`FROM foo:latest`)
result.RunDockerfile = []byte(`FROM bar:latest`)
return result, nil
}
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, outputPath, platformPath, buildpackPlanPath}),
libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithLogger(log.NewDiscard())),
)
Expect(filepath.Join(outputPath, "build.Dockerfile")).To(BeARegularFile())
Expect(filepath.Join(outputPath, "run.Dockerfile")).To(BeARegularFile())
})
it("writes extend-config.toml", func() {
generateFunc = func(_ libcnb.GenerateContext) (libcnb.GenerateResult, error) {
result := libcnb.NewGenerateResult()
result.Config = &libcnb.ExtendConfig{
Build: libcnb.BuildConfig{
Args: []libcnb.DockerfileArg{
{
Name: "foo",
Value: "bar",
},
},
},
Run: libcnb.BuildConfig{
Args: []libcnb.DockerfileArg{
{
Name: "bar",
Value: "bazz",
},
},
},
}
return result, nil
}
libcnb.Generate(generateFunc,
libcnb.NewConfig(
libcnb.WithArguments([]string{commandPath, outputPath, platformPath, buildpackPlanPath}),
libcnb.WithTOMLWriter(tomlWriter),
libcnb.WithLogger(log.NewDiscard())),
)
Expect(filepath.Join(outputPath, "extend-config.toml")).To(BeARegularFile())
})
}

21
go.mod
View File

@ -1,24 +1,21 @@
module github.com/buildpacks/libcnb/v2 module github.com/buildpacks/libcnb
go 1.24 go 1.20
require ( require (
github.com/BurntSushi/toml v1.5.0 github.com/BurntSushi/toml v1.3.2
github.com/CycloneDX/cyclonedx-go v0.9.2 github.com/Masterminds/semver/v3 v3.2.1
github.com/Masterminds/semver v1.5.0 github.com/onsi/gomega v1.33.0
github.com/onsi/gomega v1.37.0
github.com/sclevine/spec v1.4.0 github.com/sclevine/spec v1.4.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.9.0
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/net v0.41.0 // indirect golang.org/x/net v0.24.0 // indirect
golang.org/x/text v0.26.0 // indirect golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

68
go.sum
View File

@ -1,58 +1,32 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -6,8 +6,7 @@ linters:
enable: enable:
- bodyclose - bodyclose
- dogsled - dogsled
- errcheck - exportloopref
- copyloopvar
- gocritic - gocritic
- goimports - goimports
- gosec - gosec
@ -25,9 +24,5 @@ linters:
- whitespace - whitespace
linters-settings: linters-settings:
revive:
rules:
- name: dot-imports
disabled: true
goimports: goimports:
local-prefixes: github.com/buildpacks/libcnb/v2 local-prefixes: github.com/buildpacks/libcnb

View File

@ -27,13 +27,12 @@ func TestUnit(t *testing.T) {
suite := spec.New("libcnb", spec.Report(report.Terminal{})) suite := spec.New("libcnb", spec.Report(report.Terminal{}))
suite("Build", testBuild) suite("Build", testBuild)
suite("Detect", testDetect) suite("Detect", testDetect)
suite("Generate", testGenerate)
suite("Environment", testEnvironment) suite("Environment", testEnvironment)
suite("Formatter", testFormatter)
suite("Layer", testLayer) suite("Layer", testLayer)
suite("Main", testMain) suite("Main", testMain)
suite("Platform", testPlatform) suite("Platform", testPlatform)
suite("ExecD", testExecD) suite("ExecD", testExecD)
suite("BuildpackTOML", testBuildpackTOML) suite("BuildpackTOML", testBuildpackTOML)
suite("ExtensionTOML", testExtensionTOML)
suite.Run(t) suite.Run(t)
} }

View File

@ -24,7 +24,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal" "github.com/buildpacks/libcnb/internal"
) )
func testConfigMap(t *testing.T, _ spec.G, it spec.S) { func testConfigMap(t *testing.T, _ spec.G, it spec.S) {
@ -35,13 +35,7 @@ func testConfigMap(t *testing.T, _ spec.G, it spec.S) {
) )
it.Before(func() { it.Before(func() {
var err error path = t.TempDir()
path, err = os.MkdirTemp("", "config-map")
Expect(err).NotTo(HaveOccurred())
})
it.After(func() {
Expect(os.RemoveAll(path)).To(Succeed())
}) })
it("returns an empty ConfigMap when directory does not exist", func() { it("returns an empty ConfigMap when directory does not exist", func() {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2018-2022 the original author or authors. * Copyright 2018-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,53 +18,36 @@ package internal
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"github.com/buildpacks/libcnb/v2/log"
) )
// DirectoryContentsWriter is used write the contents of a directory to the given io.Writer // DirectoryContents is used to generate a collection of the names of all files within a directory.
type DirectoryContentsWriter struct { type DirectoryContents struct {
format log.DirectoryContentFormatter Path string
writer io.Writer
} }
// NewDirectoryContentsWriter returns a new DirectoryContentsWriter initialized and ready to be used // Get returns the names of all files within a directory
func NewDirectoryContentsWriter(format log.DirectoryContentFormatter, writer io.Writer) DirectoryContentsWriter { func (d DirectoryContents) Get() ([]string, error) {
return DirectoryContentsWriter{ var contents []string
format: format,
writer: writer,
}
}
// Write all the file contents to the writer if err := filepath.Walk(d.Path, func(path string, info os.FileInfo, err error) error {
func (d DirectoryContentsWriter) Write(title, path string) error {
d.format.RootPath(path)
if _, err := d.writer.Write([]byte(d.format.Title(title))); err != nil {
return fmt.Errorf("unable to write title\n%w", err)
}
if err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
msg, err := d.format.File(path, info) rel, err := filepath.Rel(d.Path, path)
if err != nil { if err != nil {
return fmt.Errorf("unable to format\n%w", err) return fmt.Errorf("unable to calculate relative path %s -> %s\n%w", d.Path, path, err)
}
if _, err := d.writer.Write([]byte(msg)); err != nil {
return fmt.Errorf("unable to write\n%w", err)
} }
contents = append(contents, rel)
return nil return nil
}); err != nil { }); err != nil {
return fmt.Errorf("error walking path %s\n%w", path, err) return nil, fmt.Errorf("error walking path %s\n%w", d.Path, err)
} }
return nil sort.Strings(contents)
return contents, nil
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2018-2022 the original author or authors. * Copyright 2018-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,8 +17,6 @@
package internal_test package internal_test
import ( import (
"bytes"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -26,53 +24,22 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal" "github.com/buildpacks/libcnb/internal"
) )
func testDirectoryContentsWriter(t *testing.T, context spec.G, it spec.S) { func testDirectoryContents(t *testing.T, _ spec.G, it spec.S) {
var ( var (
Expect = NewWithT(t).Expect Expect = NewWithT(t).Expect
path string path string
buf bytes.Buffer
) )
it.Before(func() { it.Before(func() {
var err error path = t.TempDir()
path, err = os.MkdirTemp("", "directory-contents")
Expect(err).NotTo(HaveOccurred())
})
it.After(func() {
Expect(os.RemoveAll(path)).To(Succeed())
})
context("directory content formats", func() {
fm := internal.NewPlainDirectoryContentFormatter()
it("formats title", func() {
Expect(fm.Title("foo")).To(Equal("foo:\n"))
})
it("formats a file", func() {
cwd, err := os.Getwd()
Expect(err).ToNot(HaveOccurred())
info, err := os.Stat(cwd)
Expect(err).ToNot(HaveOccurred())
fm.RootPath(filepath.Dir(cwd))
Expect(fm.File(cwd, info)).To(Equal(fmt.Sprintf("%s\n", filepath.Base(cwd))))
})
}) })
it("lists empty directory contents", func() { it("lists empty directory contents", func() {
fm := internal.NewPlainDirectoryContentFormatter() Expect(internal.DirectoryContents{path}.Get()).To(Equal([]string{"."}))
dc := internal.NewDirectoryContentsWriter(fm, &buf)
Expect(dc.Write("title", path)).To(Succeed())
Expect(buf.String()).To(Equal("title:\n.\n"))
}) })
it("lists directory contents", func() { it("lists directory contents", func() {
@ -80,10 +47,6 @@ func testDirectoryContentsWriter(t *testing.T, context spec.G, it spec.S) {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
defer f.Close() defer f.Close()
fm := internal.NewPlainDirectoryContentFormatter() Expect(internal.DirectoryContents{path}.Get()).To(Equal([]string{".", "test-file"}))
dc := internal.NewDirectoryContentsWriter(fm, &buf)
Expect(dc.Write("title", path)).To(Succeed())
Expect(buf.String()).To(Equal("title:\n.\ntest-file\n"))
}) })
} }

View File

@ -43,7 +43,7 @@ func (w EnvironmentWriter) Write(path string, environment map[string]string) err
return fmt.Errorf("unable to mkdir from key %s\n%w", filepath.Dir(f), err) return fmt.Errorf("unable to mkdir from key %s\n%w", filepath.Dir(f), err)
} }
//nolint:gosec // #nosec
if err := os.WriteFile(f, []byte(value), 0644); err != nil { if err := os.WriteFile(f, []byte(value), 0644); err != nil {
return fmt.Errorf("unable to write file %s\n%w", f, err) return fmt.Errorf("unable to write file %s\n%w", f, err)
} }

View File

@ -24,7 +24,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal" "github.com/buildpacks/libcnb/internal"
) )
func testEnvironmentWriter(t *testing.T, _ spec.G, it spec.S) { func testEnvironmentWriter(t *testing.T, _ spec.G, it spec.S) {
@ -36,13 +36,7 @@ func testEnvironmentWriter(t *testing.T, _ spec.G, it spec.S) {
) )
it.Before(func() { it.Before(func() {
var err error path = t.TempDir()
path, err = os.MkdirTemp("", "environment-writer")
Expect(err).NotTo(HaveOccurred())
Expect(os.RemoveAll(path)).To(Succeed())
})
it.After(func() {
Expect(os.RemoveAll(path)).To(Succeed()) Expect(os.RemoveAll(path)).To(Succeed())
}) })

View File

@ -23,7 +23,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal" "github.com/buildpacks/libcnb/internal"
) )
func testExecDWriter(t *testing.T, _ spec.G, it spec.S) { func testExecDWriter(t *testing.T, _ spec.G, it spec.S) {

View File

@ -24,7 +24,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal" "github.com/buildpacks/libcnb/internal"
) )
func testExitHandler(t *testing.T, _ spec.G, it spec.S) { func testExitHandler(t *testing.T, _ spec.G, it spec.S) {

View File

@ -1,38 +0,0 @@
package internal
import (
"fmt"
"os"
"path/filepath"
)
type PlainDirectoryContentFormatter struct {
rootPath string
}
// NewPlainDirectoryContentFormatter returns a formatter applies no formatting
//
// The returned formatter operates as such:
//
// Title -> returns string followed by `:\n`
// File -> returns file name relative to the root followed by `\n`
func NewPlainDirectoryContentFormatter() *PlainDirectoryContentFormatter {
return &PlainDirectoryContentFormatter{}
}
func (p *PlainDirectoryContentFormatter) File(path string, _ os.FileInfo) (string, error) {
rel, err := filepath.Rel(p.rootPath, path)
if err != nil {
return "", fmt.Errorf("unable to calculate relative path %s -> %s\n%w", p.rootPath, path, err)
}
return fmt.Sprintf("%s\n", rel), nil
}
func (p *PlainDirectoryContentFormatter) RootPath(path string) {
p.rootPath = path
}
func (p *PlainDirectoryContentFormatter) Title(title string) string {
return fmt.Sprintf("%s:\n", title)
}

View File

@ -1,67 +0,0 @@
/*
* Copyright 2018-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package internal_test
import (
"fmt"
"os"
"path/filepath"
"testing"
. "github.com/onsi/gomega"
"github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal"
)
func testFormatters(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
path string
)
it.Before(func() {
var err error
path, err = os.MkdirTemp("", "directory-contents")
Expect(err).NotTo(HaveOccurred())
})
it.After(func() {
Expect(os.RemoveAll(path)).To(Succeed())
})
context("directory content formats", func() {
fm := internal.NewPlainDirectoryContentFormatter()
it("formats title", func() {
Expect(fm.Title("foo")).To(Equal("foo:\n"))
})
it("formats a file", func() {
cwd, err := os.Getwd()
Expect(err).ToNot(HaveOccurred())
info, err := os.Stat(cwd)
Expect(err).ToNot(HaveOccurred())
fm.RootPath(filepath.Dir(cwd))
Expect(fm.File(cwd, info)).To(Equal(fmt.Sprintf("%s\n", filepath.Base(cwd))))
})
})
}

View File

@ -26,11 +26,10 @@ import (
func TestUnit(t *testing.T) { func TestUnit(t *testing.T) {
suite := spec.New("libcnb/internal", spec.Report(report.Terminal{})) suite := spec.New("libcnb/internal", spec.Report(report.Terminal{}))
suite("ConfigMap", testConfigMap) suite("ConfigMap", testConfigMap)
suite("DirectoryContents", testDirectoryContentsWriter) suite("DirectoryContents", testDirectoryContents)
suite("EnvironmentWriter", testEnvironmentWriter) suite("EnvironmentWriter", testEnvironmentWriter)
suite("ExitHandler", testExitHandler) suite("ExitHandler", testExitHandler)
suite("TOMLWriter", testTOMLWriter) suite("TOMLWriter", testTOMLWriter)
suite("ExecDWriter", testExecDWriter) suite("ExecDWriter", testExecDWriter)
suite("Formatters", testFormatters)
suite.Run(t) suite.Run(t)
} }

18
internal/layer_api_5.go Normal file
View File

@ -0,0 +1,18 @@
package internal
// LayerAPI5 is used for backwards compatibility with serialization/deserialization of the layer toml
type LayerAPI5 struct {
// bool's are inlined to instead of using libcnb.LayerTypes to break a circular package reference, internal -> libcnb -> internal -> ...
// Build indicates that a layer should be used for builds.
Build bool `toml:"build"`
// Cache indicates that a layer should be cached.
Cache bool `toml:"cache"`
// Launch indicates that a layer should be used for launch.
Launch bool `toml:"launch"`
// Metadata is the metadata associated with the layer.
Metadata map[string]interface{} `toml:"metadata"`
}

View File

@ -24,7 +24,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/internal" "github.com/buildpacks/libcnb/internal"
) )
func testTOMLWriter(t *testing.T, _ spec.G, it spec.S) { func testTOMLWriter(t *testing.T, _ spec.G, it spec.S) {
@ -37,17 +37,10 @@ func testTOMLWriter(t *testing.T, _ spec.G, it spec.S) {
) )
it.Before(func() { it.Before(func() {
var err error parent = t.TempDir()
parent, err = os.MkdirTemp("", "toml-writer")
Expect(err).NotTo(HaveOccurred())
path = filepath.Join(parent, "text.toml") path = filepath.Join(parent, "text.toml")
}) })
it.After(func() {
Expect(os.RemoveAll(parent)).To(Succeed())
})
it("writes the contents of a given object out to a .toml file", func() { it("writes the contents of a given object out to a .toml file", func() {
err := tomlWriter.Write(path, map[string]string{ err := tomlWriter.Write(path, map[string]string{
"some-field": "some-value", "some-field": "some-value",

View File

@ -1,26 +0,0 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb
// Label represents an image label.
type Label struct {
// Key is the key of the label.
Key string `toml:"key"`
// Value is the value of the label.
Value string `toml:"value"`
}

View File

@ -1,33 +0,0 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb
// LaunchTOML represents the contents of launch.toml.
type LaunchTOML struct {
// Labels is the collection of image labels contributed by the buildpack.
Labels []Label `toml:"labels"`
// Processes is the collection of process types contributed by the buildpack.
Processes []Process `toml:"processes"`
// Slices is the collection of slices contributed by the buildpack.
Slices []Slice `toml:"slices"`
}
func (l LaunchTOML) isEmpty() bool {
return len(l.Labels) == 0 && len(l.Processes) == 0 && len(l.Slices) == 0
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2018-2024 the original author or authors. * Copyright 2018-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,6 +22,8 @@ import (
"path/filepath" "path/filepath"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/buildpacks/libcnb/internal"
) )
const ( const (
@ -50,6 +52,31 @@ func (e Exec) ProcessFilePath(processType string, name string) string {
return filepath.Join(e.Path, processType, name) return filepath.Join(e.Path, processType, name)
} }
// Profile is the collection of values to be written into profile.d
type Profile map[string]string
// Add formats using the default formats for its operands and adds an entry for a .profile.d file. Spaces are added
// between operands when neither is a string.
func (p Profile) Add(name string, a ...interface{}) {
p[name] = fmt.Sprint(a...)
}
// Addf formats according to a format specifier and adds an entry for a .profile.d file.
func (p Profile) Addf(name string, format string, a ...interface{}) {
p[name] = fmt.Sprintf(format, a...)
}
// ProcessAdd formats using the default formats for its operands and adds an entry for a .profile.d file. Spaces are
// added between operands when neither is a string.
func (p Profile) ProcessAdd(processType string, name string, a ...interface{}) {
p.Add(filepath.Join(processType, name), a...)
}
// ProcessAddf formats according to a format specifier and adds an entry for a .profile.d file.
func (p Profile) ProcessAddf(processType string, name string, format string, a ...interface{}) {
p.Addf(filepath.Join(processType, name), format, a...)
}
// BOMFormat indicates the format of the SBOM entry // BOMFormat indicates the format of the SBOM entry
type SBOMFormat int type SBOMFormat int
@ -89,7 +116,7 @@ func SBOMFormatFromString(from string) (SBOMFormat, error) {
return UnknownFormat, fmt.Errorf("unable to translate from %s to SBOMFormat", from) return UnknownFormat, fmt.Errorf("unable to translate from %s to SBOMFormat", from)
} }
// Contribute represents a layer managed by the buildpack. // Layer represents a layer managed by the buildpack.
type Layer struct { type Layer struct {
// LayerTypes indicates the type of layer // LayerTypes indicates the type of layer
LayerTypes `toml:"types"` LayerTypes `toml:"types"`
@ -112,36 +139,13 @@ type Layer struct {
// SharedEnvironment are the environment variables set at both build and launch times. // SharedEnvironment are the environment variables set at both build and launch times.
SharedEnvironment Environment `toml:"-"` SharedEnvironment Environment `toml:"-"`
// Profile is the profile.d scripts set in the layer.
Profile Profile `toml:"-"`
// Exec is the exec.d executables set in the layer. // Exec is the exec.d executables set in the layer.
Exec Exec `toml:"-"` Exec Exec `toml:"-"`
} }
func (l Layer) Reset() (Layer, error) {
l.LayerTypes = LayerTypes{
Build: false,
Launch: false,
Cache: false,
}
l.SharedEnvironment = Environment{}
l.BuildEnvironment = Environment{}
l.LaunchEnvironment = Environment{}
l.Metadata = nil
err := os.RemoveAll(l.Path)
if err != nil {
return Layer{}, fmt.Errorf("error could not remove file: %s", err)
}
err = os.MkdirAll(l.Path, os.ModePerm)
if err != nil {
return Layer{}, fmt.Errorf("error could not create directory: %s", err)
}
return l, nil
}
// SBOMPath returns the path to the layer specific SBOM File
func (l Layer) SBOMPath(bt SBOMFormat) string { func (l Layer) SBOMPath(bt SBOMFormat) string {
return filepath.Join(filepath.Dir(l.Path), fmt.Sprintf("%s.sbom.%s", l.Name, bt)) return filepath.Join(filepath.Dir(l.Path), fmt.Sprintf("%s.sbom.%s", l.Name, bt))
} }
@ -159,6 +163,17 @@ type LayerTypes struct {
Launch bool `toml:"launch"` Launch bool `toml:"launch"`
} }
//go:generate mockery --name LayerContributor --case=underscore
// LayerContributor is an interface for types that create layers.
type LayerContributor interface {
// Contribute accepts a layer and transforms it, returning a layer.
Contribute(layer Layer) (Layer, error)
// Name is the name of the layer.
Name() string
}
// Layers represents the layers part of the specification. // Layers represents the layers part of the specification.
type Layers struct { type Layers struct {
// Path is the layers filesystem location. // Path is the layers filesystem location.
@ -173,6 +188,7 @@ func (l *Layers) Layer(name string) (Layer, error) {
BuildEnvironment: Environment{}, BuildEnvironment: Environment{},
LaunchEnvironment: Environment{}, LaunchEnvironment: Environment{},
SharedEnvironment: Environment{}, SharedEnvironment: Environment{},
Profile: Profile{},
Exec: Exec{Path: filepath.Join(l.Path, name, "exec.d")}, Exec: Exec{Path: filepath.Join(l.Path, name, "exec.d")},
} }
@ -181,6 +197,18 @@ func (l *Layers) Layer(name string) (Layer, error) {
return Layer{}, fmt.Errorf("unable to decode layer metadata %s\n%w", f, err) return Layer{}, fmt.Errorf("unable to decode layer metadata %s\n%w", f, err)
} }
if !layer.Build && !layer.Cache && !layer.Launch {
// if all three are false, that could mean we have a API <= 0.5 TOML file
// try parsing the <= 0.5 API format where these were top level attributes
buf := internal.LayerAPI5{}
if _, err := toml.DecodeFile(f, &buf); err != nil && !os.IsNotExist(err) {
return Layer{}, fmt.Errorf("unable to decode layer metadata %s\n%w", f, err)
}
layer.Build = buf.Build
layer.Cache = buf.Cache
layer.Launch = buf.Launch
}
return layer, nil return layer, nil
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2018-2024 the original author or authors. * Copyright 2018-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,7 +24,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2" "github.com/buildpacks/libcnb"
) )
func testLayer(t *testing.T, context spec.G, it spec.S) { func testLayer(t *testing.T, context spec.G, it spec.S) {
@ -52,154 +52,37 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
}) })
}) })
context("Reset", func() { context("Profile", func() {
var layer libcnb.Layer var profile libcnb.Profile
it.Before(func() { it.Before(func() {
layers = libcnb.Layers{Path: t.TempDir()} profile = libcnb.Profile{}
}) })
context("when there is no previous build", func() { it("adds content", func() {
it.Before(func() { profile.Add("test-name", "test-value")
layer = libcnb.Layer{ Expect(profile).To(Equal(libcnb.Profile{"test-name": "test-value"}))
Name: "test-name",
Path: filepath.Join(layers.Path, "test-name"),
LayerTypes: libcnb.LayerTypes{
Launch: true,
Build: true,
Cache: true,
},
}
})
it("initializes an empty layer", func() {
var err error
layer, err = layer.Reset()
Expect(err).NotTo(HaveOccurred())
Expect(layer).To(Equal(libcnb.Layer{
Name: "test-name",
Path: filepath.Join(layers.Path, "test-name"),
LayerTypes: libcnb.LayerTypes{
Launch: false,
Build: false,
Cache: false,
},
SharedEnvironment: libcnb.Environment{},
BuildEnvironment: libcnb.Environment{},
LaunchEnvironment: libcnb.Environment{},
}))
Expect(filepath.Join(layers.Path, "test-name")).To(BeADirectory())
})
}) })
context("when cache is retrieved from previous build", func() { it("adds formatted content", func() {
it.Before(func() { profile.Addf("test-name", "test-%s", "value")
sharedEnvDir := filepath.Join(layers.Path, "test-name", "env") Expect(profile).To(Equal(libcnb.Profile{"test-name": "test-value"}))
Expect(os.MkdirAll(sharedEnvDir, os.ModePerm)).To(Succeed())
err := os.WriteFile(filepath.Join(sharedEnvDir, "OVERRIDE_VAR.override"), []byte("override-value"), 0600)
Expect(err).NotTo(HaveOccurred())
buildEnvDir := filepath.Join(layers.Path, "test-name", "env.build")
Expect(os.MkdirAll(buildEnvDir, os.ModePerm)).To(Succeed())
err = os.WriteFile(filepath.Join(buildEnvDir, "DEFAULT_VAR.default"), []byte("default-value"), 0600)
Expect(err).NotTo(HaveOccurred())
err = os.WriteFile(filepath.Join(buildEnvDir, "INVALID_VAR.invalid"), []byte("invalid-value"), 0600)
Expect(err).NotTo(HaveOccurred())
launchEnvDir := filepath.Join(layers.Path, "test-name", "env.launch")
Expect(os.MkdirAll(launchEnvDir, os.ModePerm)).To(Succeed())
err = os.WriteFile(filepath.Join(launchEnvDir, "APPEND_VAR.append"), []byte("append-value"), 0600)
Expect(err).NotTo(HaveOccurred())
err = os.WriteFile(filepath.Join(launchEnvDir, "APPEND_VAR.delim"), []byte("!"), 0600)
Expect(err).NotTo(HaveOccurred())
layer = libcnb.Layer{
Name: "test-name",
Path: filepath.Join(layers.Path, "test-name"),
LayerTypes: libcnb.LayerTypes{
Launch: true,
Build: true,
Cache: true,
},
SharedEnvironment: libcnb.Environment{
"OVERRIDE_VAR.override": "override-value",
},
BuildEnvironment: libcnb.Environment{
"DEFAULT_VAR.default": "default-value",
},
LaunchEnvironment: libcnb.Environment{
"APPEND_VAR.append": "append-value",
"APPEND_VAR.delim": "!",
},
Metadata: map[string]interface{}{
"some-key": "some-value",
},
}
})
context("when Reset is called on a layer", func() {
it("resets all of the layer data and clears the directory", func() {
layer, err := layer.Reset()
Expect(err).NotTo(HaveOccurred())
Expect(layer).To(Equal(libcnb.Layer{
Name: "test-name",
Path: filepath.Join(layers.Path, "test-name"),
LayerTypes: libcnb.LayerTypes{
Launch: false,
Build: false,
Cache: false,
},
SharedEnvironment: libcnb.Environment{},
BuildEnvironment: libcnb.Environment{},
LaunchEnvironment: libcnb.Environment{},
}))
Expect(filepath.Join(layers.Path, "test-name")).To(BeADirectory())
files, err := filepath.Glob(filepath.Join(layers.Path, "test-name", "*"))
Expect(err).NotTo(HaveOccurred())
Expect(files).To(BeEmpty())
})
})
}) })
context("could not remove files in layer", func() { it("adds process-specific content", func() {
it.Before(func() { profile.ProcessAdd("test-process", "test-name", "test-value")
Expect(os.Chmod(layers.Path, 0000)).To(Succeed()) Expect(profile).To(Equal(libcnb.Profile{filepath.Join("test-process", "test-name"): "test-value"}))
}) })
it.After(func() { it("adds process-specific formatted content", func() {
Expect(os.Chmod(layers.Path, 0777)).To(Succeed()) profile.ProcessAddf("test-process", "test-name", "test-%s", "value")
}) Expect(profile).To(Equal(libcnb.Profile{filepath.Join("test-process", "test-name"): "test-value"}))
it("return an error", func() {
layer := libcnb.Layer{
Name: "some-layer",
Path: filepath.Join(layers.Path, "some-layer"),
}
_, err := layer.Reset()
Expect(err).To(MatchError(ContainSubstring("error could not remove file: ")))
Expect(err).To(MatchError(ContainSubstring("permission denied")))
})
}) })
}) })
context("Layers", func() { context("Layers", func() {
it.Before(func() { it.Before(func() {
var err error path = t.TempDir()
path, err = os.MkdirTemp("", "layers")
Expect(err).NotTo(HaveOccurred())
layers = libcnb.Layers{Path: path} layers = libcnb.Layers{Path: path}
}) })
@ -220,6 +103,7 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
Expect(l.BuildEnvironment).To(Equal(libcnb.Environment{})) Expect(l.BuildEnvironment).To(Equal(libcnb.Environment{}))
Expect(l.LaunchEnvironment).To(Equal(libcnb.Environment{})) Expect(l.LaunchEnvironment).To(Equal(libcnb.Environment{}))
Expect(l.SharedEnvironment).To(Equal(libcnb.Environment{})) Expect(l.SharedEnvironment).To(Equal(libcnb.Environment{}))
Expect(l.Profile).To(Equal(libcnb.Profile{}))
}) })
it("generates SBOM paths", func() { it("generates SBOM paths", func() {
@ -252,7 +136,28 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
Expect(fmt).To(Equal(libcnb.UnknownFormat)) Expect(fmt).To(Equal(libcnb.UnknownFormat))
}) })
it("reads existing metadata", func() { it("reads existing 0.5 metadata", func() {
Expect(os.WriteFile(
filepath.Join(path, "test-name.toml"),
[]byte(`
launch = true
build = false
[metadata]
test-key = "test-value"
`),
0600),
).To(Succeed())
l, err := layers.Layer("test-name")
Expect(err).NotTo(HaveOccurred())
Expect(l.Metadata).To(Equal(map[string]interface{}{"test-key": "test-value"}))
Expect(l.Launch).To(BeTrue())
Expect(l.Build).To(BeFalse())
})
it("reads existing 0.6 metadata", func() {
Expect(os.WriteFile( Expect(os.WriteFile(
filepath.Join(path, "test-name.toml"), filepath.Join(path, "test-name.toml"),
[]byte(` []byte(`
@ -274,7 +179,7 @@ test-key = "test-value"
Expect(l.Build).To(BeFalse()) Expect(l.Build).To(BeFalse())
}) })
it("reads existing metadata with launch, build and cache all false", func() { it("reads existing 0.6 metadata with launch, build and cache all false", func() {
Expect(os.WriteFile( Expect(os.WriteFile(
filepath.Join(path, "test-name.toml"), filepath.Join(path, "test-name.toml"),
[]byte(` []byte(`

View File

@ -1,59 +0,0 @@
/*
* Copyright 2018-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package log
import (
"os"
)
//go:generate mockery --name DirectoryContentFormatter --case=underscore
// DirectoryContentFormatter allows customization of logged directory output
//
// When libcnb logs the contents of a directory, each item in the directory
// is passed through a DirectoryContentFormatter.
//
// DirectoryContentsWriter implements this workflow:
// - call RootPath(string) with the root path that's being walked
// - call Title(string) with the given title, the output is logged
// - for each file in the directory:
// - call File(string, os.FileInfo), the output is logged
//
// # A default implementation is provided that returns a formatter applies no formatting
//
// The returned formatter operates as such:
//
// Title -> returns string followed by `:\n`
// File -> returns file name relative to the root followed by `\n`
//
// A buildpack author could provide their own implementation through
// WithDirectoryContentFormatter when calling Detect or Build.
//
// A custom implementation might log in color or might log additional
// information about each file, like permissions. The implementation can
// also control line endings to force all of the files to be logged on a
// single line, or as multiple lines.
type DirectoryContentFormatter interface {
// File takes the full path and os.FileInfo and returns a display string
File(path string, info os.FileInfo) (string, error)
// RootPath provides the root path being iterated
RootPath(path string)
// Title provides a plain string title which can be embellished
Title(title string) string
}

View File

@ -1,102 +0,0 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package log
import (
"fmt"
"io"
"os"
"strings"
)
//go:generate mockery --name Logger --case=underscore
// Logger is the interface implement by a type that wishes to write log messages generated by libcnb
type Logger interface {
// Debug formats using the default formats for its operands
Debug(a ...interface{})
// Debugf formats according to a format specifier
Debugf(format string, a ...interface{})
// DebugWriter returns the configured debug writer
DebugWriter() io.Writer
// IsDebugEnabled indicates whether debug logging is enabled
IsDebugEnabled() bool
}
// PlainLogger implements Logger and logs messages to a writer.
type PlainLogger struct {
debug io.Writer
}
// New creates a new instance of PlainLogger. It configures debug logging if $BP_DEBUG or $BP_LOG_LEVEL are set.
func New(debug io.Writer) PlainLogger {
if strings.ToLower(os.Getenv("BP_LOG_LEVEL")) == "debug" || os.Getenv("BP_DEBUG") != "" {
return PlainLogger{debug: debug}
}
return PlainLogger{}
}
// NewDiscard creates a new instance of PlainLogger that discards all log messages. Useful in testing.
func NewDiscard() PlainLogger {
return PlainLogger{debug: io.Discard}
}
// Debug formats using the default formats for its operands and writes to the configured debug writer. Spaces are added
// between operands when neither is a string.
func (l PlainLogger) Debug(a ...interface{}) {
if !l.IsDebugEnabled() {
return
}
s := fmt.Sprint(a...)
if !strings.HasSuffix(s, "\n") {
s += "\n"
}
_, _ = fmt.Fprint(l.debug, s)
}
// Debugf formats according to a format specifier and writes to the configured debug writer.
func (l PlainLogger) Debugf(format string, a ...interface{}) {
if !l.IsDebugEnabled() {
return
}
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
_, _ = fmt.Fprintf(l.debug, format, a...)
}
// DebugWriter returns the configured debug writer.
func (l PlainLogger) DebugWriter() io.Writer {
if l.IsDebugEnabled() {
return l.debug
}
return io.Discard
}
// IsDebugEnabled indicates whether debug logging is enabled.
func (l PlainLogger) IsDebugEnabled() bool {
return l.debug != nil
}

View File

@ -1,79 +0,0 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
package mocks
import (
fs "io/fs"
mock "github.com/stretchr/testify/mock"
)
// DirectoryContentFormatter is an autogenerated mock type for the DirectoryContentFormatter type
type DirectoryContentFormatter struct {
mock.Mock
}
// File provides a mock function with given fields: path, info
func (_m *DirectoryContentFormatter) File(path string, info fs.FileInfo) (string, error) {
ret := _m.Called(path, info)
if len(ret) == 0 {
panic("no return value specified for File")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(string, fs.FileInfo) (string, error)); ok {
return rf(path, info)
}
if rf, ok := ret.Get(0).(func(string, fs.FileInfo) string); ok {
r0 = rf(path, info)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(string, fs.FileInfo) error); ok {
r1 = rf(path, info)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RootPath provides a mock function with given fields: path
func (_m *DirectoryContentFormatter) RootPath(path string) {
_m.Called(path)
}
// Title provides a mock function with given fields: title
func (_m *DirectoryContentFormatter) Title(title string) string {
ret := _m.Called(title)
if len(ret) == 0 {
panic("no return value specified for Title")
}
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(title)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// NewDirectoryContentFormatter creates a new instance of DirectoryContentFormatter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewDirectoryContentFormatter(t interface {
mock.TestingT
Cleanup(func())
}) *DirectoryContentFormatter {
mock := &DirectoryContentFormatter{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,81 +0,0 @@
// Code generated by mockery v2.43.2. DO NOT EDIT.
package mocks
import (
io "io"
mock "github.com/stretchr/testify/mock"
)
// Logger is an autogenerated mock type for the Logger type
type Logger struct {
mock.Mock
}
// Debug provides a mock function with given fields: a
func (_m *Logger) Debug(a ...interface{}) {
var _ca []interface{}
_ca = append(_ca, a...)
_m.Called(_ca...)
}
// DebugWriter provides a mock function with given fields:
func (_m *Logger) DebugWriter() io.Writer {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for DebugWriter")
}
var r0 io.Writer
if rf, ok := ret.Get(0).(func() io.Writer); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(io.Writer)
}
}
return r0
}
// Debugf provides a mock function with given fields: format, a
func (_m *Logger) Debugf(format string, a ...interface{}) {
var _ca []interface{}
_ca = append(_ca, format)
_ca = append(_ca, a...)
_m.Called(_ca...)
}
// IsDebugEnabled provides a mock function with given fields:
func (_m *Logger) IsDebugEnabled() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for IsDebugEnabled")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// NewLogger creates a new instance of Logger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewLogger(t interface {
mock.TestingT
Cleanup(func())
}) *Logger {
mock := &Logger{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

37
main.go
View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2018-2023 the original author or authors. * Copyright 2018-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,38 +18,37 @@ package libcnb
import ( import (
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"github.com/buildpacks/libcnb/internal"
) )
func main(detect DetectFunc, build BuildFunc, generate GenerateFunc, options ...Option) { // Main is called by the main function of a buildpack, encapsulating both detection and build in the same binary.
config := NewConfig(options...) func Main(detector Detector, builder Builder, options ...Option) {
config := Config{
arguments: os.Args,
environmentWriter: internal.EnvironmentWriter{},
exitHandler: internal.NewExitHandler(),
tomlWriter: internal.TOMLWriter{},
}
for _, option := range options {
config = option(config)
}
if len(config.arguments) == 0 { if len(config.arguments) == 0 {
config.exitHandler.Error(fmt.Errorf("expected command name")) config.exitHandler.Error(fmt.Errorf("expected command name"))
return return
} }
config.extension = build == nil && generate != nil
switch c := filepath.Base(config.arguments[0]); c { switch c := filepath.Base(config.arguments[0]); c {
case "build": case "build":
Build(build, config) Build(builder, options...)
case "detect": case "detect":
Detect(detect, config) Detect(detector, options...)
case "generate":
Generate(generate, config)
default: default:
config.exitHandler.Error(fmt.Errorf("unsupported command %s", c)) config.exitHandler.Error(fmt.Errorf("unsupported command %s", c))
return return
} }
} }
// BuildpackMain is called by the main function of a buildpack, encapsulating both detection and build in the same binary.
func BuildpackMain(detect DetectFunc, build BuildFunc, options ...Option) {
main(detect, build, nil, options...)
}
// ExtensionMain is called by the main function of a extension, encapsulating both detection and generation in the same binary.
func ExtensionMain(detect DetectFunc, generate GenerateFunc, options ...Option) {
main(detect, nil, generate, options...)
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2018-2023 the original author or authors. * Copyright 2018-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,9 +25,8 @@ import (
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/buildpacks/libcnb/v2" "github.com/buildpacks/libcnb"
"github.com/buildpacks/libcnb/v2/log" "github.com/buildpacks/libcnb/mocks"
"github.com/buildpacks/libcnb/v2/mocks"
) )
func testMain(t *testing.T, _ spec.G, it spec.S) { func testMain(t *testing.T, _ spec.G, it spec.S) {
@ -35,13 +34,13 @@ func testMain(t *testing.T, _ spec.G, it spec.S) {
Expect = NewWithT(t).Expect Expect = NewWithT(t).Expect
applicationPath string applicationPath string
buildFunc libcnb.BuildFunc builder *mocks.Builder
buildpackPath string buildpackPath string
buildpackPlanPath string buildpackPlanPath string
detectFunc libcnb.DetectFunc buildPlanPath string
detector *mocks.Detector
environmentWriter *mocks.EnvironmentWriter environmentWriter *mocks.EnvironmentWriter
exitHandler *mocks.ExitHandler exitHandler *mocks.ExitHandler
generateFunc libcnb.GenerateFunc
layersPath string layersPath string
platformPath string platformPath string
tomlWriter *mocks.TOMLWriter tomlWriter *mocks.TOMLWriter
@ -52,22 +51,19 @@ func testMain(t *testing.T, _ spec.G, it spec.S) {
it.Before(func() { it.Before(func() {
var err error var err error
applicationPath, err = os.MkdirTemp("", "main-application-path") applicationPath = t.TempDir()
Expect(err).NotTo(HaveOccurred())
applicationPath, err = filepath.EvalSymlinks(applicationPath) applicationPath, err = filepath.EvalSymlinks(applicationPath)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { builder = &mocks.Builder{}
return libcnb.NewBuildResult(), nil
}
buildpackPath, err = os.MkdirTemp("", "main-buildpack-path") buildpackPath = t.TempDir()
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(os.Setenv("CNB_BUILDPACK_DIR", buildpackPath)).To(Succeed()) Expect(os.Setenv("CNB_BUILDPACK_DIR", buildpackPath)).To(Succeed())
Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
[]byte(` []byte(`
api = "0.8" api = "0.6"
[buildpack] [buildpack]
id = "test-id" id = "test-id"
@ -83,6 +79,7 @@ optional = true
[[stacks]] [[stacks]]
id = "test-id" id = "test-id"
mixins = ["test-name"]
[metadata] [metadata]
test-key = "test-value" test-key = "test-value"
@ -107,13 +104,12 @@ test-key = "test-value"
0600), 0600),
).To(Succeed()) ).To(Succeed())
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { f, err = os.CreateTemp("", "main-buildplan-path")
return libcnb.DetectResult{}, nil Expect(err).NotTo(HaveOccurred())
} Expect(f.Close()).NotTo(HaveOccurred())
buildPlanPath = f.Name()
generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) { detector = &mocks.Detector{}
return libcnb.GenerateResult{}, nil
}
environmentWriter = &mocks.EnvironmentWriter{} environmentWriter = &mocks.EnvironmentWriter{}
environmentWriter.On("Write", mock.Anything, mock.Anything).Return(nil) environmentWriter.On("Write", mock.Anything, mock.Anything).Return(nil)
@ -123,8 +119,7 @@ test-key = "test-value"
exitHandler.On("Pass", mock.Anything) exitHandler.On("Pass", mock.Anything)
exitHandler.On("Fail", mock.Anything) exitHandler.On("Fail", mock.Anything)
layersPath, err = os.MkdirTemp("", "main-layers-path") layersPath = t.TempDir()
Expect(err).NotTo(HaveOccurred())
Expect(os.WriteFile(filepath.Join(layersPath, "store.toml"), Expect(os.WriteFile(filepath.Join(layersPath, "store.toml"),
[]byte(` []byte(`
@ -134,8 +129,7 @@ test-key = "test-value"
0600), 0600),
).To(Succeed()) ).To(Succeed())
platformPath, err = os.MkdirTemp("", "main-platform-path") platformPath = t.TempDir()
Expect(err).NotTo(HaveOccurred())
Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha", "metadata"), 0755)).To(Succeed()) Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha", "metadata"), 0755)).To(Succeed())
Expect(os.WriteFile( Expect(os.WriteFile(
@ -158,10 +152,6 @@ test-key = "test-value"
tomlWriter.On("Write", mock.Anything, mock.Anything).Return(nil) tomlWriter.On("Write", mock.Anything, mock.Anything).Return(nil)
Expect(os.Setenv("CNB_STACK_ID", "test-stack-id")).To(Succeed()) Expect(os.Setenv("CNB_STACK_ID", "test-stack-id")).To(Succeed())
Expect(os.Setenv("CNB_LAYERS_DIR", layersPath)).To(Succeed())
Expect(os.Setenv("CNB_PLATFORM_DIR", platformPath)).To(Succeed())
Expect(os.Setenv("CNB_BP_PLAN_PATH", buildpackPlanPath)).To(Succeed())
Expect(os.Setenv("CNB_BUILD_PLAN_PATH", buildpackPlanPath)).To(Succeed())
workingDir, err = os.Getwd() workingDir, err = os.Getwd()
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
@ -172,10 +162,6 @@ test-key = "test-value"
Expect(os.Chdir(workingDir)).To(Succeed()) Expect(os.Chdir(workingDir)).To(Succeed())
Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed()) Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed()) Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed())
Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed())
Expect(os.Unsetenv("CNB_LAYERS_DIR")).To(Succeed())
Expect(os.Unsetenv("CNB_BUILD_PLAN_PATH")).To(Succeed())
Expect(os.RemoveAll(applicationPath)).To(Succeed()) Expect(os.RemoveAll(applicationPath)).To(Succeed())
Expect(os.RemoveAll(buildpackPath)).To(Succeed()) Expect(os.RemoveAll(buildpackPath)).To(Succeed())
@ -185,78 +171,55 @@ test-key = "test-value"
}) })
it("encounters the wrong number of arguments", func() { it("encounters the wrong number of arguments", func() {
libcnb.BuildpackMain(detectFunc, buildFunc, libcnb.Main(detector, builder,
libcnb.WithArguments([]string{}), libcnb.WithArguments([]string{}),
libcnb.WithExitHandler(exitHandler), libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
) )
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("expected command name")) Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("expected command name"))
}) })
it("calls builder for build command", func() { it("calls builder for build command", func() {
builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil)
commandPath := filepath.Join("bin", "build") commandPath := filepath.Join("bin", "build")
libcnb.BuildpackMain(detectFunc, buildFunc, libcnb.Main(detector, builder,
libcnb.WithArguments([]string{commandPath}), libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler), libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
) )
Expect(exitHandler.Calls).To(BeEmpty()) Expect(exitHandler.Calls).To(BeEmpty())
}) })
it("calls detector for detect command", func() { it("calls detector for detect command", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: true}, nil)
return libcnb.DetectResult{Pass: true}, nil
}
commandPath := filepath.Join("bin", "detect") commandPath := filepath.Join("bin", "detect")
libcnb.BuildpackMain(detectFunc, buildFunc, libcnb.Main(detector, builder,
libcnb.WithArguments([]string{commandPath}), libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler), libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
)
})
it("calls generator for generate command", func() {
generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) {
return libcnb.GenerateResult{}, nil
}
commandPath := filepath.Join("bin", "generate")
libcnb.ExtensionMain(nil, generateFunc,
libcnb.WithArguments([]string{commandPath}),
libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
) )
}) })
it("calls exitHandler.Pass() on detection pass", func() { it("calls exitHandler.Pass() on detection pass", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: true}, nil)
return libcnb.DetectResult{Pass: true}, nil
}
commandPath := filepath.Join("bin", "detect") commandPath := filepath.Join("bin", "detect")
libcnb.BuildpackMain(detectFunc, buildFunc, libcnb.Main(detector, builder,
libcnb.WithArguments([]string{commandPath}), libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler), libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
) )
Expect(exitHandler.Calls[0].Method).To(BeIdenticalTo("Pass")) Expect(exitHandler.Calls[0].Method).To(BeIdenticalTo("Pass"))
}) })
it("calls exitHandler.Fail() on detection fail", func() { it("calls exitHandler.Fail() on detection fail", func() {
detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { detector.On("Detect", mock.Anything).Return(libcnb.DetectResult{Pass: false}, nil)
return libcnb.DetectResult{Pass: false}, nil
}
commandPath := filepath.Join("bin", "detect") commandPath := filepath.Join("bin", "detect")
libcnb.BuildpackMain(detectFunc, buildFunc, libcnb.Main(detector, builder,
libcnb.WithArguments([]string{commandPath}), libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}),
libcnb.WithExitHandler(exitHandler), libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
) )
Expect(exitHandler.Calls[0].Method).To(BeIdenticalTo("Fail")) Expect(exitHandler.Calls[0].Method).To(BeIdenticalTo("Fail"))
@ -265,10 +228,9 @@ test-key = "test-value"
it("encounters an unknown command", func() { it("encounters an unknown command", func() {
commandPath := filepath.Join("bin", "test-command") commandPath := filepath.Join("bin", "test-command")
libcnb.BuildpackMain(detectFunc, buildFunc, libcnb.Main(detector, builder,
libcnb.WithArguments([]string{commandPath}), libcnb.WithArguments([]string{commandPath}),
libcnb.WithExitHandler(exitHandler), libcnb.WithExitHandler(exitHandler),
libcnb.WithLogger(log.NewDiscard()),
) )
Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unsupported command test-command")) Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unsupported command test-command"))

53
mocks/builder.go Normal file
View File

@ -0,0 +1,53 @@
// Code generated by mockery v2.24.0. DO NOT EDIT.
package mocks
import (
mock "github.com/stretchr/testify/mock"
libcnb "github.com/buildpacks/libcnb"
)
// Builder is an autogenerated mock type for the Builder type
type Builder struct {
mock.Mock
}
// Build provides a mock function with given fields: context
func (_m *Builder) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
ret := _m.Called(context)
var r0 libcnb.BuildResult
var r1 error
if rf, ok := ret.Get(0).(func(libcnb.BuildContext) (libcnb.BuildResult, error)); ok {
return rf(context)
}
if rf, ok := ret.Get(0).(func(libcnb.BuildContext) libcnb.BuildResult); ok {
r0 = rf(context)
} else {
r0 = ret.Get(0).(libcnb.BuildResult)
}
if rf, ok := ret.Get(1).(func(libcnb.BuildContext) error); ok {
r1 = rf(context)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewBuilder interface {
mock.TestingT
Cleanup(func())
}
// NewBuilder creates a new instance of Builder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewBuilder(t mockConstructorTestingTNewBuilder) *Builder {
mock := &Builder{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

53
mocks/detector.go Normal file
View File

@ -0,0 +1,53 @@
// Code generated by mockery v2.24.0. DO NOT EDIT.
package mocks
import (
mock "github.com/stretchr/testify/mock"
libcnb "github.com/buildpacks/libcnb"
)
// Detector is an autogenerated mock type for the Detector type
type Detector struct {
mock.Mock
}
// Detect provides a mock function with given fields: context
func (_m *Detector) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) {
ret := _m.Called(context)
var r0 libcnb.DetectResult
var r1 error
if rf, ok := ret.Get(0).(func(libcnb.DetectContext) (libcnb.DetectResult, error)); ok {
return rf(context)
}
if rf, ok := ret.Get(0).(func(libcnb.DetectContext) libcnb.DetectResult); ok {
r0 = rf(context)
} else {
r0 = ret.Get(0).(libcnb.DetectResult)
}
if rf, ok := ret.Get(1).(func(libcnb.DetectContext) error); ok {
r1 = rf(context)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewDetector interface {
mock.TestingT
Cleanup(func())
}
// NewDetector creates a new instance of Detector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewDetector(t mockConstructorTestingTNewDetector) *Detector {
mock := &Detector{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.2. DO NOT EDIT. // Code generated by mockery v2.24.0. DO NOT EDIT.
package mocks package mocks
@ -13,10 +13,6 @@ type EnvironmentWriter struct {
func (_m *EnvironmentWriter) Write(dir string, environment map[string]string) error { func (_m *EnvironmentWriter) Write(dir string, environment map[string]string) error {
ret := _m.Called(dir, environment) ret := _m.Called(dir, environment)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok { if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok {
r0 = rf(dir, environment) r0 = rf(dir, environment)
@ -27,12 +23,13 @@ func (_m *EnvironmentWriter) Write(dir string, environment map[string]string) er
return r0 return r0
} }
// NewEnvironmentWriter creates a new instance of EnvironmentWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. type mockConstructorTestingTNewEnvironmentWriter interface {
// The first argument is typically a *testing.T value.
func NewEnvironmentWriter(t interface {
mock.TestingT mock.TestingT
Cleanup(func()) Cleanup(func())
}) *EnvironmentWriter { }
// NewEnvironmentWriter creates a new instance of EnvironmentWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewEnvironmentWriter(t mockConstructorTestingTNewEnvironmentWriter) *EnvironmentWriter {
mock := &EnvironmentWriter{} mock := &EnvironmentWriter{}
mock.Mock.Test(t) mock.Mock.Test(t)

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.2. DO NOT EDIT. // Code generated by mockery v2.24.0. DO NOT EDIT.
package mocks package mocks
@ -13,10 +13,6 @@ type ExecD struct {
func (_m *ExecD) Execute() (map[string]string, error) { func (_m *ExecD) Execute() (map[string]string, error) {
ret := _m.Called() ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Execute")
}
var r0 map[string]string var r0 map[string]string
var r1 error var r1 error
if rf, ok := ret.Get(0).(func() (map[string]string, error)); ok { if rf, ok := ret.Get(0).(func() (map[string]string, error)); ok {
@ -39,12 +35,13 @@ func (_m *ExecD) Execute() (map[string]string, error) {
return r0, r1 return r0, r1
} }
// NewExecD creates a new instance of ExecD. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. type mockConstructorTestingTNewExecD interface {
// The first argument is typically a *testing.T value.
func NewExecD(t interface {
mock.TestingT mock.TestingT
Cleanup(func()) Cleanup(func())
}) *ExecD { }
// NewExecD creates a new instance of ExecD. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewExecD(t mockConstructorTestingTNewExecD) *ExecD {
mock := &ExecD{} mock := &ExecD{}
mock.Mock.Test(t) mock.Mock.Test(t)

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.2. DO NOT EDIT. // Code generated by mockery v2.24.0. DO NOT EDIT.
package mocks package mocks
@ -13,10 +13,6 @@ type ExecDWriter struct {
func (_m *ExecDWriter) Write(value map[string]string) error { func (_m *ExecDWriter) Write(value map[string]string) error {
ret := _m.Called(value) ret := _m.Called(value)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(map[string]string) error); ok { if rf, ok := ret.Get(0).(func(map[string]string) error); ok {
r0 = rf(value) r0 = rf(value)
@ -27,12 +23,13 @@ func (_m *ExecDWriter) Write(value map[string]string) error {
return r0 return r0
} }
// NewExecDWriter creates a new instance of ExecDWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. type mockConstructorTestingTNewExecDWriter interface {
// The first argument is typically a *testing.T value.
func NewExecDWriter(t interface {
mock.TestingT mock.TestingT
Cleanup(func()) Cleanup(func())
}) *ExecDWriter { }
// NewExecDWriter creates a new instance of ExecDWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewExecDWriter(t mockConstructorTestingTNewExecDWriter) *ExecDWriter {
mock := &ExecDWriter{} mock := &ExecDWriter{}
mock.Mock.Test(t) mock.Mock.Test(t)

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.2. DO NOT EDIT. // Code generated by mockery v2.24.0. DO NOT EDIT.
package mocks package mocks
@ -24,12 +24,13 @@ func (_m *ExitHandler) Pass() {
_m.Called() _m.Called()
} }
// NewExitHandler creates a new instance of ExitHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. type mockConstructorTestingTNewExitHandler interface {
// The first argument is typically a *testing.T value.
func NewExitHandler(t interface {
mock.TestingT mock.TestingT
Cleanup(func()) Cleanup(func())
}) *ExitHandler { }
// NewExitHandler creates a new instance of ExitHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewExitHandler(t mockConstructorTestingTNewExitHandler) *ExitHandler {
mock := &ExitHandler{} mock := &ExitHandler{}
mock.Mock.Test(t) mock.Mock.Test(t)

View File

@ -0,0 +1,67 @@
// Code generated by mockery v2.24.0. DO NOT EDIT.
package mocks
import (
mock "github.com/stretchr/testify/mock"
libcnb "github.com/buildpacks/libcnb"
)
// LayerContributor is an autogenerated mock type for the LayerContributor type
type LayerContributor struct {
mock.Mock
}
// Contribute provides a mock function with given fields: layer
func (_m *LayerContributor) Contribute(layer libcnb.Layer) (libcnb.Layer, error) {
ret := _m.Called(layer)
var r0 libcnb.Layer
var r1 error
if rf, ok := ret.Get(0).(func(libcnb.Layer) (libcnb.Layer, error)); ok {
return rf(layer)
}
if rf, ok := ret.Get(0).(func(libcnb.Layer) libcnb.Layer); ok {
r0 = rf(layer)
} else {
r0 = ret.Get(0).(libcnb.Layer)
}
if rf, ok := ret.Get(1).(func(libcnb.Layer) error); ok {
r1 = rf(layer)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Name provides a mock function with given fields:
func (_m *LayerContributor) Name() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
type mockConstructorTestingTNewLayerContributor interface {
mock.TestingT
Cleanup(func())
}
// NewLayerContributor creates a new instance of LayerContributor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewLayerContributor(t mockConstructorTestingTNewLayerContributor) *LayerContributor {
mock := &LayerContributor{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.43.2. DO NOT EDIT. // Code generated by mockery v2.24.0. DO NOT EDIT.
package mocks package mocks
@ -13,10 +13,6 @@ type TOMLWriter struct {
func (_m *TOMLWriter) Write(path string, value interface{}) error { func (_m *TOMLWriter) Write(path string, value interface{}) error {
ret := _m.Called(path, value) ret := _m.Called(path, value)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok { if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
r0 = rf(path, value) r0 = rf(path, value)
@ -27,12 +23,13 @@ func (_m *TOMLWriter) Write(path string, value interface{}) error {
return r0 return r0
} }
// NewTOMLWriter creates a new instance of TOMLWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. type mockConstructorTestingTNewTOMLWriter interface {
// The first argument is typically a *testing.T value.
func NewTOMLWriter(t interface {
mock.TestingT mock.TestingT
Cleanup(func()) Cleanup(func())
}) *TOMLWriter { }
// NewTOMLWriter creates a new instance of TOMLWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewTOMLWriter(t mockConstructorTestingTNewTOMLWriter) *TOMLWriter {
mock := &TOMLWriter{} mock := &TOMLWriter{}
mock.Mock.Test(t) mock.Mock.Test(t)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2018-2023 the original author or authors. * Copyright 2018-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,18 +18,19 @@ package libcnb
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"github.com/buildpacks/libcnb/v2/internal" "github.com/buildpacks/libcnb/internal"
) )
const ( const (
// BindingKind is the metadata key for a binding's kind.
BindingKind = "kind"
// BindingProvider is the key for a binding's provider. // BindingProvider is the key for a binding's provider.
BindingProvider = "provider" BindingProvider = "provider"
@ -41,52 +42,15 @@ const (
// See the Service Binding Specification for Kubernetes for more details - https://k8s-service-bindings.github.io/spec/ // See the Service Binding Specification for Kubernetes for more details - https://k8s-service-bindings.github.io/spec/
EnvServiceBindings = "SERVICE_BINDING_ROOT" EnvServiceBindings = "SERVICE_BINDING_ROOT"
// EnvBuildpackDirectory is the name of the environment variable that contains the path to the buildpack
EnvBuildpackDirectory = "CNB_BUILDPACK_DIR"
// EnvExtensionDirectory is the name of the environment variable that contains the path to the extension
EnvExtensionDirectory = "CNB_EXTENSION_DIR"
// EnvVcapServices is the name of the environment variable that contains the bindings in cloudfoundry // EnvVcapServices is the name of the environment variable that contains the bindings in cloudfoundry
EnvVcapServices = "VCAP_SERVICES" EnvVcapServices = "VCAP_SERVICES"
// EnvLayersDirectory is the name of the environment variable that contains the root path to all buildpack layers // EnvCNBBindings is the name of the environment variable that contains the path to the CNB bindings directory.
EnvLayersDirectory = "CNB_LAYERS_DIR" // See the CNB bindings extension spec for more details - https://github.com/buildpacks/spec/blob/main/extensions/bindings.md
// EnvOutputDirectory is the name of the environment variable that contains the path to the output directory
EnvOutputDirectory = "CNB_OUTPUT_DIR"
// EnvPlatformDirectory is the name of the environment variable that contains the path to the platform directory
EnvPlatformDirectory = "CNB_PLATFORM_DIR"
// EnvDetectBuildPlanPath is the name of the environment variable that contains the path to the build plan
EnvDetectPlanPath = "CNB_BUILD_PLAN_PATH"
// EnvBuildPlanPath is the name of the environment variable that contains the path to the build plan
EnvBuildPlanPath = "CNB_BP_PLAN_PATH"
// Deprecated: EnvStackID is the name of the environment variable that contains the stack id
EnvStackID = "CNB_STACK_ID"
// EnvTargetOS contains the name of the os
EnvTargetOS = "CNB_TARGET_OS"
// EnvTargetArch contains the architecture
EnvTargetArch = "CNB_TARGET_ARCH"
// EnvTargetOS contains the variant of the architecture
EnvTargetArchVariant = "CNB_TARGET_ARCH_VARIANT"
// EnvTargetDistroName contains the name of the ditro
EnvTargetDistroName = "CNB_TARGET_DISTRO_NAME"
// EnvTargetDistroVersion contains the version of the distro
EnvTargetDistroVersion = "CNB_TARGET_DISTRO_VERSION"
// DefaultPlatformBindingsLocation is the typical location for bindings, which exists under the platform directory
// //
// Not guaranteed to exist, but often does. This should only be used as a fallback if EnvServiceBindings and EnvPlatformDirectory are not set // Deprecated: Use the Service Binding Specification for Kubernetes instead -
DefaultPlatformBindingsLocation = "/platform/bindings" // https://github.com/buildpacks/rfcs/blob/main/text/0055-deprecate-service-bindings.md.
EnvCNBBindings = "CNB_BINDINGS"
) )
// Binding is a projection of metadata about an external entity to be bound to. // Binding is a projection of metadata about an external entity to be bound to.
@ -118,7 +82,7 @@ func NewBinding(name string, path string, secret map[string]string) Binding {
for k, v := range secret { for k, v := range secret {
switch k { switch k {
case BindingType: case BindingType, BindingKind: // TODO: Remove as CNB_BINDINGS ages out
b.Type = strings.TrimSpace(v) b.Type = strings.TrimSpace(v)
case BindingProvider: case BindingProvider:
b.Provider = strings.TrimSpace(v) b.Provider = strings.TrimSpace(v)
@ -137,58 +101,20 @@ func NewBindingFromPath(path string) (Binding, error) {
return Binding{}, fmt.Errorf("unable to create new config map from %s\n%w", path, err) return Binding{}, fmt.Errorf("unable to create new config map from %s\n%w", path, err)
} }
return NewBinding(filepath.Base(path), path, secret), nil // TODO: Remove as CNB_BINDINGS ages out
} for _, d := range []string{"metadata", "secret"} {
file := filepath.Join(path, d)
func (b Binding) String() string { cm, err := internal.NewConfigMapFromPath(file)
var s []string
for k := range b.Secret {
s = append(s, k)
}
sort.Strings(s)
return fmt.Sprintf("{Name: %s Path: %s Type: %s Provider: %s Secret: %s}",
b.Name, b.Path, b.Type, b.Provider, s)
}
// SecretFilePath return the path to a secret file with the given name.
func (b Binding) SecretFilePath(name string) (string, bool) {
if _, ok := b.Secret[name]; !ok {
return "", false
}
return filepath.Join(b.Path, name), true
}
// Bindings is a collection of bindings keyed by their name.
type Bindings []Binding
// NewBindingsFromPath creates a new instance from all the bindings at a given path.
func NewBindingsFromPath(path string) (Bindings, error) {
files, err := os.ReadDir(path)
if err != nil && errors.Is(err, fs.ErrNotExist) {
return Bindings{}, nil
} else if err != nil {
return nil, fmt.Errorf("unable to list directory %s\n%w", path, err)
}
bindings := Bindings{}
for _, file := range files {
bindingPath := filepath.Join(path, file.Name())
if strings.HasPrefix(filepath.Base(bindingPath), ".") {
// ignore hidden files
continue
}
binding, err := NewBindingFromPath(bindingPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to create new binding from %s\n%w", file, err) return Binding{}, fmt.Errorf("unable to create new config map from %s\n%w", file, err)
} }
bindings = append(bindings, binding) for k, v := range cm {
secret[k] = v
}
} }
return bindings, nil return NewBinding(filepath.Base(path), path, secret), nil
} }
type vcapServicesBinding struct { type vcapServicesBinding struct {
@ -241,23 +167,102 @@ func NewBindingsFromVcapServicesEnv(content string) (Bindings, error) {
return bindings, nil return bindings, nil
} }
// NewBindings creates a new bindings from all the bindings at the path defined by $SERVICE_BINDING_ROOT. func (b Binding) String() string {
// If that isn't defined, bindings are read from <platform>/bindings. var s []string
// If that isn't defined, bindings are read from $VCAP_SERVICES. for k := range b.Secret {
// If that isn't defined, the specified platform path will be used s = append(s, k)
func NewBindings(platformDir string) (Bindings, error) { }
sort.Strings(s)
return fmt.Sprintf("{Name: %s Path: %s Type: %s Provider: %s Secret: %s}",
b.Name, b.Path, b.Type, b.Provider, s)
}
// SecretFilePath return the path to a secret file with the given name.
func (b Binding) SecretFilePath(name string) (string, bool) {
if _, ok := b.Secret[name]; !ok {
return "", false
}
// TODO: Remove as CNB_BINDINGS ages out
for _, d := range []string{"metadata", "secret"} {
file := filepath.Join(b.Path, d, name)
if _, err := os.Stat(file); err == nil {
return file, true
}
}
return filepath.Join(b.Path, name), true
}
// Bindings is a collection of bindings keyed by their name.
type Bindings []Binding
// NewBindingsFromEnvironment creates a new bindings from all the bindings at the path defined by $SERVICE_BINDING_ROOT
// or $CNB_BINDINGS if it does not exist. If neither is defined, returns an empty collection of Bindings.
// Note - This API is deprecated. Please use NewBindingsForLaunch instead.
func NewBindingsFromEnvironment() (Bindings, error) {
return NewBindingsForLaunch()
}
// NewBindingsForLaunch creates a new bindings from all the bindings at the path defined by $SERVICE_BINDING_ROOT
// or $CNB_BINDINGS if it does not exist. If neither is defined, returns an empty collection of Bindings.
func NewBindingsForLaunch() (Bindings, error) {
if path, ok := os.LookupEnv(EnvServiceBindings); ok { if path, ok := os.LookupEnv(EnvServiceBindings); ok {
return NewBindingsFromPath(path) return NewBindingsFromPath(path)
} }
if path, ok := os.LookupEnv(EnvPlatformDirectory); ok { // TODO: Remove as CNB_BINDINGS ages out
return NewBindingsFromPath(filepath.Join(path, "bindings")) if path, ok := os.LookupEnv(EnvCNBBindings); ok {
return NewBindingsFromPath(path)
} }
if content, ok := os.LookupEnv(EnvVcapServices); ok { if content, ok := os.LookupEnv(EnvVcapServices); ok {
return NewBindingsFromVcapServicesEnv(content) return NewBindingsFromVcapServicesEnv(content)
} }
return Bindings{}, nil
}
// NewBindingsFromPath creates a new instance from all the bindings at a given path.
func NewBindingsFromPath(path string) (Bindings, error) {
files, err := filepath.Glob(filepath.Join(path, "*"))
if err != nil {
return nil, fmt.Errorf("unable to glob %s\n%w", path, err)
}
bindings := Bindings{}
for _, file := range files {
if strings.HasPrefix(filepath.Base(file), ".") {
// ignore hidden files
continue
}
binding, err := NewBindingFromPath(file)
if err != nil {
return nil, fmt.Errorf("unable to create new binding from %s\n%w", file, err)
}
bindings = append(bindings, binding)
}
return bindings, nil
}
// NewBindingsForBuild creates a new bindings from all the bindings at the path defined by $SERVICE_BINDING_ROOT
// or $CNB_BINDINGS if it does not exist. If neither is defined, bindings are read from <platform>/bindings, the default
// path defined in the CNB Binding extension specification.
func NewBindingsForBuild(platformDir string) (Bindings, error) {
if path, ok := os.LookupEnv(EnvServiceBindings); ok {
return NewBindingsFromPath(path)
}
// TODO: Remove as CNB_BINDINGS ages out
if path, ok := os.LookupEnv(EnvCNBBindings); ok {
return NewBindingsFromPath(path)
}
if content, ok := os.LookupEnv(EnvVcapServices); ok {
return NewBindingsFromVcapServicesEnv(content)
}
return NewBindingsFromPath(filepath.Join(platformDir, "bindings")) return NewBindingsFromPath(filepath.Join(platformDir, "bindings"))
} }

View File

@ -25,7 +25,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2" "github.com/buildpacks/libcnb"
) )
func testPlatform(t *testing.T, context spec.G, it spec.S) { func testPlatform(t *testing.T, context spec.G, it spec.S) {
@ -36,66 +36,237 @@ func testPlatform(t *testing.T, context spec.G, it spec.S) {
) )
it.Before(func() { it.Before(func() {
var err error platformPath = t.TempDir()
platformPath, err = os.MkdirTemp("", "platform")
path = filepath.Join(platformPath, "bindings") path = filepath.Join(platformPath, "bindings")
Expect(err).NotTo(HaveOccurred())
})
it.After(func() {
Expect(os.RemoveAll(path)).To(Succeed())
}) })
context("Cloudfoundry VCAP_SERVICES", func() { context("Cloudfoundry VCAP_SERVICES", func() {
it("creates a bindings from VCAP_SERVICES", func() { context("Build", func() {
content, err := os.ReadFile("testdata/vcap_services.json") it("creates a bindings from VCAP_SERVICES", func() {
Expect(err).NotTo(HaveOccurred()) content, err := os.ReadFile("testdata/vcap_services.json")
t.Setenv(libcnb.EnvVcapServices, string(content)) Expect(err).NotTo(HaveOccurred())
t.Setenv(libcnb.EnvVcapServices, string(content))
bindings, err := libcnb.NewBindings("") bindings, err := libcnb.NewBindingsForBuild("")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(bindings).To(ConsistOf(libcnb.Bindings{ Expect(bindings).To(ConsistOf(libcnb.Bindings{
{ {
Name: "elephantsql-binding-c6c60", Name: "elephantsql-binding-c6c60",
Type: "elephantsql-type", Type: "elephantsql-type",
Provider: "elephantsql-provider", Provider: "elephantsql-provider",
Secret: map[string]string{ Secret: map[string]string{
"bool": "true", "bool": "true",
"int": "1", "int": "1",
"uri": "postgres://exampleuser:examplepass@postgres.example.com:5432/exampleuser", "uri": "postgres://exampleuser:examplepass@postgres.example.com:5432/exampleuser",
},
}, },
}, {
{ Name: "mysendgrid",
Name: "mysendgrid", Type: "sendgrid-type",
Type: "sendgrid-type", Provider: "sendgrid-provider",
Provider: "sendgrid-provider", Secret: map[string]string{
Secret: map[string]string{ "username": "QvsXMbJ3rK",
"username": "QvsXMbJ3rK", "password": "HCHMOYluTv",
"password": "HCHMOYluTv", "hostname": "smtp.example.com",
"hostname": "smtp.example.com", },
}, },
}, {
{ Name: "postgres",
Name: "postgres", Type: "postgres",
Type: "postgres", Provider: "postgres",
Provider: "postgres", Secret: map[string]string{
Secret: map[string]string{ "urls": "{\"example\":\"http://example.com\"}",
"urls": "{\"example\":\"http://example.com\"}", "username": "foo",
"username": "foo", "password": "bar",
"password": "bar", },
}, },
}, }))
})) })
it("creates empty bindings from empty VCAP_SERVICES", func() {
t.Setenv(libcnb.EnvVcapServices, "{}")
bindings, err := libcnb.NewBindingsForBuild("")
Expect(err).NotTo(HaveOccurred())
Expect(bindings).To(HaveLen(0))
})
}) })
it("creates empty bindings from empty VCAP_SERVICES", func() { context("Launch", func() {
t.Setenv(libcnb.EnvVcapServices, "{}") it("creates a bindings from VCAP_SERVICES", func() {
content, err := os.ReadFile("testdata/vcap_services.json")
Expect(err).NotTo(HaveOccurred())
t.Setenv(libcnb.EnvVcapServices, string(content))
bindings, err := libcnb.NewBindings("") bindings, err := libcnb.NewBindingsForLaunch()
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(bindings).To(HaveLen(0)) Expect(bindings).To(HaveLen(3))
types := []string{bindings[0].Type, bindings[1].Type, bindings[2].Type}
Expect(types).To(ContainElements("elephantsql-type", "sendgrid-type", "postgres"))
})
it("creates empty bindings from empty VCAP_SERVICES", func() {
t.Setenv(libcnb.EnvVcapServices, "{}")
bindings, err := libcnb.NewBindingsForLaunch()
Expect(err).NotTo(HaveOccurred())
Expect(bindings).To(HaveLen(0))
})
})
})
context("CNB Bindings", func() {
it.Before(func() {
Expect(os.MkdirAll(filepath.Join(path, "alpha", "metadata"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "alpha", "metadata", "kind"), []byte("test-kind"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "alpha", "metadata", "provider"), []byte("test-provider"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "alpha", "metadata", "test-metadata-key"), []byte("test-metadata-value"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "alpha", "metadata", "test-metadata-key-trimmed"), []byte(" test-metadata-value-trimmed \n"), 0600)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(path, "alpha", "secret"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "alpha", "secret", "test-secret-key"), []byte("test-secret-value"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "alpha", "secret", "test-secret-key-trimmed"), []byte(" test-secret-value-trimmed \n"), 0600)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(path, "bravo", "metadata"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "bravo", "metadata", "kind"), []byte("test-kind"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "bravo", "metadata", "provider"), []byte("test-provider"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "bravo", "metadata", "test-metadata-key"), []byte("test-metadata-value"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "bravo", "metadata", "test-metadata-key-trimmed"), []byte(" test-metadata-value-trimmed \n"), 0600)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(path, "bravo", "secret"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "bravo", "secret", "test-secret-key"), []byte("test-secret-value"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, "bravo", "secret", "test-secret-key-trimmed"), []byte(" test-secret-value-trimmed \n"), 0600)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(path, ".hidden", "metadata"), 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, ".hidden", "metadata", "kind"), []byte("test-kind"), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(path, ".hiddenFile"), []byte("test-kind"), 0600)).To(Succeed())
})
context("Binding", func() {
it("creates an empty binding", func() {
Expect(libcnb.NewBinding("test-name", "test-path", map[string]string{
libcnb.BindingKind: "test-kind",
libcnb.BindingProvider: "test-provider",
"test-key": "test-value",
})).To(Equal(libcnb.Binding{
Name: "test-name",
Path: "test-path",
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{"test-key": "test-value"},
}))
})
it("creates a binding from a path", func() {
path := filepath.Join(path, "alpha")
binding, err := libcnb.NewBindingFromPath(path)
Expect(binding, err).To(Equal(libcnb.Binding{
Name: filepath.Base(path),
Path: path,
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{
"test-metadata-key": "test-metadata-value",
"test-metadata-key-trimmed": "test-metadata-value-trimmed",
"test-secret-key": "test-secret-value",
"test-secret-key-trimmed": "test-secret-value-trimmed",
},
}))
metadataFilePath, ok := binding.SecretFilePath("test-metadata-key")
Expect(ok).To(BeTrue())
Expect(metadataFilePath).To(Equal(filepath.Join(path, "metadata", "test-metadata-key")))
secretFilePath, ok := binding.SecretFilePath("test-secret-key")
Expect(ok).To(BeTrue())
Expect(secretFilePath).To(Equal(filepath.Join(path, "secret", "test-secret-key")))
})
it("sanitizes secrets", func() {
path := filepath.Join(path, "alpha")
b, err := libcnb.NewBindingFromPath(path)
Expect(err).NotTo(HaveOccurred())
Expect(b.String()).To(Equal(fmt.Sprintf("{Name: alpha Path: %s Type: test-kind Provider: test-provider Secret: [test-metadata-key test-metadata-key-trimmed test-secret-key test-secret-key-trimmed]}", path)))
})
})
context("Bindings", func() {
it("creates a bindings from a path", func() {
Expect(libcnb.NewBindingsFromPath(path)).To(Equal(libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(path, "alpha"),
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{
"test-metadata-key": "test-metadata-value",
"test-metadata-key-trimmed": "test-metadata-value-trimmed",
"test-secret-key": "test-secret-value",
"test-secret-key-trimmed": "test-secret-value-trimmed",
},
},
libcnb.Binding{
Name: "bravo",
Path: filepath.Join(path, "bravo"),
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{
"test-metadata-key": "test-metadata-value",
"test-metadata-key-trimmed": "test-metadata-value-trimmed",
"test-secret-key": "test-secret-value",
"test-secret-key-trimmed": "test-secret-value-trimmed",
},
},
}))
})
it("returns empty bindings if environment variable is not set", func() {
Expect(libcnb.NewBindingsFromEnvironment()).To(Equal(libcnb.Bindings{}))
})
context("from environment", func() {
it.Before(func() {
Expect(os.Setenv(libcnb.EnvCNBBindings, path))
})
it.After(func() {
Expect(os.Unsetenv(libcnb.EnvCNBBindings))
})
it("creates bindings from path in $CNB_BINDINGS", func() {
Expect(libcnb.NewBindingsFromEnvironment()).To(Equal(libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(path, "alpha"),
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{
"test-metadata-key": "test-metadata-value",
"test-metadata-key-trimmed": "test-metadata-value-trimmed",
"test-secret-key": "test-secret-value",
"test-secret-key-trimmed": "test-secret-value-trimmed",
},
},
libcnb.Binding{
Name: "bravo",
Path: filepath.Join(path, "bravo"),
Type: "test-kind",
Provider: "test-provider",
Secret: map[string]string{
"test-metadata-key": "test-metadata-value",
"test-metadata-key-trimmed": "test-metadata-value-trimmed",
"test-secret-key": "test-secret-value",
"test-secret-key-trimmed": "test-secret-value-trimmed",
},
},
}))
})
})
}) })
}) })
@ -180,25 +351,21 @@ func testPlatform(t *testing.T, context spec.G, it spec.S) {
})) }))
}) })
it("creates an empty binding if path does not exist", func() { it("returns empty bindings if environment variable is not set", func() {
Expect(libcnb.NewBindingsFromPath("/path/doesnt/exist")).To(Equal(libcnb.Bindings{})) Expect(libcnb.NewBindingsFromEnvironment()).To(Equal(libcnb.Bindings{}))
})
it("returns empty bindings if SERVICE_BINDING_ROOT and CNB_PLATFORM_DIR are not set and /platform/bindings does not exist", func() {
Expect(libcnb.NewBindings(libcnb.DefaultPlatformBindingsLocation)).To(Equal(libcnb.Bindings{}))
}) })
context("from environment", func() { context("from environment", func() {
it.Before(func() {
Expect(os.Setenv(libcnb.EnvServiceBindings, path))
})
it.After(func() { it.After(func() {
Expect(os.Unsetenv(libcnb.EnvServiceBindings)) Expect(os.Unsetenv(libcnb.EnvServiceBindings))
Expect(os.Unsetenv("CNB_PLATFORM_DIR"))
}) })
it("creates bindings from path in SERVICE_BINDING_ROOT if both set", func() { it("creates bindings from path in SERVICE_BINDING_ROOT", func() {
Expect(os.Setenv(libcnb.EnvServiceBindings, path)) Expect(libcnb.NewBindingsFromEnvironment()).To(Equal(libcnb.Bindings{
Expect(os.Setenv("CNB_PLATFORM_DIR", "/does/not/exist"))
Expect(libcnb.NewBindings(libcnb.DefaultPlatformBindingsLocation)).To(Equal(libcnb.Bindings{
libcnb.Binding{ libcnb.Binding{
Name: "alpha", Name: "alpha",
Path: filepath.Join(path, "alpha"), Path: filepath.Join(path, "alpha"),
@ -215,26 +382,94 @@ func testPlatform(t *testing.T, context spec.G, it spec.S) {
}, },
})) }))
}) })
})
it("creates bindings from path in SERVICE_BINDING_ROOT if SERVICE_BINDING_ROOT not set", func() { context("from environment or path", func() {
Expect(os.Setenv("CNB_PLATFORM_DIR", filepath.Dir(path))) context("when SERVICE_BINDING_ROOT is defined but CNB_BINDINGS or the passed path does not exist", func() {
it.Before(func() {
Expect(os.Setenv(libcnb.EnvServiceBindings, path))
Expect(os.Setenv(libcnb.EnvCNBBindings, "does not exist"))
})
Expect(libcnb.NewBindings(libcnb.DefaultPlatformBindingsLocation)).To(Equal(libcnb.Bindings{ it.After(func() {
libcnb.Binding{ Expect(os.Unsetenv(libcnb.EnvServiceBindings))
Name: "alpha", Expect(os.Unsetenv(libcnb.EnvCNBBindings))
Path: filepath.Join(path, "alpha"), })
Type: "test-type",
Provider: "test-provider", it("creates bindings from path in SERVICE_BINDING_ROOT", func() {
Secret: map[string]string{"test-secret-key": "test-secret-value"}, Expect(libcnb.NewBindingsForBuild("random-path-that-does-not-exist")).To(Equal(libcnb.Bindings{
}, libcnb.Binding{
libcnb.Binding{ Name: "alpha",
Name: "bravo", Path: filepath.Join(path, "alpha"),
Path: filepath.Join(path, "bravo"), Type: "test-type",
Type: "test-type", Provider: "test-provider",
Provider: "test-provider", Secret: map[string]string{"test-secret-key": "test-secret-value"},
Secret: map[string]string{"test-secret-key": "test-secret-value"}, },
}, libcnb.Binding{
})) Name: "bravo",
Path: filepath.Join(path, "bravo"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
}))
})
})
context("when CNB_BINDINGS is defined but the path does not exist", func() {
it.Before(func() {
Expect(os.Setenv(libcnb.EnvCNBBindings, path))
})
it.After(func() {
Expect(os.Unsetenv(libcnb.EnvCNBBindings))
})
it("creates bindings from path in CNB_BINDINGS", func() {
Expect(libcnb.NewBindingsForBuild("random-path-that-does-not-exist")).To(Equal(libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(path, "alpha"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
libcnb.Binding{
Name: "bravo",
Path: filepath.Join(path, "bravo"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
}))
})
})
context("when SERVICE_BINDING_ROOT and CNB_BINDINGS is not defined but the path exists", func() {
it("creates bindings from the given path", func() {
Expect(libcnb.NewBindingsForBuild(platformPath)).To(Equal(libcnb.Bindings{
libcnb.Binding{
Name: "alpha",
Path: filepath.Join(path, "alpha"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
libcnb.Binding{
Name: "bravo",
Path: filepath.Join(path, "bravo"),
Type: "test-type",
Provider: "test-provider",
Secret: map[string]string{"test-secret-key": "test-secret-value"},
},
}))
})
})
context("when no valid binding variable is set", func() {
it("returns an an empty binding", func() {
Expect(libcnb.NewBindingsForBuild("does-not-exist")).To(Equal(libcnb.Bindings{}))
})
}) })
}) })
}) })

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package log_test package poet_test
import ( import (
"testing" "testing"
@ -24,7 +24,7 @@ import (
) )
func TestUnit(t *testing.T) { func TestUnit(t *testing.T) {
suite := spec.New("libcnb/log", spec.Report(report.Terminal{})) suite := spec.New("libcnb/poet", spec.Report(report.Terminal{}))
suite("PlainLogger", testLogger) suite("Logger", testLogger)
suite.Run(t) suite.Run(t)
} }

151
poet/logger.go Normal file
View File

@ -0,0 +1,151 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package poet
import (
"fmt"
"io"
"os"
"strings"
)
// Logger logs messages to a writer.
type Logger struct {
debug io.Writer
info io.Writer
}
// Option is a function that configures a Logger.
type Option func(Logger) Logger
// WithDebug configures the debug Writer.
func WithDebug(writer io.Writer) Option {
return func(logger Logger) Logger {
logger.debug = writer
return logger
}
}
// NewLoggerWithOptions create a new instance of Logger. It configures the Logger with options.
func NewLoggerWithOptions(writer io.Writer, options ...Option) Logger {
l := Logger{
info: writer,
}
for _, option := range options {
l = option(l)
}
return l
}
// NewLogger creates a new instance of Logger. It configures debug logging if $BP_DEBUG is set.
func NewLogger(writer io.Writer) Logger {
var options []Option
// check for presence and value of log level environment variable
options = LogLevel(options, writer)
return NewLoggerWithOptions(writer, options...)
}
func LogLevel(options []Option, writer io.Writer) []Option {
// Check for older log level env variable
_, dbSet := os.LookupEnv("BP_DEBUG")
// Then check for common buildpack log level env variable - if either are set to DEBUG/true, enable Debug Writer
if level, ok := os.LookupEnv("BP_LOG_LEVEL"); (ok && strings.ToLower(level) == "debug") || dbSet {
options = append(options, WithDebug(writer))
}
return options
}
// Debug formats using the default formats for its operands and writes to the configured debug writer. Spaces are added
// between operands when neither is a string.
func (l Logger) Debug(a ...interface{}) {
if !l.IsDebugEnabled() {
return
}
l.print(l.debug, a...)
}
// Debugf formats according to a format specifier and writes to the configured debug writer.
func (l Logger) Debugf(format string, a ...interface{}) {
if !l.IsDebugEnabled() {
return
}
l.printf(l.debug, format, a...)
}
// DebugWriter returns the configured debug writer.
func (l Logger) DebugWriter() io.Writer {
return l.debug
}
// IsDebugEnabled indicates whether debug logging is enabled.
func (l Logger) IsDebugEnabled() bool {
return l.debug != nil
}
// Info formats using the default formats for its operands and writes to the configured info writer. Spaces are added
// between operands when neither is a string.
func (l Logger) Info(a ...interface{}) {
if !l.IsInfoEnabled() {
return
}
l.print(l.info, a...)
}
// Infof formats according to a format specifier and writes to the configured info writer.
func (l Logger) Infof(format string, a ...interface{}) {
if !l.IsInfoEnabled() {
return
}
l.printf(l.info, format, a...)
}
// InfoWriter returns the configured info writer.
func (l Logger) InfoWriter() io.Writer {
return l.info
}
// IsInfoEnabled indicates whether info logging is enabled.
func (l Logger) IsInfoEnabled() bool {
return l.info != nil
}
func (Logger) print(writer io.Writer, a ...interface{}) {
s := fmt.Sprint(a...)
if !strings.HasSuffix(s, "\n") {
s += "\n"
}
_, _ = fmt.Fprint(writer, s)
}
func (Logger) printf(writer io.Writer, format string, a ...interface{}) {
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
_, _ = fmt.Fprintf(writer, format, a...)
}

View File

@ -14,18 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
package log_test package poet_test
import ( import (
"bytes" "bytes"
"io"
"os" "os"
"testing" "testing"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/buildpacks/libcnb/v2/log" "github.com/buildpacks/libcnb/poet"
) )
func testLogger(t *testing.T, context spec.G, it spec.S) { func testLogger(t *testing.T, context spec.G, it spec.S) {
@ -33,7 +32,7 @@ func testLogger(t *testing.T, context spec.G, it spec.S) {
Expect = NewWithT(t).Expect Expect = NewWithT(t).Expect
b *bytes.Buffer b *bytes.Buffer
l log.PlainLogger l poet.Logger
) )
it.Before(func() { it.Before(func() {
@ -42,26 +41,18 @@ func testLogger(t *testing.T, context spec.G, it spec.S) {
context("without BP_DEBUG", func() { context("without BP_DEBUG", func() {
it.Before(func() { it.Before(func() {
l = log.New(b) l = poet.NewLogger(b)
}) })
it("does not configure debug", func() { it("does not configure debug", func() {
Expect(l.IsDebugEnabled()).To(BeFalse()) Expect(l.IsDebugEnabled()).To(BeFalse())
}) })
it("does not return nil debug writer", func() {
Expect(l.DebugWriter()).To(Not(BeNil()))
})
it("does not return non-discard writer", func() {
Expect(l.DebugWriter()).To(Equal(io.Discard))
})
}) })
context("with BP_DEBUG", func() { context("with BP_DEBUG", func() {
it.Before(func() { it.Before(func() {
Expect(os.Setenv("BP_DEBUG", "")).To(Succeed()) Expect(os.Setenv("BP_DEBUG", "")).To(Succeed())
l = log.New(b) l = poet.NewLogger(b)
}) })
it.After(func() { it.After(func() {
@ -69,14 +60,14 @@ func testLogger(t *testing.T, context spec.G, it spec.S) {
}) })
it("configures debug", func() { it("configures debug", func() {
Expect(l.IsDebugEnabled()).To(BeFalse()) Expect(l.IsDebugEnabled()).To(BeTrue())
}) })
}) })
context("with BP_LOG_LEVEL set to DEBUG", func() { context("with BP_LOG_LEVEL set to DEBUG", func() {
it.Before(func() { it.Before(func() {
Expect(os.Setenv("BP_LOG_LEVEL", "DEBUG")).To(Succeed()) Expect(os.Setenv("BP_LOG_LEVEL", "DEBUG")).To(Succeed())
l = log.New(b) l = poet.NewLogger(b)
}) })
it.After(func() { it.After(func() {
@ -88,14 +79,51 @@ func testLogger(t *testing.T, context spec.G, it spec.S) {
}) })
}) })
context("with debug enabled", func() { context("with debug disabled", func() {
it.Before(func() { it.Before(func() {
Expect(os.Setenv("BP_LOG_LEVEL", "DEBUG")).To(Succeed()) l = poet.NewLoggerWithOptions(b)
l = log.New(b)
}) })
it.After(func() { it("does not write debug log", func() {
Expect(os.Unsetenv("BP_LOG_LEVEL")).To(Succeed()) l.Debug("test-message")
Expect(b.String()).To(Equal(""))
})
it("does not write debugf log", func() {
l.Debugf("test-%s", "message")
Expect(b.String()).To(Equal(""))
})
it("does not return debug writer", func() {
Expect(l.DebugWriter()).To(BeNil())
})
it("indicates that debug is not enabled", func() {
Expect(l.IsDebugEnabled()).To(BeFalse())
})
it("writes info log", func() {
l.Info("test-message")
Expect(b.String()).To(Equal("test-message\n"))
})
it("writes infof log", func() {
l.Infof("test-%s", "message")
Expect(b.String()).To(Equal("test-message\n"))
})
it("returns info writer", func() {
Expect(l.InfoWriter()).NotTo(BeNil())
})
it("indicates that info is enabled", func() {
Expect(l.IsInfoEnabled()).To(BeTrue())
})
})
context("with debug enabled", func() {
it.Before(func() {
l = poet.NewLoggerWithOptions(b, poet.WithDebug(b))
}) })
it("writes debug log", func() { it("writes debug log", func() {
@ -108,14 +136,30 @@ func testLogger(t *testing.T, context spec.G, it spec.S) {
Expect(b.String()).To(Equal("test-message\n")) Expect(b.String()).To(Equal("test-message\n"))
}) })
it("writes debug directly", func() { it("returns debug writer", func() {
_, err := l.DebugWriter().Write([]byte("test-message\n")) Expect(l.DebugWriter()).NotTo(BeNil())
Expect(err).NotTo(HaveOccurred())
Expect(b.String()).To(Equal("test-message\n"))
}) })
it("indicates that debug is enabled", func() { it("indicates that debug is enabled", func() {
Expect(l.IsDebugEnabled()).To(BeTrue()) Expect(l.IsDebugEnabled()).To(BeTrue())
}) })
it("writes info log", func() {
l.Info("test-message")
Expect(b.String()).To(Equal("test-message\n"))
})
it("writes infof log", func() {
l.Infof("test-%s", "message")
Expect(b.String()).To(Equal("test-message\n"))
})
it("returns info writer", func() {
Expect(l.InfoWriter()).NotTo(BeNil())
})
it("indicates that info is enabled", func() {
Expect(l.IsInfoEnabled()).To(BeTrue())
})
}) })
} }

View File

@ -1,36 +0,0 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb
// Process represents metadata about a type of command that can be run.
type Process struct {
// Type is the type of the process.
Type string `toml:"type"`
// Command is the command of the process.
Command []string `toml:"command"`
// Arguments are arguments to the command.
Arguments []string `toml:"args"`
// WorkingDirectory is a directory to execute the command in, removes the need to use a shell environment to CD into working directory
WorkingDirectory string `toml:"working-dir,omitempty"`
// Default can be set to true to indicate that the process
// type being defined should be the default process type for the app image.
Default bool `toml:"default,omitempty"`
}

View File

@ -1,24 +0,0 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb
// Slice represents metadata about a slice.
type Slice struct {
// Paths are the contents of the slice.
Paths []string `toml:"paths"`
}

View File

@ -1,24 +0,0 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcnb
// Store represents the contents of store.toml
type Store struct {
// Metadata represents the persistent metadata.
Metadata map[string]interface{} `toml:"metadata"`
}

View File

@ -1,190 +1,184 @@
module github.com/buildpacks/libcnb/tools/v2 module github.com/buildpacks/libcnb/tools
go 1.22.1 go 1.20
toolchain go1.23.2 require golang.org/x/tools v0.8.0
require golang.org/x/tools v0.26.0 require github.com/golangci/golangci-lint v1.52.2
require github.com/golangci/golangci-lint v1.61.0
require ( require (
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
4d63.com/gochecknoglobals v0.2.1 // indirect 4d63.com/gochecknoglobals v0.2.1 // indirect
github.com/4meepo/tagalign v1.3.4 // indirect github.com/Abirdcfly/dupword v0.0.11 // indirect
github.com/Abirdcfly/dupword v0.1.1 // indirect github.com/Antonboom/errname v0.1.9 // indirect
github.com/Antonboom/errname v0.1.13 // indirect github.com/Antonboom/nilnil v0.1.4 // indirect
github.com/Antonboom/nilnil v0.1.9 // indirect github.com/BurntSushi/toml v1.2.1 // indirect
github.com/Antonboom/testifylint v1.4.3 // indirect
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/Crocmagnon/fatcontext v0.5.2 // indirect
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect github.com/OpenPeeDeeP/depguard v1.1.1 // indirect
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect github.com/alingse/asasalint v0.0.11 // indirect
github.com/ashanbrown/forbidigo v1.6.0 // indirect github.com/ashanbrown/forbidigo v1.5.1 // indirect
github.com/ashanbrown/makezero v1.1.1 // indirect github.com/ashanbrown/makezero v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.1 // indirect github.com/bkielbasa/cyclop v1.2.0 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.4.1 // indirect github.com/bombsimon/wsl/v3 v3.4.0 // indirect
github.com/breml/bidichk v0.2.7 // indirect github.com/breml/bidichk v0.2.4 // indirect
github.com/breml/errchkjson v0.3.6 // indirect github.com/breml/errchkjson v0.3.1 // indirect
github.com/butuzov/ireturn v0.3.0 // indirect github.com/butuzov/ireturn v0.1.1 // indirect
github.com/butuzov/mirror v1.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/catenacyber/perfsprint v0.7.1 // indirect
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/charithe/durationcheck v0.0.10 // indirect github.com/charithe/durationcheck v0.0.10 // indirect
github.com/chavacava/garif v0.1.0 // indirect github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8 // indirect
github.com/ckaznocha/intrange v0.2.0 // indirect
github.com/curioswitch/go-reassign v0.2.0 // indirect github.com/curioswitch/go-reassign v0.2.0 // indirect
github.com/daixiang0/gci v0.13.5 // indirect github.com/daixiang0/gci v0.10.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/denis-tingaikin/go-header v0.4.3 // indirect
github.com/ettle/strcase v0.2.0 // indirect github.com/esimonov/ifshort v1.0.4 // indirect
github.com/fatih/color v1.17.0 // indirect github.com/ettle/strcase v0.1.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.5 // indirect github.com/firefart/nonamedreturns v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.6 // indirect github.com/go-critic/go-critic v0.7.0 // indirect
github.com/go-critic/go-critic v0.11.4 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect github.com/go-toolsmith/astequal v1.1.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect github.com/gofrs/flock v0.8.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 // indirect github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe // indirect
github.com/golangci/misspell v0.6.0 // indirect github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2 // indirect
github.com/golangci/modinfo v0.3.4 // indirect github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect
github.com/golangci/plugin-module-register v0.1.1 // indirect github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect
github.com/golangci/revgrep v0.5.3 // indirect github.com/golangci/misspell v0.4.0 // indirect
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
github.com/gordonklaus/ineffassign v0.1.0 // indirect github.com/google/go-cmp v0.5.9 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.4.2 // indirect github.com/gostaticanalysis/comment v1.4.2 // indirect
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.1 // indirect github.com/gostaticanalysis/nilerr v0.1.1 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jgautheron/goconst v1.7.1 // indirect github.com/jgautheron/goconst v1.5.1 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
github.com/jjti/go-spancheck v0.6.2 // indirect
github.com/julz/importas v0.1.0 // indirect github.com/julz/importas v0.1.0 // indirect
github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect github.com/junk1tm/musttag v0.5.0 // indirect
github.com/kisielk/errcheck v1.7.0 // indirect github.com/kisielk/errcheck v1.6.3 // indirect
github.com/kkHAIKE/contextcheck v1.1.5 // indirect github.com/kisielk/gotool v1.0.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.4 // indirect
github.com/kulti/thelper v0.6.3 // indirect github.com/kulti/thelper v0.6.3 // indirect
github.com/kunwardeep/paralleltest v1.0.10 // indirect github.com/kunwardeep/paralleltest v1.0.6 // indirect
github.com/kyoh86/exportloopref v0.1.11 // indirect github.com/kyoh86/exportloopref v0.1.11 // indirect
github.com/lasiar/canonicalheader v1.1.1 // indirect github.com/ldez/gomoddirectives v0.2.3 // indirect
github.com/ldez/gomoddirectives v0.2.4 // indirect github.com/ldez/tagliatelle v0.4.0 // indirect
github.com/ldez/tagliatelle v0.5.0 // indirect github.com/leonklingele/grouper v1.1.1 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lufeee/execinquery v1.2.1 // indirect github.com/lufeee/execinquery v1.2.1 // indirect
github.com/macabu/inamedparam v0.1.3 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.1 // indirect github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mgechev/revive v1.3.9 // indirect github.com/mbilski/exhaustivestruct v1.2.0 // indirect
github.com/mgechev/revive v1.3.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moricho/tparallel v0.3.2 // indirect github.com/moricho/tparallel v0.3.1 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
github.com/nishanths/exhaustive v0.10.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.16.2 // indirect github.com/nunnatsa/ginkgolinter v0.11.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pelletier/go-toml/v2 v2.0.7 // indirect
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v1.6.0 // indirect github.com/polyfloyd/go-errorlint v1.4.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.9.0 // indirect
github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect github.com/quasilyte/go-ruleguard v0.3.19 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/ryancurrah/gomodguard v1.3.5 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/ryancurrah/gomodguard v1.3.0 // indirect
github.com/ryanrolds/sqlclosecheck v0.4.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.27.0 // indirect github.com/sashamelentyev/usestdlibvars v1.23.0 // indirect
github.com/securego/gosec/v2 v2.21.2 // indirect github.com/securego/gosec/v2 v2.15.0 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.0 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sivchari/tenv v1.10.0 // indirect github.com/sivchari/nosnakecase v1.7.0 // indirect
github.com/sivchari/tenv v1.7.1 // indirect
github.com/sonatard/noctx v0.0.2 // indirect github.com/sonatard/noctx v0.0.2 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.11.0 // indirect github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.12.0 // indirect github.com/spf13/viper v1.15.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect github.com/stretchr/testify v1.8.2 // indirect
github.com/subosito/gotenv v1.4.1 // indirect github.com/subosito/gotenv v1.4.2 // indirect
github.com/sylvia7788/contextcheck v1.0.9 // indirect
github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect
github.com/tdakkota/asciicheck v0.2.0 // indirect github.com/tdakkota/asciicheck v0.2.0 // indirect
github.com/tetafro/godot v1.4.17 // indirect github.com/tetafro/godot v1.4.11 // indirect
github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect github.com/timakin/bodyclose v0.0.0-20221125081123-e39cf3fc478e // indirect
github.com/timonwong/loggercheck v0.9.4 // indirect github.com/timonwong/loggercheck v0.9.4 // indirect
github.com/tomarrell/wrapcheck/v2 v2.9.0 // indirect github.com/tomarrell/wrapcheck/v2 v2.8.1 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/ultraware/funlen v0.1.0 // indirect github.com/ultraware/funlen v0.0.3 // indirect
github.com/ultraware/whitespace v0.1.1 // indirect github.com/ultraware/whitespace v0.0.5 // indirect
github.com/uudashr/gocognit v1.1.3 // indirect github.com/uudashr/gocognit v1.0.6 // indirect
github.com/xen0n/gosmopolitan v1.2.2 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.3.0 // indirect github.com/yeya24/promlinter v0.2.0 // indirect
github.com/ykadowak/zerologlint v0.1.5 // indirect gitlab.com/bosi/decorder v0.2.3 // indirect
gitlab.com/bosi/decorder v0.4.2 // indirect go.uber.org/atomic v1.10.0 // indirect
go-simpler.org/musttag v0.12.2 // indirect go.uber.org/multierr v1.11.0 // indirect
go-simpler.org/sloglint v0.7.2 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect go.uber.org/zap v1.24.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect golang.org/x/exp/typeparams v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/mod v0.21.0 // indirect golang.org/x/mod v0.10.0 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.26.0 // indirect golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.18.0 // indirect golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/tools v0.5.1 // indirect honnef.co/go/tools v0.4.3 // indirect
mvdan.cc/gofumpt v0.7.0 // indirect mvdan.cc/gofumpt v0.5.0 // indirect
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8 // indirect
) )

File diff suppressed because it is too large Load Diff