├── .gitignore ├── LICENSE ├── MIGRATION-v1-to-v2.md ├── Makefile ├── NOTICE ├── README.md ├── build.go ├── build_plan.go ├── build_test.go ├── build_toml.go ├── buildpack.go ├── buildpack_plan.go ├── buildpack_test.go ├── config.go ├── detect.go ├── detect_test.go ├── environment.go ├── environment_test.go ├── examples ├── build_test.go ├── detect_test.go └── generate_test.go ├── exec_d.go ├── exec_d_test.go ├── extension.go ├── extension_test.go ├── generate.go ├── generate_test.go ├── go.mod ├── go.sum ├── golangci.yaml ├── init_test.go ├── internal ├── config_map.go ├── config_map_test.go ├── directory_contents.go ├── directory_contents_test.go ├── environment_writer.go ├── environment_writer_test.go ├── execd_writer.go ├── execd_writer_test.go ├── exit_handler.go ├── exit_handler_test.go ├── formatter.go ├── formatter_test.go ├── init_test.go ├── match_toml.go ├── toml_writer.go └── toml_writer_test.go ├── label.go ├── launch_toml.go ├── layer.go ├── layer_test.go ├── log ├── formatter.go ├── init_test.go ├── logger.go ├── logger_test.go └── mocks │ ├── directory_content_formatter.go │ └── logger.go ├── main.go ├── main_test.go ├── mocks ├── environment_writer.go ├── exec_d.go ├── exec_d_writer.go ├── exit_handler.go └── toml_writer.go ├── platform.go ├── platform_test.go ├── process.go ├── slice.go ├── store.go ├── testdata └── vcap_services.json └── tools ├── go.mod ├── go.sum └── tools.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2020 the original author or authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | bin/ 16 | linux/ 17 | dependencies/ 18 | package/ 19 | scratch/ 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2018 The Cloud Native Buildpacks Authors 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | https://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MIGRATION-v1-to-v2.md: -------------------------------------------------------------------------------- 1 | # Migration Guide 2 | 3 | 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. 4 | 5 | ## Buildpack API Support 6 | 7 | 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 8 | 9 | ## Removal of LayerContributor 10 | 11 | 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. 12 | 13 | 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. 14 | 15 | ## Replace Builder and Detector with Functions 16 | 17 | 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. 18 | 19 | The path forward is to remove your Builder and Detector structs and pass the method directly into libcnb. 20 | 21 | ## Rename `poet` to `log` 22 | 23 | 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`. 24 | 25 | 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. 26 | 27 | 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. 28 | 29 | ## Remove Deprecated Pre-Buildpack API 0.7 BOM 30 | 31 | 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). 32 | 33 | ## Path to Source Code Changed 34 | 35 | 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`. 36 | 37 | ## Remove Deprecated CNB Binding Support 38 | 39 | 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. 40 | 41 | ## Remove Shell-Specific Logic & Overridable Process Arguments 42 | 43 | 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. 44 | 45 | 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`. 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOCMD?=go 3 | GO_VERSION=$(shell go list -m -f "{{.GoVersion}}") 4 | PACKAGE_BASE=github.com/harshfelony/libcnb/v2 5 | 6 | all: test 7 | 8 | install-goimports: 9 | @echo "> Installing goimports..." 10 | cd tools && $(GOCMD) install golang.org/x/tools/cmd/goimports 11 | 12 | format: install-goimports 13 | @echo "> Formating code..." 14 | @goimports -l -w -local ${PACKAGE_BASE} . 15 | 16 | install-golangci-lint: 17 | @echo "> Installing golangci-lint..." 18 | cd tools && $(GOCMD) install github.com/golangci/golangci-lint/cmd/golangci-lint 19 | 20 | lint: install-golangci-lint 21 | @echo "> Linting code..." 22 | @golangci-lint run -c golangci.yaml 23 | 24 | test: format lint 25 | $(GOCMD) test -parallel=1 -count=1 -v ./... 26 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | libcnb 2 | 3 | Copyright (c) the original author or authors. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `github.com/harshfelony/libcnb` 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/harshfelony/libcnb.svg)](https://pkg.go.dev/github.com/harshfelony/libcnb) 4 | 5 | `libcnb` is a Go language binding of the Cloud Native Buildpacks API. It is a non-opinionated implementation adding language constructs and convenience methods for working with the API. 6 | 7 | > For operations such as building an app, creating a builder or packaging a buildpack, you may use [`pack`](https://github.com/buildpacks/pack) as a Go library. 8 | 9 | ## Usage 10 | 11 | #### Installation 12 | 13 | ``` 14 | go get github.com/harshfelony/libcnb 15 | ``` 16 | 17 | or for the v2 alpha 18 | 19 | ``` 20 | go get github.com/harshfelony/libcnb@v2.0.0-alpha.1 21 | ``` 22 | 23 | #### Docs 24 | 25 | https://pkg.go.dev/github.com/harshfelony/libcnb?tab=doc 26 | 27 | ## License 28 | This library is released under version 2.0 of the [Apache License][a]. 29 | 30 | [a]: https://www.apache.org/licenses/LICENSE-2.0 31 | 32 | -------------------------------------------------------------------------------- /build_plan.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | // BuildPlanProvide represents a dependency provided by a buildpack. 20 | type BuildPlanProvide struct { 21 | // Name is the name of the dependency. 22 | Name string `toml:"name"` 23 | } 24 | 25 | // BuildPlanRequire represents a dependency required by a buildpack. 26 | type BuildPlanRequire struct { 27 | // Name is the name of the dependency. 28 | Name string `toml:"name"` 29 | 30 | // Metadata is the metadata for the dependency. Optional. 31 | Metadata map[string]interface{} `toml:"metadata,omitempty"` 32 | } 33 | 34 | // BuildPlan represents the provisions and requirements of a buildpack during detection. 35 | type BuildPlan struct { 36 | // Provides is the dependencies provided by the buildpack. 37 | Provides []BuildPlanProvide `toml:"provides,omitempty"` 38 | 39 | // Requires is the dependencies required by the buildpack. 40 | Requires []BuildPlanRequire `toml:"requires,omitempty"` 41 | } 42 | 43 | // BuildPlans represents a collection of build plans produced by a buildpack during detection. 44 | type BuildPlans struct { 45 | // BuildPlan is the first build plan. 46 | BuildPlan 47 | 48 | // Or is the collection of other build plans. 49 | Or []BuildPlan `toml:"or,omitempty"` 50 | } 51 | -------------------------------------------------------------------------------- /build_toml.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | // BuildTOML represents the contents of build.toml. 20 | type BuildTOML struct { 21 | // Unmet is a collection of buildpack plan entries that should be passed through to subsequent providers. 22 | Unmet []UnmetPlanEntry 23 | } 24 | 25 | func (b BuildTOML) isEmpty() bool { 26 | return len(b.Unmet) == 0 27 | } 28 | -------------------------------------------------------------------------------- /buildpack.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | // BuildpackInfo is information about the buildpack. 20 | type BuildpackInfo struct { 21 | // ID is the ID of the buildpack. 22 | ID string `toml:"id"` 23 | 24 | // Name is the name of the buildpack. 25 | Name string `toml:"name"` 26 | 27 | // Version is the version of the buildpack. 28 | Version string `toml:"version"` 29 | 30 | // Homepage is the homepage of the buildpack. 31 | Homepage string `toml:"homepage"` 32 | 33 | // ClearEnvironment is whether the environment should be clear of user-configured environment variables. 34 | ClearEnvironment bool `toml:"clear-env"` 35 | 36 | // Description is a string describing the buildpack. 37 | Description string `toml:"description"` 38 | 39 | // Keywords is a list of words that are associated with the buildpack. 40 | Keywords []string `toml:"keywords"` 41 | 42 | // Licenses a list of buildpack licenses. 43 | Licenses []License `toml:"licenses"` 44 | 45 | // SBOM is the list of supported SBOM media types 46 | SBOMFormats []string `toml:"sbom-formats"` 47 | } 48 | 49 | // License contains information about a Software License 50 | // governing the use or redistribution of a buildpack 51 | type License struct { 52 | // Type is the identifier for the license. 53 | // It MAY use the SPDX 2.1 license expression, but is not limited to identifiers in the SPDX Licenses List. 54 | Type string `toml:"type"` 55 | 56 | // URI may be specified in lieu of or in addition to type to point to the license 57 | // if this buildpack is using a nonstandard license. 58 | URI string `toml:"uri"` 59 | } 60 | 61 | // BuildpackOrderBuildpack is a buildpack within in a buildpack order group. 62 | type BuildpackOrderBuildpack struct { 63 | // ID is the id of the buildpack. 64 | ID string `toml:"id"` 65 | 66 | // Version is the version of the buildpack. 67 | Version string `toml:"version"` 68 | 69 | // Optional is whether the buildpack is optional within the buildpack. 70 | Optional bool `toml:"optional"` 71 | } 72 | 73 | // BuildpackOrder is an order definition in the buildpack. 74 | type BuildpackOrder struct { 75 | // Groups is the collection of groups within the order. 76 | Groups []BuildpackOrderBuildpack `toml:"group"` 77 | } 78 | 79 | // Deprecated: BuildpackStack is a stack supported by the buildpack. 80 | type BuildpackStack struct { 81 | // ID is the id of the stack. 82 | ID string `toml:"id"` 83 | } 84 | 85 | // TargetDistro is the supported target distro 86 | type TargetDistro struct { 87 | // Name is the name of the supported distro. 88 | Name string `toml:"name"` 89 | 90 | // Version is the version of the supported distro. 91 | Version string `toml:"version"` 92 | } 93 | 94 | // TargetInfo is the supported target 95 | type TargetInfo struct { 96 | // OS is the supported os. 97 | OS string `toml:"os"` 98 | 99 | // Arch is the supported architecture. 100 | Arch string `toml:"arch"` 101 | 102 | // Variant is the supported variant of the architecture. 103 | Variant string `toml:"variant"` 104 | } 105 | 106 | // Target is a target supported by the buildpack. 107 | type Target struct { 108 | TargetInfo 109 | 110 | // Distros is the collection of distros associated with the target. 111 | Distros []TargetDistro `toml:"distros"` 112 | } 113 | 114 | // Buildpack is the contents of the buildpack.toml file. 115 | type Buildpack struct { 116 | // API is the api version expected by the buildpack. 117 | API string `toml:"api"` 118 | 119 | // Info is information about the buildpack. 120 | Info BuildpackInfo `toml:"buildpack"` 121 | 122 | // Path is the path to the buildpack. 123 | Path string `toml:"-"` 124 | 125 | // Deprecated: Stacks is the collection of stacks supported by the buildpack. 126 | Stacks []BuildpackStack `toml:"stacks"` 127 | 128 | // Targets is the collection of targets supported by the buildpack. 129 | Targets []Target `toml:"targets"` 130 | 131 | // Metadata is arbitrary metadata attached to the buildpack. 132 | Metadata map[string]interface{} `toml:"metadata"` 133 | } 134 | -------------------------------------------------------------------------------- /buildpack_plan.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | // BuildpackPlan represents a buildpack plan. 20 | type BuildpackPlan struct { 21 | 22 | // Entries represents all the buildpack plan entries. 23 | Entries []BuildpackPlanEntry `toml:"entries,omitempty"` 24 | } 25 | 26 | // BuildpackPlanEntry represents an entry in the buildpack plan. 27 | type BuildpackPlanEntry struct { 28 | // Name represents the name of the entry. 29 | Name string `toml:"name"` 30 | 31 | // Metadata is the metadata of the entry. Optional. 32 | Metadata map[string]interface{} `toml:"metadata,omitempty"` 33 | } 34 | 35 | // UnmetPlanEntry denotes an unmet buildpack plan entry. When a buildpack returns an UnmetPlanEntry 36 | // in the BuildResult, any BuildpackPlanEntry with a matching Name will be provided to subsequent 37 | // providers. 38 | type UnmetPlanEntry struct { 39 | // Name represents the name of the entry. 40 | Name string `toml:"name"` 41 | } 42 | -------------------------------------------------------------------------------- /buildpack_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb_test 18 | 19 | import ( 20 | "bytes" 21 | "testing" 22 | 23 | "github.com/BurntSushi/toml" 24 | "github.com/sclevine/spec" 25 | 26 | "github.com/harshfelony/libcnb/v2" 27 | 28 | . "github.com/onsi/gomega" 29 | ) 30 | 31 | func testBuildpackTOML(t *testing.T, _ spec.G, it spec.S) { 32 | var ( 33 | Expect = NewWithT(t).Expect 34 | ) 35 | 36 | it("does not serialize the Path field", func() { 37 | bp := libcnb.Buildpack{ 38 | API: "0.8", 39 | Info: libcnb.BuildpackInfo{ 40 | ID: "test-buildpack/sample", 41 | Name: "sample", 42 | }, 43 | Path: "../buildpack", 44 | } 45 | 46 | output := &bytes.Buffer{} 47 | 48 | Expect(toml.NewEncoder(output).Encode(bp)).To(Succeed()) 49 | Expect(output.String()).NotTo(Or(ContainSubstring("Path = "), ContainSubstring("path = "))) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | import ( 20 | "os" 21 | 22 | "github.com/harshfelony/libcnb/v2/internal" 23 | "github.com/harshfelony/libcnb/v2/log" 24 | ) 25 | 26 | //go:generate mockery --name EnvironmentWriter --case=underscore 27 | 28 | // EnvironmentWriter is the interface implemented by a type that wants to serialize a map of environment variables to 29 | // the file system. 30 | type EnvironmentWriter interface { 31 | 32 | // Write is called with the path to a directory where the environment variables should be serialized to and the 33 | // environment variables to serialize to that directory. 34 | Write(dir string, environment map[string]string) error 35 | } 36 | 37 | //go:generate mockery --name ExitHandler --case=underscore 38 | 39 | // ExitHandler is the interface implemented by a type that wants to handle exit behavior when a buildpack encounters an 40 | // error. 41 | type ExitHandler interface { 42 | 43 | // Error is called when an error is encountered. 44 | Error(error) 45 | 46 | // Fail is called when a buildpack fails. 47 | Fail() 48 | 49 | // Pass is called when a buildpack passes. 50 | Pass() 51 | } 52 | 53 | //go:generate mockery --name TOMLWriter --case=underscore 54 | 55 | // TOMLWriter is the interface implemented by a type that wants to serialize an object to a TOML file. 56 | type TOMLWriter interface { 57 | 58 | // Write is called with the path that a TOML file should be written to and the object to serialize to that file. 59 | Write(path string, value interface{}) error 60 | } 61 | 62 | //go:generate mockery --name ExecDWriter --case=underscore 63 | 64 | // ExecDWriter is the interface implemented by a type that wants to write exec.d output to file descriptor 3. 65 | type ExecDWriter interface { 66 | 67 | // Write is called with the map of environment value key value 68 | // pairs that will be written out 69 | Write(value map[string]string) error 70 | } 71 | 72 | // Config is an object that contains configurable properties for execution. 73 | type Config struct { 74 | arguments []string 75 | dirContentFormatter log.DirectoryContentFormatter 76 | environmentWriter EnvironmentWriter 77 | execdWriter ExecDWriter 78 | exitHandler ExitHandler 79 | logger log.Logger 80 | tomlWriter TOMLWriter 81 | contentWriter internal.DirectoryContentsWriter 82 | extension bool 83 | } 84 | 85 | // Option is a function for configuring a Config instance. 86 | type Option func(config Config) Config 87 | 88 | // NewConfig will generate a config from the given set of options 89 | func NewConfig(options ...Option) Config { 90 | config := Config{} 91 | 92 | // apply defaults 93 | options = append([]Option{ 94 | WithArguments(os.Args), 95 | WithEnvironmentWriter(internal.EnvironmentWriter{}), 96 | WithExitHandler(internal.NewExitHandler()), 97 | WithLogger(log.New(os.Stdout)), 98 | WithTOMLWriter(internal.TOMLWriter{}), 99 | WithDirectoryContentFormatter(internal.NewPlainDirectoryContentFormatter()), 100 | }, options...) 101 | 102 | for _, opt := range options { 103 | config = opt(config) 104 | } 105 | 106 | config.contentWriter = internal.NewDirectoryContentsWriter(config.dirContentFormatter, config.logger.DebugWriter()) 107 | 108 | return config 109 | } 110 | 111 | // WithArguments creates an Option that sets a collection of arguments. 112 | func WithArguments(arguments []string) Option { 113 | return func(config Config) Config { 114 | config.arguments = arguments 115 | return config 116 | } 117 | } 118 | 119 | // WithEnvironmentWriter creates an Option that sets an EnvironmentWriter implementation. 120 | func WithEnvironmentWriter(environmentWriter EnvironmentWriter) Option { 121 | return func(config Config) Config { 122 | config.environmentWriter = environmentWriter 123 | return config 124 | } 125 | } 126 | 127 | // WithExitHandler creates an Option that sets an ExitHandler implementation. 128 | func WithExitHandler(exitHandler ExitHandler) Option { 129 | return func(config Config) Config { 130 | config.exitHandler = exitHandler 131 | return config 132 | } 133 | } 134 | 135 | // WithTOMLWriter creates an Option that sets a TOMLWriter implementation. 136 | func WithTOMLWriter(tomlWriter TOMLWriter) Option { 137 | return func(config Config) Config { 138 | config.tomlWriter = tomlWriter 139 | return config 140 | } 141 | } 142 | 143 | // WithExecDWriter creates an Option that sets a ExecDWriter implementation. 144 | func WithExecDWriter(execdWriter ExecDWriter) Option { 145 | return func(config Config) Config { 146 | config.execdWriter = execdWriter 147 | return config 148 | } 149 | } 150 | 151 | // WithLogger creates an Option that sets a ExecDWriter implementation. 152 | func WithLogger(logger log.Logger) Option { 153 | return func(config Config) Config { 154 | config.logger = logger 155 | return config 156 | } 157 | } 158 | 159 | // WithDirectoryContentFormatter creates an Option that sets a ExecDWriter implementation. 160 | func WithDirectoryContentFormatter(formatter log.DirectoryContentFormatter) Option { 161 | return func(config Config) Config { 162 | config.dirContentFormatter = formatter 163 | return config 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /detect.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | 25 | "github.com/BurntSushi/toml" 26 | "github.com/Masterminds/semver" 27 | 28 | "github.com/harshfelony/libcnb/v2/internal" 29 | "github.com/harshfelony/libcnb/v2/log" 30 | ) 31 | 32 | // DetectContext contains the inputs to detection. 33 | type DetectContext struct { 34 | 35 | // ApplicationPath is the location of the application source code as provided by 36 | // the lifecycle. 37 | ApplicationPath string 38 | 39 | // Buildpack is metadata about the buildpack from buildpack.toml (empty when processing an extension) 40 | Buildpack Buildpack 41 | 42 | // Extension is metadata about the extension from extension.toml (empty when processing a buildpack) 43 | Extension Extension 44 | 45 | // Logger is the way to write messages to the end user 46 | Logger log.Logger 47 | 48 | // Platform is the contents of the platform. 49 | Platform Platform 50 | 51 | // StackID is the ID of the stack. 52 | StackID string 53 | } 54 | 55 | // DetectResult contains the results of detection. 56 | type DetectResult struct { 57 | 58 | // Pass indicates whether detection has passed. 59 | Pass bool 60 | 61 | // Plans are the build plans contributed by the buildpack. 62 | Plans []BuildPlan 63 | } 64 | 65 | // DetectFunc takes a context and returns a result, performing buildpack detect behaviors. 66 | type DetectFunc func(context DetectContext) (DetectResult, error) 67 | 68 | // Detect is called by the main function of a buildpack, for detection. 69 | func Detect(detect DetectFunc, config Config) { 70 | var ( 71 | err error 72 | file string 73 | ok bool 74 | api string 75 | path string 76 | destination interface{} 77 | ) 78 | ctx := DetectContext{Logger: config.logger} 79 | 80 | var moduletype = "buildpack" 81 | if config.extension { 82 | moduletype = "extension" 83 | } 84 | 85 | ctx.ApplicationPath, err = os.Getwd() 86 | if err != nil { 87 | config.exitHandler.Error(fmt.Errorf("unable to get working directory\n%w", err)) 88 | return 89 | } 90 | 91 | if config.logger.IsDebugEnabled() { 92 | if err := config.contentWriter.Write("Application contents", ctx.ApplicationPath); err != nil { 93 | config.logger.Debugf("unable to write application contents\n%w", err) 94 | } 95 | } 96 | 97 | if !config.extension { 98 | if s, ok := os.LookupEnv(EnvBuildpackDirectory); ok { 99 | path = filepath.Clean(s) 100 | } else { 101 | config.exitHandler.Error(fmt.Errorf("unable to get CNB_BUILDPACK_DIR, not found")) 102 | return 103 | } 104 | ctx.Buildpack.Path = path 105 | destination = &ctx.Buildpack 106 | file = filepath.Join(ctx.Buildpack.Path, "buildpack.toml") 107 | } else { 108 | if s, ok := os.LookupEnv(EnvExtensionDirectory); ok { 109 | path = filepath.Clean(s) 110 | } else { 111 | config.exitHandler.Error(fmt.Errorf("unable to get CNB_EXTENSION_DIR, not found")) 112 | return 113 | } 114 | ctx.Extension.Path = path 115 | destination = &ctx.Extension 116 | file = filepath.Join(ctx.Extension.Path, "extension.toml") 117 | } 118 | 119 | if _, err = toml.DecodeFile(file, destination); err != nil && !os.IsNotExist(err) { 120 | config.exitHandler.Error(fmt.Errorf("unable to decode %s %s\n%w", moduletype, file, err)) 121 | return 122 | } 123 | config.logger.Debugf("%s: %+v", moduletype, ctx.Buildpack) 124 | 125 | if config.logger.IsDebugEnabled() { 126 | if err := config.contentWriter.Write(moduletype+" contents", path); err != nil { 127 | config.logger.Debugf("unable to write %s contents\n%w", moduletype, err) 128 | } 129 | } 130 | 131 | if config.extension { 132 | api = ctx.Extension.API 133 | } else { 134 | api = ctx.Buildpack.API 135 | } 136 | API, err := semver.NewVersion(api) 137 | if err != nil { 138 | config.exitHandler.Error(errors.New("version cannot be parsed")) 139 | return 140 | } 141 | 142 | compatVersionCheck, _ := semver.NewConstraint(fmt.Sprintf(">= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion)) 143 | if !compatVersionCheck.Check(API) { 144 | if MinSupportedBPVersion == MaxSupportedBPVersion { 145 | config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack API == %s", MinSupportedBPVersion)) 146 | return 147 | } 148 | 149 | config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion)) 150 | return 151 | } 152 | 153 | var buildPlanPath string 154 | 155 | ctx.Platform.Path, ok = os.LookupEnv(EnvPlatformDirectory) 156 | if !ok { 157 | config.exitHandler.Error(fmt.Errorf("expected CNB_PLATFORM_DIR to be set")) 158 | return 159 | } 160 | 161 | buildPlanPath, ok = os.LookupEnv(EnvDetectPlanPath) 162 | if !ok { 163 | config.exitHandler.Error(fmt.Errorf("expected CNB_BUILD_PLAN_PATH to be set")) 164 | return 165 | } 166 | 167 | if config.logger.IsDebugEnabled() { 168 | if err := config.contentWriter.Write("Platform contents", ctx.Platform.Path); err != nil { 169 | config.logger.Debugf("unable to write platform contents\n%w", err) 170 | } 171 | } 172 | 173 | file = filepath.Join(ctx.Platform.Path, "bindings") 174 | if ctx.Platform.Bindings, err = NewBindings(ctx.Platform.Path); err != nil { 175 | config.exitHandler.Error(fmt.Errorf("unable to read platform bindings %s\n%w", file, err)) 176 | return 177 | } 178 | config.logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings) 179 | 180 | file = filepath.Join(ctx.Platform.Path, "env") 181 | if ctx.Platform.Environment, err = internal.NewConfigMapFromPath(file); err != nil { 182 | config.exitHandler.Error(fmt.Errorf("unable to read platform environment %s\n%w", file, err)) 183 | return 184 | } 185 | config.logger.Debugf("Platform Environment: %s", ctx.Platform.Environment) 186 | 187 | if ctx.StackID, ok = os.LookupEnv(EnvStackID); !ok { 188 | config.logger.Debug("CNB_STACK_ID not set") 189 | } else { 190 | config.logger.Debugf("Stack: %s", ctx.StackID) 191 | } 192 | 193 | result, err := detect(ctx) 194 | if err != nil { 195 | config.exitHandler.Error(err) 196 | return 197 | } 198 | config.logger.Debugf("Result: %+v", result) 199 | 200 | if !result.Pass { 201 | config.exitHandler.Fail() 202 | return 203 | } 204 | 205 | if len(result.Plans) > 0 { 206 | var plans BuildPlans 207 | if len(result.Plans) > 0 { 208 | plans.BuildPlan = result.Plans[0] 209 | } 210 | if len(result.Plans) > 1 { 211 | plans.Or = result.Plans[1:] 212 | } 213 | 214 | config.logger.Debugf("Writing build plans: %s <= %+v", buildPlanPath, plans) 215 | if err := config.tomlWriter.Write(buildPlanPath, plans); err != nil { 216 | config.exitHandler.Error(fmt.Errorf("unable to write buildplan %s\n%w", buildPlanPath, err)) 217 | return 218 | } 219 | } 220 | 221 | config.exitHandler.Pass() 222 | } 223 | -------------------------------------------------------------------------------- /detect_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb_test 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | . "github.com/onsi/gomega" 26 | "github.com/sclevine/spec" 27 | "github.com/stretchr/testify/mock" 28 | 29 | "github.com/harshfelony/libcnb/v2" 30 | "github.com/harshfelony/libcnb/v2/log" 31 | "github.com/harshfelony/libcnb/v2/mocks" 32 | ) 33 | 34 | func testDetect(t *testing.T, context spec.G, it spec.S) { 35 | var ( 36 | Expect = NewWithT(t).Expect 37 | 38 | applicationPath string 39 | buildpackPath string 40 | buildPlanPath string 41 | commandPath string 42 | detectFunc libcnb.DetectFunc 43 | exitHandler *mocks.ExitHandler 44 | platformPath string 45 | tomlWriter *mocks.TOMLWriter 46 | 47 | workingDir string 48 | ) 49 | 50 | it.Before(func() { 51 | var err error 52 | 53 | applicationPath, err = os.MkdirTemp("", "detect-application-path") 54 | Expect(err).NotTo(HaveOccurred()) 55 | applicationPath, err = filepath.EvalSymlinks(applicationPath) 56 | Expect(err).NotTo(HaveOccurred()) 57 | 58 | buildpackPath, err = os.MkdirTemp("", "detect-buildpack-path") 59 | Expect(err).NotTo(HaveOccurred()) 60 | Expect(os.Setenv("CNB_BUILDPACK_DIR", buildpackPath)).To(Succeed()) 61 | 62 | Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), 63 | []byte(` 64 | api = "0.8" 65 | 66 | [buildpack] 67 | id = "test-id" 68 | name = "test-name" 69 | version = "1.1.1" 70 | clear-env = true 71 | description = "A test buildpack" 72 | keywords = ["test", "buildpack"] 73 | 74 | [[buildpack.licenses]] 75 | type = "Apache-2.0" 76 | uri = "https://spdx.org/licenses/Apache-2.0.html" 77 | 78 | [[buildpack.licenses]] 79 | type = "Apache-1.1" 80 | uri = "https://spdx.org/licenses/Apache-1.1.html" 81 | 82 | [[stacks]] 83 | id = "test-id" 84 | 85 | [metadata] 86 | test-key = "test-value" 87 | `), 88 | 0600), 89 | ).To(Succeed()) 90 | 91 | f, err := os.CreateTemp("", "detect-buildplan-path") 92 | Expect(err).NotTo(HaveOccurred()) 93 | Expect(f.Close()).NotTo(HaveOccurred()) 94 | buildPlanPath = f.Name() 95 | 96 | commandPath = filepath.Join("bin", "detect") 97 | 98 | detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { 99 | return libcnb.DetectResult{}, nil 100 | } 101 | 102 | exitHandler = &mocks.ExitHandler{} 103 | exitHandler.On("Error", mock.Anything) 104 | exitHandler.On("Fail") 105 | exitHandler.On("Pass") 106 | 107 | platformPath, err = os.MkdirTemp("", "detect-platform-path") 108 | Expect(err).NotTo(HaveOccurred()) 109 | 110 | Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha"), 0755)).To(Succeed()) 111 | Expect(os.WriteFile(filepath.Join(platformPath, "bindings", "alpha", "test-secret-key"), 112 | []byte("test-secret-value"), 0600)).To(Succeed()) 113 | 114 | Expect(os.MkdirAll(filepath.Join(platformPath, "env"), 0755)).To(Succeed()) 115 | Expect(os.WriteFile(filepath.Join(platformPath, "env", "TEST_ENV"), []byte("test-value"), 0600)). 116 | To(Succeed()) 117 | 118 | tomlWriter = &mocks.TOMLWriter{} 119 | tomlWriter.On("Write", mock.Anything, mock.Anything).Return(nil) 120 | 121 | Expect(os.Setenv("CNB_STACK_ID", "test-stack-id")).To(Succeed()) 122 | Expect(os.Setenv("CNB_PLATFORM_DIR", platformPath)).To(Succeed()) 123 | Expect(os.Setenv("CNB_BUILD_PLAN_PATH", buildPlanPath)).To(Succeed()) 124 | 125 | workingDir, err = os.Getwd() 126 | Expect(err).NotTo(HaveOccurred()) 127 | Expect(os.Chdir(applicationPath)).To(Succeed()) 128 | }) 129 | 130 | it.After(func() { 131 | Expect(os.Chdir(workingDir)).To(Succeed()) 132 | Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed()) 133 | Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed()) 134 | Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed()) 135 | Expect(os.Unsetenv("CNB_BUILD_PLAN_PATH")).To(Succeed()) 136 | 137 | Expect(os.RemoveAll(applicationPath)).To(Succeed()) 138 | Expect(os.RemoveAll(buildpackPath)).To(Succeed()) 139 | Expect(os.RemoveAll(buildPlanPath)).To(Succeed()) 140 | Expect(os.RemoveAll(platformPath)).To(Succeed()) 141 | }) 142 | 143 | context("buildpack API is not within the supported range", func() { 144 | it.Before(func() { 145 | Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), 146 | []byte(` 147 | api = "0.7" 148 | 149 | [buildpack] 150 | id = "test-id" 151 | name = "test-name" 152 | version = "1.1.1" 153 | `), 154 | 0600), 155 | ).To(Succeed()) 156 | }) 157 | 158 | it("fails", func() { 159 | libcnb.Detect(detectFunc, 160 | libcnb.NewConfig( 161 | libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}), 162 | libcnb.WithExitHandler(exitHandler), 163 | libcnb.WithLogger(log.NewDiscard())), 164 | ) 165 | 166 | if libcnb.MinSupportedBPVersion == libcnb.MaxSupportedBPVersion { 167 | Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError( 168 | fmt.Sprintf("this version of libcnb is only compatible with buildpack API == %s", libcnb.MinSupportedBPVersion))) 169 | } else { 170 | Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError( 171 | fmt.Sprintf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", libcnb.MinSupportedBPVersion, libcnb.MaxSupportedBPVersion), 172 | )) 173 | } 174 | }) 175 | }) 176 | 177 | context("errors if required env vars are not set", func() { 178 | for _, e := range []string{"CNB_PLATFORM_DIR", "CNB_BUILD_PLAN_PATH"} { 179 | // We need to do this assignment because of the way that spec binds variables 180 | envVar := e 181 | context(fmt.Sprintf("when %s is unset", envVar), func() { 182 | it.Before(func() { 183 | Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), 184 | []byte(` 185 | api = "0.8" 186 | 187 | [buildpack] 188 | id = "test-id" 189 | name = "test-name" 190 | version = "1.1.1" 191 | `), 192 | 0600), 193 | ).To(Succeed()) 194 | os.Unsetenv(envVar) 195 | }) 196 | 197 | it("fails", func() { 198 | libcnb.Detect(detectFunc, 199 | libcnb.NewConfig( 200 | libcnb.WithArguments([]string{commandPath}), 201 | libcnb.WithExitHandler(exitHandler)), 202 | ) 203 | Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError( 204 | fmt.Sprintf("expected %s to be set", envVar), 205 | )) 206 | }) 207 | }) 208 | } 209 | }) 210 | 211 | context("has a detect environment", func() { 212 | var ctx libcnb.DetectContext 213 | 214 | it.Before(func() { 215 | Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), 216 | []byte(` 217 | api = "0.8" 218 | 219 | [buildpack] 220 | id = "test-id" 221 | name = "test-name" 222 | version = "1.1.1" 223 | `), 224 | 0600), 225 | ).To(Succeed()) 226 | 227 | detectFunc = func(context libcnb.DetectContext) (libcnb.DetectResult, error) { 228 | ctx = context 229 | return libcnb.DetectResult{}, nil 230 | } 231 | }) 232 | 233 | it("creates context", func() { 234 | libcnb.Detect(detectFunc, 235 | libcnb.NewConfig( 236 | libcnb.WithArguments([]string{commandPath}), 237 | libcnb.WithExitHandler(exitHandler)), 238 | ) 239 | 240 | Expect(ctx.ApplicationPath).To(Equal(applicationPath)) 241 | Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{ 242 | API: "0.8", 243 | Info: libcnb.BuildpackInfo{ 244 | ID: "test-id", 245 | Name: "test-name", 246 | Version: "1.1.1", 247 | }, 248 | Path: buildpackPath, 249 | })) 250 | Expect(ctx.Platform).To(Equal(libcnb.Platform{ 251 | Bindings: libcnb.Bindings{ 252 | libcnb.Binding{ 253 | Name: "alpha", 254 | Path: filepath.Join(platformPath, "bindings", "alpha"), 255 | Secret: map[string]string{ 256 | "test-secret-key": "test-secret-value", 257 | }, 258 | }, 259 | }, 260 | Environment: map[string]string{"TEST_ENV": "test-value"}, 261 | Path: platformPath, 262 | })) 263 | Expect(ctx.StackID).To(Equal("test-stack-id")) 264 | }) 265 | }) 266 | 267 | it("fails if CNB_BUILDPACK_DIR is not set", func() { 268 | Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed()) 269 | 270 | libcnb.Detect(detectFunc, 271 | libcnb.NewConfig( 272 | libcnb.WithArguments([]string{filepath.Join(buildpackPath, commandPath), platformPath, buildPlanPath}), 273 | libcnb.WithExitHandler(exitHandler), 274 | libcnb.WithLogger(log.NewDiscard())), 275 | ) 276 | 277 | Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to get CNB_BUILDPACK_DIR, not found")) 278 | }) 279 | 280 | it("handles error from DetectFunc", func() { 281 | detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { 282 | return libcnb.DetectResult{}, fmt.Errorf("test-error") 283 | } 284 | 285 | libcnb.Detect(detectFunc, 286 | libcnb.NewConfig( 287 | libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}), 288 | libcnb.WithExitHandler(exitHandler), 289 | libcnb.WithLogger(log.NewDiscard())), 290 | ) 291 | 292 | Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("test-error")) 293 | }) 294 | 295 | it("does not write empty files", func() { 296 | detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { 297 | return libcnb.DetectResult{Pass: true}, nil 298 | } 299 | 300 | libcnb.Detect(detectFunc, 301 | libcnb.NewConfig( 302 | libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}), 303 | libcnb.WithExitHandler(exitHandler), 304 | libcnb.WithTOMLWriter(tomlWriter), 305 | libcnb.WithLogger(log.NewDiscard())), 306 | ) 307 | 308 | Expect(tomlWriter.Calls).To(HaveLen(0)) 309 | }) 310 | 311 | it("writes one build plan", func() { 312 | detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { 313 | return libcnb.DetectResult{ 314 | Pass: true, 315 | Plans: []libcnb.BuildPlan{ 316 | { 317 | Provides: []libcnb.BuildPlanProvide{ 318 | {Name: "test-name"}, 319 | }, 320 | Requires: []libcnb.BuildPlanRequire{ 321 | { 322 | Name: "test-name", 323 | Metadata: map[string]interface{}{"test-key": "test-value"}, 324 | }, 325 | }, 326 | }, 327 | }, 328 | }, nil 329 | } 330 | 331 | libcnb.Detect(detectFunc, 332 | libcnb.NewConfig( 333 | libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}), 334 | libcnb.WithExitHandler(exitHandler), 335 | libcnb.WithTOMLWriter(tomlWriter), 336 | libcnb.WithLogger(log.NewDiscard())), 337 | ) 338 | 339 | Expect(tomlWriter.Calls[0].Arguments.Get(0)).To(Equal(buildPlanPath)) 340 | Expect(tomlWriter.Calls[0].Arguments.Get(1)).To(Equal(libcnb.BuildPlans{ 341 | BuildPlan: libcnb.BuildPlan{ 342 | Provides: []libcnb.BuildPlanProvide{ 343 | {Name: "test-name"}, 344 | }, 345 | Requires: []libcnb.BuildPlanRequire{ 346 | { 347 | Name: "test-name", 348 | Metadata: map[string]interface{}{"test-key": "test-value"}, 349 | }, 350 | }, 351 | }, 352 | })) 353 | }) 354 | 355 | it("writes two build plans", func() { 356 | detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { 357 | return libcnb.DetectResult{ 358 | Pass: true, 359 | Plans: []libcnb.BuildPlan{ 360 | { 361 | Provides: []libcnb.BuildPlanProvide{ 362 | {Name: "test-name-1"}, 363 | }, 364 | Requires: []libcnb.BuildPlanRequire{ 365 | { 366 | Name: "test-name-1", 367 | Metadata: map[string]interface{}{"test-key-1": "test-value-1"}, 368 | }, 369 | }, 370 | }, 371 | { 372 | Provides: []libcnb.BuildPlanProvide{ 373 | {Name: "test-name-2"}, 374 | }, 375 | Requires: []libcnb.BuildPlanRequire{ 376 | { 377 | Name: "test-name-2", 378 | Metadata: map[string]interface{}{"test-key-2": "test-value-2"}, 379 | }, 380 | }, 381 | }, 382 | }, 383 | }, nil 384 | } 385 | 386 | libcnb.Detect(detectFunc, 387 | libcnb.NewConfig( 388 | libcnb.WithArguments([]string{commandPath, platformPath, buildPlanPath}), 389 | libcnb.WithExitHandler(exitHandler), 390 | libcnb.WithTOMLWriter(tomlWriter), 391 | libcnb.WithLogger(log.NewDiscard())), 392 | ) 393 | 394 | Expect(tomlWriter.Calls[0].Arguments.Get(0)).To(Equal(buildPlanPath)) 395 | Expect(tomlWriter.Calls[0].Arguments.Get(1)).To(Equal(libcnb.BuildPlans{ 396 | BuildPlan: libcnb.BuildPlan{ 397 | Provides: []libcnb.BuildPlanProvide{ 398 | {Name: "test-name-1"}, 399 | }, 400 | Requires: []libcnb.BuildPlanRequire{ 401 | { 402 | Name: "test-name-1", 403 | Metadata: map[string]interface{}{"test-key-1": "test-value-1"}, 404 | }, 405 | }, 406 | }, 407 | Or: []libcnb.BuildPlan{ 408 | { 409 | Provides: []libcnb.BuildPlanProvide{ 410 | {Name: "test-name-2"}, 411 | }, 412 | Requires: []libcnb.BuildPlanRequire{ 413 | { 414 | Name: "test-name-2", 415 | Metadata: map[string]interface{}{"test-key-2": "test-value-2"}, 416 | }, 417 | }, 418 | }, 419 | }, 420 | })) 421 | }) 422 | } 423 | -------------------------------------------------------------------------------- /environment.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | import ( 20 | "fmt" 21 | "path/filepath" 22 | ) 23 | 24 | // Environment represents the file-based environment variable specification. 25 | type Environment map[string]string 26 | 27 | // Append formats using the default formats for its operands and appends the value of this environment variable to any 28 | // previous declarations of the value without any delimitation. Spaces are added between operands when neither is a 29 | // string. If delimitation is important during concatenation, callers are required to add it. 30 | func (e Environment) Append(name string, delimiter string, a ...interface{}) { 31 | e.delimiter(name, delimiter) 32 | e[fmt.Sprintf("%s.append", name)] = fmt.Sprint(a...) 33 | } 34 | 35 | // Appendf formats according to a format specifier and appends the value of this environment variable to any previous 36 | // declarations of the value without any delimitation. If delimitation is important during concatenation, callers are 37 | // required to add it. 38 | func (e Environment) Appendf(name string, delimiter string, format string, a ...interface{}) { 39 | e.delimiter(name, delimiter) 40 | e[fmt.Sprintf("%s.append", name)] = fmt.Sprintf(format, a...) 41 | } 42 | 43 | // Default formats using the default formats for its operands and sets a default for an environment variable with this 44 | // value. Spaces are added between operands when neither is a string. 45 | func (e Environment) Default(name string, a ...interface{}) { 46 | e[fmt.Sprintf("%s.default", name)] = fmt.Sprint(a...) 47 | } 48 | 49 | // Defaultf formats according to a format specifier and sets a default for an environment variable with this value. 50 | func (e Environment) Defaultf(name string, format string, a ...interface{}) { 51 | e[fmt.Sprintf("%s.default", name)] = fmt.Sprintf(format, a...) 52 | } 53 | 54 | // Override formats using the default formats for its operands and overrides any existing value for an environment 55 | // variable with this value. Spaces are added between operands when neither is a string. 56 | func (e Environment) Override(name string, a ...interface{}) { 57 | e[fmt.Sprintf("%s.override", name)] = fmt.Sprint(a...) 58 | } 59 | 60 | // Overridef formats according to a format specifier and overrides any existing value for an environment variable with 61 | // this value. 62 | func (e Environment) Overridef(name string, format string, a ...interface{}) { 63 | e[fmt.Sprintf("%s.override", name)] = fmt.Sprintf(format, a...) 64 | } 65 | 66 | // Prepend formats using the default formats for its operands and prepends the value of this environment variable to any 67 | // previous declarations of the value without any delimitation. Spaces are added between operands when neither is a 68 | // string. If delimitation is important during concatenation, callers are required to add it. 69 | func (e Environment) Prepend(name string, delimiter string, a ...interface{}) { 70 | e.delimiter(name, delimiter) 71 | e[fmt.Sprintf("%s.prepend", name)] = fmt.Sprint(a...) 72 | } 73 | 74 | // Prependf formats using the default formats for its operands and prepends the value of this environment variable to 75 | // any previous declarations of the value without any delimitation. If delimitation is important during concatenation, 76 | // callers are required to add it. 77 | func (e Environment) Prependf(name string, delimiter string, format string, a ...interface{}) { 78 | e.delimiter(name, delimiter) 79 | e[fmt.Sprintf("%s.prepend", name)] = fmt.Sprintf(format, a...) 80 | } 81 | 82 | // ProcessAppend formats using the default formats for its operands and appends the value of this environment variable 83 | // to any previous declarations of the value without any delimitation. Spaces are added between operands when neither is 84 | // a string. If delimitation is important during concatenation, callers are required to add it. 85 | func (e Environment) ProcessAppend(processType string, name string, delimiter string, a ...interface{}) { 86 | e.Append(filepath.Join(processType, name), delimiter, a...) 87 | } 88 | 89 | // ProcessAppendf formats according to a format specifier and appends the value of this environment variable to any 90 | // previous declarations of the value without any delimitation. If delimitation is important during concatenation, 91 | // callers are required to add it. 92 | func (e Environment) ProcessAppendf(processType string, name string, delimiter string, format string, a ...interface{}) { 93 | e.Appendf(filepath.Join(processType, name), delimiter, format, a...) 94 | } 95 | 96 | // ProcessDefault formats using the default formats for its operands and sets a default for an environment variable with 97 | // this value. Spaces are added between operands when neither is a string. 98 | func (e Environment) ProcessDefault(processType string, name string, a ...interface{}) { 99 | e.Default(filepath.Join(processType, name), a...) 100 | } 101 | 102 | // ProcessDefaultf formats according to a format specifier and sets a default for an environment variable with this 103 | // value. 104 | func (e Environment) ProcessDefaultf(processType string, name string, format string, a ...interface{}) { 105 | e.Defaultf(filepath.Join(processType, name), format, a...) 106 | } 107 | 108 | // ProcessOverride formats using the default formats for its operands and overrides any existing value for an 109 | // environment variable with this value. Spaces are added between operands when neither is a string. 110 | func (e Environment) ProcessOverride(processType string, name string, a ...interface{}) { 111 | e.Override(filepath.Join(processType, name), a...) 112 | } 113 | 114 | // ProcessOverridef formats according to a format specifier and overrides any existing value for an environment variable 115 | // with this value. 116 | func (e Environment) ProcessOverridef(processType string, name string, format string, a ...interface{}) { 117 | e.Overridef(filepath.Join(processType, name), format, a...) 118 | } 119 | 120 | // ProcessPrepend formats using the default formats for its operands and prepends the value of this environment variable 121 | // to any previous declarations of the value without any delimitation. Spaces are added between operands when neither 122 | // is a string. If delimitation is important during concatenation, callers are required to add it. 123 | func (e Environment) ProcessPrepend(processType string, name string, delimiter string, a ...interface{}) { 124 | e.Prepend(filepath.Join(processType, name), delimiter, a...) 125 | } 126 | 127 | // ProcessPrependf formats using the default formats for its operands and prepends the value of this environment 128 | // variable to any previous declarations of the value without any delimitation. If delimitation is important during 129 | // concatenation, callers are required to add it. 130 | func (e Environment) ProcessPrependf(processType string, name string, delimiter string, format string, a ...interface{}) { 131 | e.Prependf(filepath.Join(processType, name), delimiter, format, a...) 132 | } 133 | 134 | func (e Environment) delimiter(name string, delimiter string) { 135 | e[fmt.Sprintf("%s.delim", name)] = delimiter 136 | } 137 | -------------------------------------------------------------------------------- /environment_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb_test 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | . "github.com/onsi/gomega" 24 | "github.com/sclevine/spec" 25 | 26 | "github.com/harshfelony/libcnb/v2" 27 | ) 28 | 29 | func testEnvironment(t *testing.T, _ spec.G, it spec.S) { 30 | var ( 31 | Expect = NewWithT(t).Expect 32 | 33 | environment libcnb.Environment 34 | ) 35 | 36 | it.Before(func() { 37 | environment = libcnb.Environment{} 38 | }) 39 | 40 | it("adds append value", func() { 41 | environment.Append("TEST_NAME", "test-delimiter", "test-value") 42 | Expect(environment).To(Equal(libcnb.Environment{ 43 | "TEST_NAME.delim": "test-delimiter", 44 | "TEST_NAME.append": "test-value", 45 | })) 46 | }) 47 | 48 | it("adds append formatted value", func() { 49 | environment.Appendf("TEST_NAME", "test-delimiter", "test-%s", "value") 50 | Expect(environment).To(Equal(libcnb.Environment{ 51 | "TEST_NAME.delim": "test-delimiter", 52 | "TEST_NAME.append": "test-value", 53 | })) 54 | }) 55 | 56 | it("adds default value", func() { 57 | environment.Default("TEST_NAME", "test-value") 58 | Expect(environment).To(Equal(libcnb.Environment{"TEST_NAME.default": "test-value"})) 59 | }) 60 | 61 | it("adds default formatted value", func() { 62 | environment.Defaultf("TEST_NAME", "test-%s", "value") 63 | Expect(environment).To(Equal(libcnb.Environment{"TEST_NAME.default": "test-value"})) 64 | }) 65 | 66 | it("adds override value", func() { 67 | environment.Override("TEST_NAME", "test-value") 68 | Expect(environment).To(Equal(libcnb.Environment{"TEST_NAME.override": "test-value"})) 69 | }) 70 | 71 | it("adds override formatted value", func() { 72 | environment.Overridef("TEST_NAME", "test-%s", "value") 73 | Expect(environment).To(Equal(libcnb.Environment{"TEST_NAME.override": "test-value"})) 74 | }) 75 | 76 | it("adds prepend value", func() { 77 | environment.Prepend("TEST_NAME", "test-delimiter", "test-value") 78 | Expect(environment).To(Equal(libcnb.Environment{ 79 | "TEST_NAME.delim": "test-delimiter", 80 | "TEST_NAME.prepend": "test-value", 81 | })) 82 | }) 83 | 84 | it("adds prepend formatted value", func() { 85 | environment.Prependf("TEST_NAME", "test-delimiter", "test-%s", "value") 86 | Expect(environment).To(Equal(libcnb.Environment{ 87 | "TEST_NAME.delim": "test-delimiter", 88 | "TEST_NAME.prepend": "test-value", 89 | })) 90 | }) 91 | 92 | it("adds process-specific append value", func() { 93 | environment.ProcessAppend("test-process", "TEST_NAME", "test-delimiter", "test-value") 94 | Expect(environment).To(Equal(libcnb.Environment{ 95 | filepath.Join("test-process", "TEST_NAME.delim"): "test-delimiter", 96 | filepath.Join("test-process", "TEST_NAME.append"): "test-value", 97 | })) 98 | }) 99 | 100 | it("adds process-specific append formatted value", func() { 101 | environment.ProcessAppendf("test-process", "TEST_NAME", "test-delimiter", "test-%s", "value") 102 | Expect(environment).To(Equal(libcnb.Environment{ 103 | filepath.Join("test-process", "TEST_NAME.delim"): "test-delimiter", 104 | filepath.Join("test-process", "TEST_NAME.append"): "test-value", 105 | })) 106 | }) 107 | 108 | it("adds process-specific default value", func() { 109 | environment.ProcessDefault("test-process", "TEST_NAME", "test-value") 110 | Expect(environment).To(Equal(libcnb.Environment{filepath.Join("test-process", "TEST_NAME.default"): "test-value"})) 111 | }) 112 | 113 | it("adds process-specific default formatted value", func() { 114 | environment.ProcessDefaultf("test-process", "TEST_NAME", "test-%s", "value") 115 | Expect(environment).To(Equal(libcnb.Environment{filepath.Join("test-process", "TEST_NAME.default"): "test-value"})) 116 | }) 117 | 118 | it("adds process-specific override value", func() { 119 | environment.ProcessOverride("test-process", "TEST_NAME", "test-value") 120 | Expect(environment).To(Equal(libcnb.Environment{filepath.Join("test-process", "TEST_NAME.override"): "test-value"})) 121 | }) 122 | 123 | it("adds process-specific override formatted value", func() { 124 | environment.ProcessOverridef("test-process", "TEST_NAME", "test-%s", "value") 125 | Expect(environment).To(Equal(libcnb.Environment{filepath.Join("test-process", "TEST_NAME.override"): "test-value"})) 126 | }) 127 | 128 | it("adds process-specific prepend value", func() { 129 | environment.ProcessPrepend("test-process", "TEST_NAME", "test-delimiter", "test-value") 130 | Expect(environment).To(Equal(libcnb.Environment{ 131 | filepath.Join("test-process", "TEST_NAME.delim"): "test-delimiter", 132 | filepath.Join("test-process", "TEST_NAME.prepend"): "test-value", 133 | })) 134 | }) 135 | 136 | it("adds process-specific prepend formatted value", func() { 137 | environment.ProcessPrependf("test-process", "TEST_NAME", "test-delimiter", "test-%s", "value") 138 | Expect(environment).To(Equal(libcnb.Environment{ 139 | filepath.Join("test-process", "TEST_NAME.delim"): "test-delimiter", 140 | filepath.Join("test-process", "TEST_NAME.prepend"): "test-value", 141 | })) 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /examples/build_test.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | cdx "github.com/CycloneDX/cyclonedx-go" 9 | 10 | "github.com/harshfelony/libcnb/v2" 11 | "github.com/harshfelony/libcnb/v2/log" 12 | ) 13 | 14 | const ( 15 | DefaultVersion = "0.1" 16 | ) 17 | 18 | type Builder struct { 19 | Logger log.Logger 20 | } 21 | 22 | // BuildpackPlan may contain multiple entries for a single buildpack, resolve 23 | // into a single entry. 24 | func resolve(plan libcnb.BuildpackPlan, name string) libcnb.BuildpackPlanEntry { 25 | entry := libcnb.BuildpackPlanEntry{ 26 | Name: name, 27 | Metadata: map[string]interface{}{}, 28 | } 29 | for _, e := range plan.Entries { 30 | for k, v := range e.Metadata { 31 | entry.Metadata[k] = v 32 | } 33 | } 34 | return entry 35 | } 36 | 37 | func populateLayer(layer libcnb.Layer, version string) (libcnb.Layer, error) { 38 | exampleFile := filepath.Join(layer.Path, "example.txt") 39 | if err := os.WriteFile(exampleFile, []byte(version), 0600); err != nil { 40 | return libcnb.Layer{}, fmt.Errorf("unable to write example file: %w", err) 41 | } 42 | 43 | layer.SharedEnvironment.Default("EXAMPLE_FILE", exampleFile) 44 | 45 | // Provide an SBOM 46 | bom := cdx.NewBOM() 47 | bom.Metadata = &cdx.Metadata{ 48 | Component: &cdx.Component{ 49 | Type: cdx.ComponentTypeFile, 50 | Name: "example", 51 | Version: version, 52 | }, 53 | } 54 | sbomPath := layer.SBOMPath(libcnb.CycloneDXJSON) 55 | sbomFile, err := os.OpenFile(sbomPath, os.O_CREATE|os.O_WRONLY, 0600) 56 | if err != nil { 57 | return layer, err 58 | } 59 | defer sbomFile.Close() 60 | encoder := cdx.NewBOMEncoder(sbomFile, cdx.BOMFileFormatJSON) 61 | if err := encoder.Encode(bom); err != nil { 62 | return layer, err 63 | } 64 | return layer, nil 65 | } 66 | 67 | func (b Builder) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { 68 | // Reduce possible multiple buildpack plan entries to a single entry 69 | entry := resolve(context.Plan, Provides) 70 | result := libcnb.NewBuildResult() 71 | 72 | // Read metadata from the buildpack plan, often contributed by libcnb.Requires 73 | // of the Detect phase 74 | version := DefaultVersion 75 | if v, ok := entry.Metadata["version"].(string); ok { 76 | version = v 77 | } 78 | 79 | // Create a layer 80 | layer, err := context.Layers.Layer("example") 81 | if err != nil { 82 | return result, err 83 | } 84 | layer.LayerTypes = libcnb.LayerTypes{ 85 | Launch: true, 86 | Build: true, 87 | Cache: true, 88 | } 89 | 90 | layer, err = populateLayer(layer, version) 91 | if err != nil { 92 | return result, nil 93 | } 94 | 95 | result.Layers = append(result.Layers, layer) 96 | return result, nil 97 | } 98 | 99 | func ExampleBuild() { 100 | detector := Detector{log.New(os.Stdout)} 101 | builder := Builder{log.New(os.Stdout)} 102 | libcnb.BuildpackMain(detector.Detect, builder.Build) 103 | } 104 | -------------------------------------------------------------------------------- /examples/detect_test.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/harshfelony/libcnb/v2" 9 | "github.com/harshfelony/libcnb/v2/log" 10 | ) 11 | 12 | const ( 13 | Provides = "example" 14 | BpExampleVersion = "BP_EXAMPLE_VERSION" 15 | ) 16 | 17 | type Detector struct { 18 | Logger log.Logger 19 | } 20 | 21 | func (Detector) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) { 22 | version := "1.0" 23 | // Scan the application source folder to see if the example buildpack is 24 | // required. If `version.toml` does not exist we return a failed DetectResult 25 | // but no runtime error has occurred, so we return an empty error. 26 | versionPath := filepath.Join(context.ApplicationPath, "version.toml") 27 | if _, err := os.Open(versionPath); errors.Is(err, os.ErrNotExist) { 28 | return libcnb.DetectResult{}, nil 29 | } 30 | // Read the version number from the buildpack definition 31 | if exampleVersion, exists := context.Buildpack.Metadata["version"]; exists { 32 | version = exampleVersion.(string) 33 | } 34 | // Accept version number from the environment if the user provides it 35 | if exampleVersion, exists := context.Platform.Environment[BpExampleVersion]; exists { 36 | version = exampleVersion 37 | } 38 | metadata := map[string]interface{}{ 39 | "version": version, 40 | } 41 | return libcnb.DetectResult{ 42 | Pass: true, 43 | Plans: []libcnb.BuildPlan{ 44 | { 45 | // Let the system know that if other buildpacks Require "example" 46 | // then this buildpack Provides the implementation logic. 47 | Provides: []libcnb.BuildPlanProvide{ 48 | {Name: Provides}, 49 | }, 50 | // It is common for a buildpack to Require itself if the build phase 51 | // needs information from the detect phase. Here we pass the version number 52 | // as metadata to the build phase. 53 | Requires: []libcnb.BuildPlanRequire{ 54 | { 55 | Name: Provides, 56 | Metadata: metadata, 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, nil 62 | } 63 | 64 | func ExampleDetect() { 65 | detector := Detector{log.New(os.Stdout)} 66 | libcnb.BuildpackMain(detector.Detect, nil) 67 | } 68 | -------------------------------------------------------------------------------- /examples/generate_test.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/harshfelony/libcnb/v2" 8 | "github.com/harshfelony/libcnb/v2/log" 9 | ) 10 | 11 | type Generator struct { 12 | Logger log.Logger 13 | } 14 | 15 | func (Generator) Generate(context libcnb.GenerateContext) (libcnb.GenerateResult, error) { 16 | // here you can read the context.ApplicationPath folder 17 | // and create run.Dockerfile and build.Dockerfile in the context.OutputPath folder 18 | // and read metadata from the context.Extension struct 19 | 20 | // Just to use context to keep compiler happy =) 21 | fmt.Println(context.Extension.Info.ID) 22 | 23 | result := libcnb.NewGenerateResult() 24 | return result, nil 25 | } 26 | 27 | func ExampleGenerate() { 28 | generator := Generator{log.New(os.Stdout)} 29 | libcnb.ExtensionMain(nil, generator.Generate) 30 | } 31 | -------------------------------------------------------------------------------- /exec_d.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | 24 | "github.com/harshfelony/libcnb/v2/internal" 25 | ) 26 | 27 | //go:generate mockery --name ExecD --case=underscore 28 | 29 | // ExecD describes an interface for types that follow the Exec.d specification. 30 | // It should return a map of environment variables and their values as output. 31 | type ExecD interface { 32 | Execute() (map[string]string, error) 33 | } 34 | 35 | // RunExecD is called by the main function of a buildpack's execd binary, encompassing multiple execd 36 | // executors in one binary. 37 | func RunExecD(execDMap map[string]ExecD, options ...Option) { 38 | config := Config{ 39 | arguments: os.Args, 40 | execdWriter: internal.NewExecDWriter(), 41 | exitHandler: internal.NewExitHandler(), 42 | } 43 | 44 | for _, option := range options { 45 | config = option(config) 46 | } 47 | 48 | if len(config.arguments) == 0 { 49 | config.exitHandler.Error(fmt.Errorf("expected command name")) 50 | 51 | return 52 | } 53 | 54 | c := filepath.Base(config.arguments[0]) 55 | e, ok := execDMap[c] 56 | if !ok { 57 | config.exitHandler.Error(fmt.Errorf("unsupported command %s", c)) 58 | return 59 | } 60 | 61 | r, err := e.Execute() 62 | if err != nil { 63 | config.exitHandler.Error(err) 64 | return 65 | } 66 | 67 | if err := config.execdWriter.Write(r); err != nil { 68 | config.exitHandler.Error(err) 69 | return 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /exec_d_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb_test 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | . "github.com/onsi/gomega" 24 | "github.com/sclevine/spec" 25 | "github.com/stretchr/testify/mock" 26 | 27 | "github.com/harshfelony/libcnb/v2" 28 | "github.com/harshfelony/libcnb/v2/mocks" 29 | ) 30 | 31 | func testExecD(t *testing.T, _ spec.G, it spec.S) { 32 | var ( 33 | Expect = NewWithT(t).Expect 34 | 35 | exitHandler *mocks.ExitHandler 36 | execdWriter *mocks.ExecDWriter 37 | ) 38 | 39 | it.Before(func() { 40 | execdWriter = &mocks.ExecDWriter{} 41 | execdWriter.On("Write", mock.Anything).Return(nil) 42 | exitHandler = &mocks.ExitHandler{} 43 | exitHandler.On("Error", mock.Anything) 44 | exitHandler.On("Pass", mock.Anything) 45 | exitHandler.On("Fail", mock.Anything) 46 | }) 47 | 48 | it("encounters the wrong number of arguments", func() { 49 | libcnb.RunExecD(map[string]libcnb.ExecD{}, 50 | libcnb.WithArguments([]string{}), 51 | libcnb.WithExitHandler(exitHandler), 52 | ) 53 | 54 | Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("expected command name")) 55 | }) 56 | 57 | it("encounters an unsupported execd binary name", func() { 58 | libcnb.RunExecD(map[string]libcnb.ExecD{}, 59 | libcnb.WithArguments([]string{"/dne"}), 60 | libcnb.WithExitHandler(exitHandler), 61 | ) 62 | 63 | Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unsupported command dne")) 64 | }) 65 | 66 | it("calls the appropriate execd for a given execd invoker binary", func() { 67 | execd1 := &mocks.ExecD{} 68 | execd2 := &mocks.ExecD{} 69 | execd1.On("Execute", mock.Anything).Return(map[string]string{}, nil) 70 | 71 | libcnb.RunExecD(map[string]libcnb.ExecD{"execd1": execd1, "execd2": execd2}, 72 | libcnb.WithArguments([]string{"execd1"}), 73 | libcnb.WithExitHandler(exitHandler), 74 | libcnb.WithExecDWriter(execdWriter), 75 | ) 76 | 77 | Expect(execd1.Calls).To(HaveLen(1)) 78 | Expect(execd2.Calls).To(BeEmpty()) 79 | }) 80 | 81 | it("calls exitHandler with the error from the execd", func() { 82 | e := &mocks.ExecD{} 83 | err := fmt.Errorf("example error") 84 | e.On("Execute", mock.Anything).Return(nil, err) 85 | 86 | libcnb.RunExecD(map[string]libcnb.ExecD{"e": e}, 87 | libcnb.WithArguments([]string{"/bin/e"}), 88 | libcnb.WithExitHandler(exitHandler), 89 | libcnb.WithExecDWriter(execdWriter), 90 | ) 91 | 92 | Expect(e.Calls).To(HaveLen(1)) 93 | Expect(execdWriter.Calls).To(HaveLen(0)) 94 | Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(err)) 95 | }) 96 | 97 | it("calls execdWriter.write with the appropriate input", func() { 98 | e := &mocks.ExecD{} 99 | o := map[string]string{"test": "test"} 100 | e.On("Execute", mock.Anything).Return(o, nil) 101 | 102 | libcnb.RunExecD(map[string]libcnb.ExecD{"e": e}, 103 | libcnb.WithArguments([]string{"/bin/e"}), 104 | libcnb.WithExitHandler(exitHandler), 105 | libcnb.WithExecDWriter(execdWriter), 106 | ) 107 | 108 | Expect(e.Calls).To(HaveLen(1)) 109 | Expect(execdWriter.Calls).To(HaveLen(1)) 110 | Expect(execdWriter.Calls[0].Method).To(BeIdenticalTo("Write")) 111 | Expect(execdWriter.Calls[0].Arguments).To(HaveLen(1)) 112 | Expect(execdWriter.Calls[0].Arguments[0]).To(Equal(o)) 113 | }) 114 | 115 | it("calls exitHandler with the error from the execd", func() { 116 | e := &mocks.ExecD{} 117 | err := fmt.Errorf("example error") 118 | e.On("Execute", mock.Anything).Return(nil, err) 119 | 120 | libcnb.RunExecD(map[string]libcnb.ExecD{"e": e}, 121 | libcnb.WithArguments([]string{"/bin/e"}), 122 | libcnb.WithExitHandler(exitHandler), 123 | libcnb.WithExecDWriter(execdWriter), 124 | ) 125 | 126 | Expect(e.Calls).To(HaveLen(1)) 127 | Expect(execdWriter.Calls).To(HaveLen(0)) 128 | Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(err)) 129 | }) 130 | } 131 | -------------------------------------------------------------------------------- /extension.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | // ExtensionInfo is information about the extension. 20 | type ExtensionInfo struct { 21 | // ID is the ID of the extension. 22 | ID string `toml:"id"` 23 | 24 | // Name is the name of the extension. 25 | Name string `toml:"name"` 26 | 27 | // Version is the version of the extension. 28 | Version string `toml:"version"` 29 | 30 | // Homepage is the homepage of the extension. 31 | Homepage string `toml:"homepage"` 32 | 33 | // Description is a string describing the extension. 34 | Description string `toml:"description"` 35 | 36 | // Keywords is a list of words that are associated with the extension. 37 | Keywords []string `toml:"keywords"` 38 | 39 | // Licenses a list of extension licenses. 40 | Licenses []License `toml:"licenses"` 41 | } 42 | 43 | // Extension is the contents of the extension.toml file. 44 | type Extension struct { 45 | // API is the api version expected by the extension. 46 | API string `toml:"api"` 47 | 48 | // Info is information about the extension. 49 | Info ExtensionInfo `toml:"extension"` 50 | 51 | // Path is the path to the extension. 52 | Path string `toml:"-"` 53 | 54 | // Targets is the collection of targets supported by the buildpack. 55 | Targets []Target `toml:"targets"` 56 | 57 | // Metadata is arbitrary metadata attached to the extension. 58 | Metadata map[string]interface{} `toml:"metadata"` 59 | } 60 | -------------------------------------------------------------------------------- /extension_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb_test 18 | 19 | import ( 20 | "bytes" 21 | "testing" 22 | 23 | "github.com/BurntSushi/toml" 24 | "github.com/sclevine/spec" 25 | 26 | "github.com/harshfelony/libcnb/v2" 27 | 28 | . "github.com/onsi/gomega" 29 | ) 30 | 31 | func testExtensionTOML(t *testing.T, _ spec.G, it spec.S) { 32 | var ( 33 | Expect = NewWithT(t).Expect 34 | ) 35 | 36 | it("does not serialize the Path field", func() { 37 | extn := libcnb.Extension{ 38 | API: "0.8", 39 | Info: libcnb.ExtensionInfo{ 40 | ID: "test-buildpack/sample", 41 | Name: "sample", 42 | }, 43 | Path: "../buildpack", 44 | } 45 | 46 | output := &bytes.Buffer{} 47 | 48 | Expect(toml.NewEncoder(output).Encode(extn)).To(Succeed()) 49 | Expect(output.String()).NotTo(Or(ContainSubstring("Path = "), ContainSubstring("path = "))) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | 25 | "github.com/BurntSushi/toml" 26 | "github.com/Masterminds/semver" 27 | 28 | "github.com/harshfelony/libcnb/v2/internal" 29 | "github.com/harshfelony/libcnb/v2/log" 30 | ) 31 | 32 | // GenerateContext contains the inputs to generate. 33 | type GenerateContext struct { 34 | // ApplicationPath is the location of the application source code as provided by 35 | // the lifecycle. 36 | ApplicationPath string 37 | 38 | // Extension is metadata about the extension, from extension.toml. 39 | Extension Extension 40 | 41 | // OutputDirectory is the location Dockerfiles should be written to. 42 | OutputDirectory string 43 | 44 | // Logger is the way to write messages to the end user 45 | Logger log.Logger 46 | 47 | // Plan is the buildpack plan provided to the buildpack. 48 | Plan BuildpackPlan 49 | 50 | // Platform is the contents of the platform. 51 | Platform Platform 52 | 53 | // TargetInfo contains info of the target (os, arch, ...). 54 | TargetInfo TargetInfo 55 | 56 | // TargetDistro is the target distribution (name, version). 57 | TargetDistro TargetDistro 58 | 59 | // Deprecated: StackID is the ID of the stack. 60 | StackID string 61 | } 62 | 63 | // GenerateResult contains the results of detection. 64 | type GenerateResult struct { 65 | // Unmet contains buildpack plan entries that were not satisfied by the buildpack and therefore should be 66 | // passed to subsequent providers. 67 | Unmet []UnmetPlanEntry 68 | RunDockerfile []byte 69 | BuildDockerfile []byte 70 | Config *ExtendConfig 71 | } 72 | 73 | // DockerfileArg is a Dockerfile argument 74 | type DockerfileArg struct { 75 | Name string `toml:"name"` 76 | Value string `toml:"value"` 77 | } 78 | 79 | // BuildConfig contains additional arguments passed to the generated Dockerfiles 80 | type BuildConfig struct { 81 | Args []DockerfileArg `toml:"args"` 82 | } 83 | 84 | // ExtendConfig contains additional configuration for the Dockerfiles 85 | type ExtendConfig struct { 86 | Build BuildConfig `toml:"build"` 87 | Run BuildConfig `toml:"run"` 88 | } 89 | 90 | // NewGenerateResult creates a new BuildResult instance, initializing empty fields. 91 | func NewGenerateResult() GenerateResult { 92 | return GenerateResult{} 93 | } 94 | 95 | func (b GenerateResult) String() string { 96 | return fmt.Sprintf( 97 | "{Unmet:%+v}", 98 | b.Unmet, 99 | ) 100 | } 101 | 102 | // GenerateFunc takes a context and returns a result, performing extension generate behaviors. 103 | type GenerateFunc func(context GenerateContext) (GenerateResult, error) 104 | 105 | // Generate is called by the main function of a extension, for generate phase 106 | func Generate(generate GenerateFunc, config Config) { 107 | var ( 108 | err error 109 | file string 110 | ok bool 111 | ) 112 | ctx := GenerateContext{Logger: config.logger} 113 | 114 | ctx.ApplicationPath, err = os.Getwd() 115 | if err != nil { 116 | config.exitHandler.Error(fmt.Errorf("unable to get working directory\n%w", err)) 117 | return 118 | } 119 | 120 | if config.logger.IsDebugEnabled() { 121 | if err := config.contentWriter.Write("Application contents", ctx.ApplicationPath); err != nil { 122 | config.logger.Debugf("unable to write application contents\n%w", err) 123 | } 124 | } 125 | 126 | if s, ok := os.LookupEnv(EnvExtensionDirectory); ok { 127 | ctx.Extension.Path = filepath.Clean(s) 128 | } else { 129 | config.exitHandler.Error(fmt.Errorf("unable to get CNB_EXTENSION_DIR, not found")) 130 | return 131 | } 132 | 133 | if config.logger.IsDebugEnabled() { 134 | if err := config.contentWriter.Write("Extension contents", ctx.Extension.Path); err != nil { 135 | config.logger.Debugf("unable to write extension contents\n%w", err) 136 | } 137 | } 138 | 139 | file = filepath.Join(ctx.Extension.Path, "extension.toml") 140 | if _, err = toml.DecodeFile(file, &ctx.Extension); err != nil && !os.IsNotExist(err) { 141 | config.exitHandler.Error(fmt.Errorf("unable to decode extension %s\n%w", file, err)) 142 | return 143 | } 144 | config.logger.Debugf("Extension: %+v", ctx.Extension) 145 | 146 | API, err := semver.NewVersion(ctx.Extension.API) 147 | if err != nil { 148 | config.exitHandler.Error(errors.New("version cannot be parsed")) 149 | return 150 | } 151 | 152 | compatVersionCheck, _ := semver.NewConstraint(fmt.Sprintf(">= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion)) 153 | if !compatVersionCheck.Check(API) { 154 | if MinSupportedBPVersion == MaxSupportedBPVersion { 155 | config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack API == %s", MinSupportedBPVersion)) 156 | return 157 | } 158 | 159 | config.exitHandler.Error(fmt.Errorf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", MinSupportedBPVersion, MaxSupportedBPVersion)) 160 | return 161 | } 162 | 163 | outputDir, ok := os.LookupEnv(EnvOutputDirectory) 164 | if !ok { 165 | config.exitHandler.Error(fmt.Errorf("expected CNB_OUTPUT_DIR to be set")) 166 | return 167 | } 168 | ctx.OutputDirectory = outputDir 169 | 170 | ctx.Platform.Path, ok = os.LookupEnv(EnvPlatformDirectory) 171 | if !ok { 172 | config.exitHandler.Error(fmt.Errorf("expected CNB_PLATFORM_DIR to be set")) 173 | return 174 | } 175 | 176 | buildpackPlanPath, ok := os.LookupEnv(EnvBuildPlanPath) 177 | if !ok { 178 | config.exitHandler.Error(fmt.Errorf("expected CNB_BP_PLAN_PATH to be set")) 179 | return 180 | } 181 | 182 | if config.logger.IsDebugEnabled() { 183 | if err := config.contentWriter.Write("Platform contents", ctx.Platform.Path); err != nil { 184 | config.logger.Debugf("unable to write platform contents\n%w", err) 185 | } 186 | } 187 | 188 | if ctx.Platform.Bindings, err = NewBindings(ctx.Platform.Path); err != nil { 189 | config.exitHandler.Error(fmt.Errorf("unable to read platform bindings %s\n%w", ctx.Platform.Path, err)) 190 | return 191 | } 192 | config.logger.Debugf("Platform Bindings: %+v", ctx.Platform.Bindings) 193 | 194 | file = filepath.Join(ctx.Platform.Path, "env") 195 | if ctx.Platform.Environment, err = internal.NewConfigMapFromPath(file); err != nil { 196 | config.exitHandler.Error(fmt.Errorf("unable to read platform environment %s\n%w", file, err)) 197 | return 198 | } 199 | config.logger.Debugf("Platform Environment: %s", ctx.Platform.Environment) 200 | 201 | if _, err = toml.DecodeFile(buildpackPlanPath, &ctx.Plan); err != nil && !os.IsNotExist(err) { 202 | config.exitHandler.Error(fmt.Errorf("unable to decode buildpack plan %s\n%w", buildpackPlanPath, err)) 203 | return 204 | } 205 | config.logger.Debugf("Buildpack Plan: %+v", ctx.Plan) 206 | 207 | if ctx.StackID, ok = os.LookupEnv(EnvStackID); !ok { 208 | config.logger.Debug("CNB_STACK_ID not set") 209 | } else { 210 | config.logger.Debugf("Stack: %s", ctx.StackID) 211 | } 212 | 213 | if API.GreaterThan(semver.MustParse("0.9")) { 214 | ctx.TargetInfo = TargetInfo{} 215 | ctx.TargetInfo.OS, _ = os.LookupEnv(EnvTargetOS) 216 | ctx.TargetInfo.Arch, _ = os.LookupEnv(EnvTargetArch) 217 | ctx.TargetInfo.Variant, _ = os.LookupEnv(EnvTargetArchVariant) 218 | config.logger.Debugf("System: %+v", ctx.TargetInfo) 219 | 220 | ctx.TargetDistro = TargetDistro{} 221 | ctx.TargetDistro.Name, _ = os.LookupEnv(EnvTargetDistroName) 222 | ctx.TargetDistro.Version, _ = os.LookupEnv(EnvTargetDistroVersion) 223 | config.logger.Debugf("Distro: %+v", ctx.TargetDistro) 224 | } 225 | 226 | result, err := generate(ctx) 227 | if err != nil { 228 | config.exitHandler.Error(err) 229 | return 230 | } 231 | config.logger.Debugf("Result: %+v", result) 232 | 233 | if len(result.RunDockerfile) > 0 { 234 | //nolint:gosec 235 | if err := os.WriteFile(filepath.Join(ctx.OutputDirectory, "run.Dockerfile"), result.RunDockerfile, 0644); err != nil { 236 | config.exitHandler.Error(err) 237 | return 238 | } 239 | } 240 | 241 | if len(result.BuildDockerfile) > 0 { 242 | //nolint:gosec 243 | if err := os.WriteFile(filepath.Join(ctx.OutputDirectory, "build.Dockerfile"), result.BuildDockerfile, 0644); err != nil { 244 | config.exitHandler.Error(err) 245 | return 246 | } 247 | } 248 | 249 | if result.Config != nil { 250 | configFile, err := os.Create(filepath.Join(ctx.OutputDirectory, "extend-config.toml")) 251 | if err != nil { 252 | config.exitHandler.Error(err) 253 | return 254 | } 255 | 256 | if err := toml.NewEncoder(configFile).Encode(result.Config); err != nil { 257 | config.exitHandler.Error(err) 258 | return 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/harshfelony/libcnb/v2 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.5.0 7 | github.com/CycloneDX/cyclonedx-go v0.9.2 8 | github.com/Masterminds/semver v1.5.0 9 | github.com/onsi/gomega v1.36.3 10 | github.com/sclevine/spec v1.4.0 11 | github.com/stretchr/testify v1.10.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/google/go-cmp v0.7.0 // indirect 17 | github.com/kr/text v0.2.0 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/stretchr/objx v0.5.2 // indirect 20 | golang.org/x/net v0.38.0 // indirect 21 | golang.org/x/text v0.23.0 // indirect 22 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 2 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo= 4 | github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= 5 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 6 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 7 | github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= 8 | github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= 9 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 13 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 14 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 15 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 16 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 17 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 18 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= 19 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 20 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 21 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 22 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 23 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 24 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 25 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 26 | github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= 27 | github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= 28 | github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= 29 | github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= 33 | github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= 34 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 35 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 36 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 37 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 38 | github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= 39 | github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= 40 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 41 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 42 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 43 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 44 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 45 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 46 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 47 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 48 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 49 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 50 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 51 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 52 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 53 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 55 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 56 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 57 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 58 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 59 | -------------------------------------------------------------------------------- /golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 6m 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - bodyclose 8 | - dogsled 9 | - errcheck 10 | - copyloopvar 11 | - gocritic 12 | - goimports 13 | - gosec 14 | - gosimple 15 | - govet 16 | - ineffassign 17 | - misspell 18 | - nakedret 19 | - revive 20 | - staticcheck 21 | - stylecheck 22 | - typecheck 23 | - unconvert 24 | - unused 25 | - whitespace 26 | 27 | linters-settings: 28 | revive: 29 | rules: 30 | - name: dot-imports 31 | disabled: true 32 | goimports: 33 | local-prefixes: github.com/harshfelony/libcnb/v2 34 | -------------------------------------------------------------------------------- /init_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/sclevine/spec" 23 | "github.com/sclevine/spec/report" 24 | ) 25 | 26 | func TestUnit(t *testing.T) { 27 | suite := spec.New("libcnb", spec.Report(report.Terminal{})) 28 | suite("Build", testBuild) 29 | suite("Detect", testDetect) 30 | suite("Generate", testGenerate) 31 | suite("Environment", testEnvironment) 32 | suite("Layer", testLayer) 33 | suite("Main", testMain) 34 | suite("Platform", testPlatform) 35 | suite("ExecD", testExecD) 36 | suite("BuildpackTOML", testBuildpackTOML) 37 | suite("ExtensionTOML", testExtensionTOML) 38 | suite.Run(t) 39 | } 40 | -------------------------------------------------------------------------------- /internal/config_map.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | ) 25 | 26 | // ConfigMap represents a file-based projection of a collection of key-value pairs. 27 | type ConfigMap map[string]string 28 | 29 | // NewConfigMapFromPath creates a new ConfigMap from the files located within a given path. 30 | func NewConfigMapFromPath(path string) (ConfigMap, error) { 31 | files, err := filepath.Glob(filepath.Join(path, "*")) 32 | if err != nil { 33 | return nil, fmt.Errorf("unable to glob %s\n%w", path, err) 34 | } 35 | 36 | configMap := ConfigMap{} 37 | for _, file := range files { 38 | if strings.HasPrefix(filepath.Base(file), ".") { 39 | // ignore hidden files 40 | continue 41 | } 42 | if stat, err := os.Stat(file); err != nil { 43 | return nil, fmt.Errorf("failed to stat file %s\n%w", file, err) 44 | } else if stat.IsDir() { 45 | continue 46 | } 47 | contents, err := os.ReadFile(file) 48 | if err != nil { 49 | return nil, fmt.Errorf("unable to read file %s\n%w", file, err) 50 | } 51 | 52 | configMap[filepath.Base(file)] = string(contents) 53 | } 54 | 55 | return configMap, nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/config_map_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal_test 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | 24 | . "github.com/onsi/gomega" 25 | "github.com/sclevine/spec" 26 | 27 | "github.com/harshfelony/libcnb/v2/internal" 28 | ) 29 | 30 | func testConfigMap(t *testing.T, _ spec.G, it spec.S) { 31 | var ( 32 | Expect = NewWithT(t).Expect 33 | 34 | path string 35 | ) 36 | 37 | it.Before(func() { 38 | var err error 39 | path, err = os.MkdirTemp("", "config-map") 40 | Expect(err).NotTo(HaveOccurred()) 41 | }) 42 | 43 | it.After(func() { 44 | Expect(os.RemoveAll(path)).To(Succeed()) 45 | }) 46 | 47 | it("returns an empty ConfigMap when directory does not exist", func() { 48 | Expect(os.RemoveAll(path)).To(Succeed()) 49 | 50 | cm, err := internal.NewConfigMapFromPath(path) 51 | Expect(err).NotTo(HaveOccurred()) 52 | 53 | Expect(cm).To(Equal(internal.ConfigMap{})) 54 | }) 55 | 56 | it("loads the ConfigMap from a directory", func() { 57 | Expect(os.WriteFile(filepath.Join(path, "test-key"), []byte("test-value"), 0600)).To(Succeed()) 58 | 59 | cm, err := internal.NewConfigMapFromPath(path) 60 | Expect(err).NotTo(HaveOccurred()) 61 | 62 | Expect(cm).To(Equal(internal.ConfigMap{"test-key": "test-value"})) 63 | }) 64 | 65 | it("ignores dirs and follows symlinks", func() { 66 | // this is necessary to support bindings mounted as k8s config maps & secrets 67 | Expect(os.MkdirAll(filepath.Join(path, ".hidden"), 0755)).To(Succeed()) 68 | Expect(os.WriteFile( 69 | filepath.Join(path, ".hidden", "test-key"), 70 | []byte("test-value"), 71 | 0600, 72 | )).To(Succeed()) 73 | Expect(os.Symlink( 74 | filepath.Join(".hidden", "test-key"), 75 | filepath.Join(path, "test-key"), 76 | )).To(Succeed()) 77 | 78 | cm, err := internal.NewConfigMapFromPath(path) 79 | Expect(err).NotTo(HaveOccurred()) 80 | Expect(cm).To(Equal(internal.ConfigMap{"test-key": "test-value"})) 81 | }) 82 | 83 | it("ignores hidden files", func() { 84 | Expect(os.WriteFile(filepath.Join(path, ".hidden-key"), []byte("hidden-value"), 0600)).To(Succeed()) 85 | 86 | cm, err := internal.NewConfigMapFromPath(path) 87 | Expect(err).NotTo(HaveOccurred()) 88 | 89 | Expect(cm).To(BeEmpty()) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /internal/directory_contents.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | "os" 23 | "path/filepath" 24 | 25 | "github.com/harshfelony/libcnb/v2/log" 26 | ) 27 | 28 | // DirectoryContentsWriter is used write the contents of a directory to the given io.Writer 29 | type DirectoryContentsWriter struct { 30 | format log.DirectoryContentFormatter 31 | writer io.Writer 32 | } 33 | 34 | // NewDirectoryContentsWriter returns a new DirectoryContentsWriter initialized and ready to be used 35 | func NewDirectoryContentsWriter(format log.DirectoryContentFormatter, writer io.Writer) DirectoryContentsWriter { 36 | return DirectoryContentsWriter{ 37 | format: format, 38 | writer: writer, 39 | } 40 | } 41 | 42 | // Write all the file contents to the writer 43 | func (d DirectoryContentsWriter) Write(title, path string) error { 44 | d.format.RootPath(path) 45 | 46 | if _, err := d.writer.Write([]byte(d.format.Title(title))); err != nil { 47 | return fmt.Errorf("unable to write title\n%w", err) 48 | } 49 | 50 | if err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 51 | if err != nil { 52 | return err 53 | } 54 | 55 | msg, err := d.format.File(path, info) 56 | if err != nil { 57 | return fmt.Errorf("unable to format\n%w", err) 58 | } 59 | 60 | if _, err := d.writer.Write([]byte(msg)); err != nil { 61 | return fmt.Errorf("unable to write\n%w", err) 62 | } 63 | 64 | return nil 65 | }); err != nil { 66 | return fmt.Errorf("error walking path %s\n%w", path, err) 67 | } 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /internal/directory_contents_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal_test 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | "testing" 25 | 26 | . "github.com/onsi/gomega" 27 | "github.com/sclevine/spec" 28 | 29 | "github.com/harshfelony/libcnb/v2/internal" 30 | ) 31 | 32 | func testDirectoryContentsWriter(t *testing.T, context spec.G, it spec.S) { 33 | var ( 34 | Expect = NewWithT(t).Expect 35 | 36 | path string 37 | buf bytes.Buffer 38 | ) 39 | 40 | it.Before(func() { 41 | var err error 42 | path, err = os.MkdirTemp("", "directory-contents") 43 | Expect(err).NotTo(HaveOccurred()) 44 | }) 45 | 46 | it.After(func() { 47 | Expect(os.RemoveAll(path)).To(Succeed()) 48 | }) 49 | 50 | context("directory content formats", func() { 51 | fm := internal.NewPlainDirectoryContentFormatter() 52 | 53 | it("formats title", func() { 54 | Expect(fm.Title("foo")).To(Equal("foo:\n")) 55 | }) 56 | 57 | it("formats a file", func() { 58 | cwd, err := os.Getwd() 59 | Expect(err).ToNot(HaveOccurred()) 60 | 61 | info, err := os.Stat(cwd) 62 | Expect(err).ToNot(HaveOccurred()) 63 | 64 | fm.RootPath(filepath.Dir(cwd)) 65 | 66 | Expect(fm.File(cwd, info)).To(Equal(fmt.Sprintf("%s\n", filepath.Base(cwd)))) 67 | }) 68 | }) 69 | 70 | it("lists empty directory contents", func() { 71 | fm := internal.NewPlainDirectoryContentFormatter() 72 | dc := internal.NewDirectoryContentsWriter(fm, &buf) 73 | 74 | Expect(dc.Write("title", path)).To(Succeed()) 75 | Expect(buf.String()).To(Equal("title:\n.\n")) 76 | }) 77 | 78 | it("lists directory contents", func() { 79 | f, err := os.Create(filepath.Join(path, "test-file")) 80 | Expect(err).NotTo(HaveOccurred()) 81 | defer f.Close() 82 | 83 | fm := internal.NewPlainDirectoryContentFormatter() 84 | dc := internal.NewDirectoryContentsWriter(fm, &buf) 85 | 86 | Expect(dc.Write("title", path)).To(Succeed()) 87 | Expect(buf.String()).To(Equal("title:\n.\ntest-file\n")) 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /internal/environment_writer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | ) 24 | 25 | // EnvironmentWriter is a type used to write an environment to file filesystem. 26 | type EnvironmentWriter struct{} 27 | 28 | // Write creates the path directory, and creates a new file for each key with the value as the contents of each file. 29 | func (w EnvironmentWriter) Write(path string, environment map[string]string) error { 30 | if len(environment) == 0 { 31 | return nil 32 | } 33 | 34 | if err := os.MkdirAll(path, 0755); err != nil { 35 | return fmt.Errorf("unable to mkdir %s\n%w", path, err) 36 | } 37 | 38 | for key, value := range environment { 39 | f := filepath.Join(path, key) 40 | 41 | // required to support process-specific environment variables 42 | if err := os.MkdirAll(filepath.Dir(f), 0755); err != nil { 43 | return fmt.Errorf("unable to mkdir from key %s\n%w", filepath.Dir(f), err) 44 | } 45 | 46 | //nolint:gosec 47 | if err := os.WriteFile(f, []byte(value), 0644); err != nil { 48 | return fmt.Errorf("unable to write file %s\n%w", f, err) 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/environment_writer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal_test 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | 24 | . "github.com/onsi/gomega" 25 | "github.com/sclevine/spec" 26 | 27 | "github.com/harshfelony/libcnb/v2/internal" 28 | ) 29 | 30 | func testEnvironmentWriter(t *testing.T, _ spec.G, it spec.S) { 31 | var ( 32 | Expect = NewWithT(t).Expect 33 | 34 | path string 35 | writer internal.EnvironmentWriter 36 | ) 37 | 38 | it.Before(func() { 39 | var err error 40 | path, err = os.MkdirTemp("", "environment-writer") 41 | Expect(err).NotTo(HaveOccurred()) 42 | Expect(os.RemoveAll(path)).To(Succeed()) 43 | }) 44 | 45 | it.After(func() { 46 | Expect(os.RemoveAll(path)).To(Succeed()) 47 | }) 48 | 49 | it("writes the given environment to a directory", func() { 50 | err := writer.Write(path, map[string]string{ 51 | "some-name": "some-content", 52 | "other-name": "other-content", 53 | }) 54 | Expect(err).NotTo(HaveOccurred()) 55 | 56 | content, err := os.ReadFile(filepath.Join(path, "some-name")) 57 | Expect(err).NotTo(HaveOccurred()) 58 | Expect(string(content)).To(Equal("some-content")) 59 | 60 | content, err = os.ReadFile(filepath.Join(path, "other-name")) 61 | Expect(err).NotTo(HaveOccurred()) 62 | Expect(string(content)).To(Equal("other-content")) 63 | }) 64 | 65 | it("writes the given environment with process specific envs to a directory", func() { 66 | err := writer.Write(path, map[string]string{ 67 | "some-proc/some-name": "some-content", 68 | }) 69 | Expect(err).NotTo(HaveOccurred()) 70 | 71 | content, err := os.ReadFile(filepath.Join(path, "some-proc", "some-name")) 72 | Expect(err).NotTo(HaveOccurred()) 73 | Expect(string(content)).To(Equal("some-content")) 74 | }) 75 | 76 | it("writes does not create a directory of the env map is empty", func() { 77 | err := writer.Write(path, map[string]string{}) 78 | Expect(err).NotTo(HaveOccurred()) 79 | 80 | Expect(path).NotTo(BeAnExistingFile()) 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /internal/execd_writer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | import ( 20 | "io" 21 | "os" 22 | 23 | "github.com/BurntSushi/toml" 24 | ) 25 | 26 | // ExecDWriter is a type used to write TOML files to fd3. 27 | type ExecDWriter struct { 28 | outputWriter io.Writer 29 | } 30 | 31 | // Option is a function for configuring an ExitHandler instance. 32 | type ExecDOption func(handler ExecDWriter) ExecDWriter 33 | 34 | // WithExecDOutputWriter creates an Option that configures the writer. 35 | func WithExecDOutputWriter(writer io.Writer) ExecDOption { 36 | return func(execdWriter ExecDWriter) ExecDWriter { 37 | execdWriter.outputWriter = writer 38 | return execdWriter 39 | } 40 | } 41 | 42 | // NewExitHandler creates a new instance that calls os.Exit and writes to os.stderr. 43 | func NewExecDWriter(options ...ExecDOption) ExecDWriter { 44 | h := ExecDWriter{ 45 | outputWriter: os.NewFile(3, "/dev/fd/3"), 46 | } 47 | 48 | for _, option := range options { 49 | h = option(h) 50 | } 51 | 52 | return h 53 | } 54 | 55 | // Write outputs the value serialized in TOML format to the appropriate writer. 56 | func (e ExecDWriter) Write(value map[string]string) error { 57 | if value == nil { 58 | return nil 59 | } 60 | 61 | return toml.NewEncoder(e.outputWriter).Encode(value) 62 | } 63 | -------------------------------------------------------------------------------- /internal/execd_writer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal_test 18 | 19 | import ( 20 | "bytes" 21 | "testing" 22 | 23 | . "github.com/onsi/gomega" 24 | "github.com/sclevine/spec" 25 | 26 | "github.com/harshfelony/libcnb/v2/internal" 27 | ) 28 | 29 | func testExecDWriter(t *testing.T, _ spec.G, it spec.S) { 30 | var ( 31 | Expect = NewWithT(t).Expect 32 | 33 | b *bytes.Buffer 34 | writer internal.ExecDWriter 35 | ) 36 | 37 | it.Before(func() { 38 | b = bytes.NewBuffer([]byte{}) 39 | 40 | writer = internal.NewExecDWriter( 41 | internal.WithExecDOutputWriter(b), 42 | ) 43 | }) 44 | 45 | it("writes the correct set of values", func() { 46 | env := map[string]string{ 47 | "test": "test", 48 | "test2": "te∆t", 49 | } 50 | Expect(writer.Write(env)).To(BeNil()) 51 | Expect(b.String()).To(internal.MatchTOML(` 52 | test = "test" 53 | test2 = "te∆t"`)) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /internal/exit_handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | "os" 23 | ) 24 | 25 | const ( 26 | // ErrorStatusCode is the status code returned for error. 27 | ErrorStatusCode = 1 28 | 29 | // FailStatusCode is the status code returned for fail. 30 | FailStatusCode = 100 31 | 32 | // PassStatusCode is the status code returned for pass. 33 | PassStatusCode = 0 34 | ) 35 | 36 | // ExitHandler is the default implementation of the libcnb.ExitHandler interface. 37 | type ExitHandler struct { 38 | exitFunc func(int) 39 | writer io.Writer 40 | } 41 | 42 | // Option is a function for configuring an ExitHandler instance. 43 | type Option func(handler ExitHandler) ExitHandler 44 | 45 | // WithExitHandler creates an Option that configures the exit function. 46 | func WithExitHandlerExitFunc(exitFunc func(int)) Option { 47 | return func(handler ExitHandler) ExitHandler { 48 | handler.exitFunc = exitFunc 49 | return handler 50 | } 51 | } 52 | 53 | // WithExitHandlerWriter creates an Option that configures the writer. 54 | func WithExitHandlerWriter(writer io.Writer) Option { 55 | return func(handler ExitHandler) ExitHandler { 56 | handler.writer = writer 57 | return handler 58 | } 59 | } 60 | 61 | // NewExitHandler creates a new instance that calls os.Exit and writes to os.stderr. 62 | func NewExitHandler(options ...Option) ExitHandler { 63 | h := ExitHandler{ 64 | exitFunc: os.Exit, 65 | writer: os.Stderr, 66 | } 67 | 68 | for _, option := range options { 69 | h = option(h) 70 | } 71 | 72 | return h 73 | } 74 | 75 | func (e ExitHandler) Error(err error) { 76 | _, _ = fmt.Fprintln(e.writer, err) 77 | e.exitFunc(ErrorStatusCode) 78 | } 79 | 80 | func (e ExitHandler) Fail() { 81 | e.exitFunc(FailStatusCode) 82 | } 83 | 84 | func (e ExitHandler) Pass() { 85 | e.exitFunc(PassStatusCode) 86 | } 87 | -------------------------------------------------------------------------------- /internal/exit_handler_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal_test 18 | 19 | import ( 20 | "bytes" 21 | "errors" 22 | "testing" 23 | 24 | . "github.com/onsi/gomega" 25 | "github.com/sclevine/spec" 26 | 27 | "github.com/harshfelony/libcnb/v2/internal" 28 | ) 29 | 30 | func testExitHandler(t *testing.T, _ spec.G, it spec.S) { 31 | var ( 32 | Expect = NewWithT(t).Expect 33 | 34 | b *bytes.Buffer 35 | exitCode int 36 | handler internal.ExitHandler 37 | ) 38 | 39 | it.Before(func() { 40 | b = bytes.NewBuffer([]byte{}) 41 | 42 | handler = internal.NewExitHandler( 43 | internal.WithExitHandlerExitFunc(func(c int) { exitCode = c }), 44 | internal.WithExitHandlerWriter(b), 45 | ) 46 | }) 47 | 48 | it("exits with code 0 when passing", func() { 49 | handler.Pass() 50 | Expect(exitCode).To(Equal(0)) 51 | }) 52 | 53 | it("exits with code 100 when failing", func() { 54 | handler.Fail() 55 | Expect(exitCode).To(Equal(100)) 56 | }) 57 | 58 | it("exits with code 1 when the error is non-nil", func() { 59 | handler.Error(errors.New("failed")) 60 | Expect(exitCode).To(Equal(1)) 61 | }) 62 | 63 | it("writes the error message", func() { 64 | handler.Error(errors.New("test-message")) 65 | Expect(b).To(ContainSubstring("test-message")) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /internal/formatter.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type PlainDirectoryContentFormatter struct { 10 | rootPath string 11 | } 12 | 13 | // NewPlainDirectoryContentFormatter returns a formatter applies no formatting 14 | // 15 | // The returned formatter operates as such: 16 | // 17 | // Title -> returns string followed by `:\n` 18 | // File -> returns file name relative to the root followed by `\n` 19 | func NewPlainDirectoryContentFormatter() *PlainDirectoryContentFormatter { 20 | return &PlainDirectoryContentFormatter{} 21 | } 22 | 23 | func (p *PlainDirectoryContentFormatter) File(path string, _ os.FileInfo) (string, error) { 24 | rel, err := filepath.Rel(p.rootPath, path) 25 | if err != nil { 26 | return "", fmt.Errorf("unable to calculate relative path %s -> %s\n%w", p.rootPath, path, err) 27 | } 28 | 29 | return fmt.Sprintf("%s\n", rel), nil 30 | } 31 | 32 | func (p *PlainDirectoryContentFormatter) RootPath(path string) { 33 | p.rootPath = path 34 | } 35 | 36 | func (p *PlainDirectoryContentFormatter) Title(title string) string { 37 | return fmt.Sprintf("%s:\n", title) 38 | } 39 | -------------------------------------------------------------------------------- /internal/formatter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal_test 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | . "github.com/onsi/gomega" 26 | "github.com/sclevine/spec" 27 | 28 | "github.com/harshfelony/libcnb/v2/internal" 29 | ) 30 | 31 | func testFormatters(t *testing.T, context spec.G, it spec.S) { 32 | var ( 33 | Expect = NewWithT(t).Expect 34 | 35 | path string 36 | ) 37 | 38 | it.Before(func() { 39 | var err error 40 | path, err = os.MkdirTemp("", "directory-contents") 41 | Expect(err).NotTo(HaveOccurred()) 42 | }) 43 | 44 | it.After(func() { 45 | Expect(os.RemoveAll(path)).To(Succeed()) 46 | }) 47 | 48 | context("directory content formats", func() { 49 | fm := internal.NewPlainDirectoryContentFormatter() 50 | 51 | it("formats title", func() { 52 | Expect(fm.Title("foo")).To(Equal("foo:\n")) 53 | }) 54 | 55 | it("formats a file", func() { 56 | cwd, err := os.Getwd() 57 | Expect(err).ToNot(HaveOccurred()) 58 | 59 | info, err := os.Stat(cwd) 60 | Expect(err).ToNot(HaveOccurred()) 61 | 62 | fm.RootPath(filepath.Dir(cwd)) 63 | 64 | Expect(fm.File(cwd, info)).To(Equal(fmt.Sprintf("%s\n", filepath.Base(cwd)))) 65 | }) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /internal/init_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/sclevine/spec" 23 | "github.com/sclevine/spec/report" 24 | ) 25 | 26 | func TestUnit(t *testing.T) { 27 | suite := spec.New("libcnb/internal", spec.Report(report.Terminal{})) 28 | suite("ConfigMap", testConfigMap) 29 | suite("DirectoryContents", testDirectoryContentsWriter) 30 | suite("EnvironmentWriter", testEnvironmentWriter) 31 | suite("ExitHandler", testExitHandler) 32 | suite("TOMLWriter", testTOMLWriter) 33 | suite("ExecDWriter", testExecDWriter) 34 | suite("Formatters", testFormatters) 35 | suite.Run(t) 36 | } 37 | -------------------------------------------------------------------------------- /internal/match_toml.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | 23 | "github.com/BurntSushi/toml" 24 | "github.com/onsi/gomega/types" 25 | ) 26 | 27 | func MatchTOML(expected interface{}) types.GomegaMatcher { 28 | return &matchTOML{ 29 | expected: expected, 30 | } 31 | } 32 | 33 | type matchTOML struct { 34 | expected interface{} 35 | } 36 | 37 | func (matcher *matchTOML) Match(actual interface{}) (success bool, err error) { 38 | var e, a string 39 | 40 | switch eType := matcher.expected.(type) { 41 | case string: 42 | e = eType 43 | case []byte: 44 | e = string(eType) 45 | default: 46 | return false, fmt.Errorf("expected value must be []byte or string, received %T", matcher.expected) 47 | } 48 | 49 | switch aType := actual.(type) { 50 | case string: 51 | a = aType 52 | case []byte: 53 | a = string(aType) 54 | default: 55 | return false, fmt.Errorf("actual value must be []byte or string, received %T", matcher.expected) 56 | } 57 | 58 | var eValue map[string]interface{} 59 | _, err = toml.Decode(e, &eValue) 60 | if err != nil { 61 | return false, err 62 | } 63 | 64 | var aValue map[string]interface{} 65 | _, err = toml.Decode(a, &aValue) 66 | if err != nil { 67 | return false, err 68 | } 69 | 70 | return reflect.DeepEqual(eValue, aValue), nil 71 | } 72 | 73 | func (matcher *matchTOML) FailureMessage(actual interface{}) (message string) { 74 | return fmt.Sprintf("Expected\n%s\nto match the TOML representation of\n%s", actual, matcher.expected) 75 | } 76 | 77 | func (matcher *matchTOML) NegatedFailureMessage(actual interface{}) (message string) { 78 | return fmt.Sprintf("Expected\n%s\nnot to match the TOML representation of\n%s", actual, matcher.expected) 79 | } 80 | -------------------------------------------------------------------------------- /internal/toml_writer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | 24 | "github.com/BurntSushi/toml" 25 | ) 26 | 27 | // TOMLWriter is a type used to write TOML files to the filesystem. 28 | type TOMLWriter struct{} 29 | 30 | // Write creates the path's parent directories, and creates a new file or truncates an existing file and then marshals 31 | // the value to the file. 32 | func (TOMLWriter) Write(path string, value interface{}) error { 33 | if value == nil { 34 | return nil 35 | } 36 | 37 | d := filepath.Dir(path) 38 | if err := os.MkdirAll(d, 0755); err != nil { 39 | return fmt.Errorf("unable to mkdir %s\n%w", d, err) 40 | } 41 | 42 | file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 43 | if err != nil { 44 | return fmt.Errorf("unable to open file %s\n%w", path, err) 45 | } 46 | defer file.Close() 47 | 48 | return toml.NewEncoder(file).Encode(value) 49 | } 50 | -------------------------------------------------------------------------------- /internal/toml_writer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal_test 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | 24 | . "github.com/onsi/gomega" 25 | "github.com/sclevine/spec" 26 | 27 | "github.com/harshfelony/libcnb/v2/internal" 28 | ) 29 | 30 | func testTOMLWriter(t *testing.T, _ spec.G, it spec.S) { 31 | var ( 32 | Expect = NewWithT(t).Expect 33 | 34 | parent string 35 | path string 36 | tomlWriter internal.TOMLWriter 37 | ) 38 | 39 | it.Before(func() { 40 | var err error 41 | parent, err = os.MkdirTemp("", "toml-writer") 42 | Expect(err).NotTo(HaveOccurred()) 43 | 44 | path = filepath.Join(parent, "text.toml") 45 | }) 46 | 47 | it.After(func() { 48 | Expect(os.RemoveAll(parent)).To(Succeed()) 49 | }) 50 | 51 | it("writes the contents of a given object out to a .toml file", func() { 52 | err := tomlWriter.Write(path, map[string]string{ 53 | "some-field": "some-value", 54 | "other-field": "other-value", 55 | }) 56 | Expect(err).NotTo(HaveOccurred()) 57 | 58 | Expect(os.ReadFile(path)).To(internal.MatchTOML(` 59 | some-field = "some-value" 60 | other-field = "other-value"`)) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /label.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | // Label represents an image label. 20 | type Label struct { 21 | // Key is the key of the label. 22 | Key string `toml:"key"` 23 | 24 | // Value is the value of the label. 25 | Value string `toml:"value"` 26 | } 27 | -------------------------------------------------------------------------------- /launch_toml.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | // LaunchTOML represents the contents of launch.toml. 20 | type LaunchTOML struct { 21 | // Labels is the collection of image labels contributed by the buildpack. 22 | Labels []Label `toml:"labels"` 23 | 24 | // Processes is the collection of process types contributed by the buildpack. 25 | Processes []Process `toml:"processes"` 26 | 27 | // Slices is the collection of slices contributed by the buildpack. 28 | Slices []Slice `toml:"slices"` 29 | } 30 | 31 | func (l LaunchTOML) isEmpty() bool { 32 | return len(l.Labels) == 0 && len(l.Processes) == 0 && len(l.Slices) == 0 33 | } 34 | -------------------------------------------------------------------------------- /layer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | 24 | "github.com/BurntSushi/toml" 25 | ) 26 | 27 | const ( 28 | BOMFormatCycloneDXExtension = "cdx.json" 29 | BOMFormatSPDXExtension = "spdx.json" 30 | BOMFormatSyftExtension = "syft.json" 31 | BOMMediaTypeCycloneDX = "application/vnd.cyclonedx+json" 32 | BOMMediaTypeSPDX = "application/spdx+json" 33 | BOMMediaTypeSyft = "application/vnd.syft+json" 34 | BOMUnknown = "unknown" 35 | ) 36 | 37 | // Exec represents the exec.d layer location 38 | type Exec struct { 39 | // Path is the path to the exec.d directory. 40 | Path string 41 | } 42 | 43 | // FilePath returns the fully qualified file path for a given name. 44 | func (e Exec) FilePath(name string) string { 45 | return filepath.Join(e.Path, name) 46 | } 47 | 48 | // ProcessFilePath returns the fully qualified file path for a given name. 49 | func (e Exec) ProcessFilePath(processType string, name string) string { 50 | return filepath.Join(e.Path, processType, name) 51 | } 52 | 53 | // BOMFormat indicates the format of the SBOM entry 54 | type SBOMFormat int 55 | 56 | const ( 57 | CycloneDXJSON SBOMFormat = iota 58 | SPDXJSON 59 | SyftJSON 60 | UnknownFormat 61 | ) 62 | 63 | func (b SBOMFormat) String() string { 64 | return []string{ 65 | BOMFormatCycloneDXExtension, 66 | BOMFormatSPDXExtension, 67 | BOMFormatSyftExtension, 68 | BOMUnknown}[b] 69 | } 70 | 71 | func (b SBOMFormat) MediaType() string { 72 | return []string{ 73 | BOMMediaTypeCycloneDX, 74 | BOMMediaTypeSPDX, 75 | BOMMediaTypeSyft, 76 | BOMUnknown}[b] 77 | } 78 | 79 | func SBOMFormatFromString(from string) (SBOMFormat, error) { 80 | switch from { 81 | case CycloneDXJSON.String(): 82 | return CycloneDXJSON, nil 83 | case SPDXJSON.String(): 84 | return SPDXJSON, nil 85 | case SyftJSON.String(): 86 | return SyftJSON, nil 87 | } 88 | 89 | return UnknownFormat, fmt.Errorf("unable to translate from %s to SBOMFormat", from) 90 | } 91 | 92 | // Contribute represents a layer managed by the buildpack. 93 | type Layer struct { 94 | // LayerTypes indicates the type of layer 95 | LayerTypes `toml:"types"` 96 | 97 | // Metadata is the metadata associated with the layer. 98 | Metadata map[string]interface{} `toml:"metadata"` 99 | 100 | // Name is the name of the layer. 101 | Name string `toml:"-"` 102 | 103 | // Path is the filesystem location of the layer. 104 | Path string `toml:"-"` 105 | 106 | // BuildEnvironment are the environment variables set at build time. 107 | BuildEnvironment Environment `toml:"-"` 108 | 109 | // LaunchEnvironment are the environment variables set at launch time. 110 | LaunchEnvironment Environment `toml:"-"` 111 | 112 | // SharedEnvironment are the environment variables set at both build and launch times. 113 | SharedEnvironment Environment `toml:"-"` 114 | 115 | // Exec is the exec.d executables set in the layer. 116 | Exec Exec `toml:"-"` 117 | } 118 | 119 | func (l Layer) Reset() (Layer, error) { 120 | l.LayerTypes = LayerTypes{ 121 | Build: false, 122 | Launch: false, 123 | Cache: false, 124 | } 125 | 126 | l.SharedEnvironment = Environment{} 127 | l.BuildEnvironment = Environment{} 128 | l.LaunchEnvironment = Environment{} 129 | l.Metadata = nil 130 | 131 | err := os.RemoveAll(l.Path) 132 | if err != nil { 133 | return Layer{}, fmt.Errorf("error could not remove file: %s", err) 134 | } 135 | 136 | err = os.MkdirAll(l.Path, os.ModePerm) 137 | if err != nil { 138 | return Layer{}, fmt.Errorf("error could not create directory: %s", err) 139 | } 140 | 141 | return l, nil 142 | } 143 | 144 | // SBOMPath returns the path to the layer specific SBOM File 145 | func (l Layer) SBOMPath(bt SBOMFormat) string { 146 | return filepath.Join(filepath.Dir(l.Path), fmt.Sprintf("%s.sbom.%s", l.Name, bt)) 147 | } 148 | 149 | // LayerTypes describes which types apply to a given layer. A layer may have any combination of Launch, Build, and 150 | // Cache types. 151 | type LayerTypes struct { 152 | // Build indicates that a layer should be used for builds. 153 | Build bool `toml:"build"` 154 | 155 | // Cache indicates that a layer should be cached. 156 | Cache bool `toml:"cache"` 157 | 158 | // Launch indicates that a layer should be used for launch. 159 | Launch bool `toml:"launch"` 160 | } 161 | 162 | // Layers represents the layers part of the specification. 163 | type Layers struct { 164 | // Path is the layers filesystem location. 165 | Path string 166 | } 167 | 168 | // Layer creates a new layer, loading metadata if it exists. 169 | func (l *Layers) Layer(name string) (Layer, error) { 170 | layer := Layer{ 171 | Name: name, 172 | Path: filepath.Join(l.Path, name), 173 | BuildEnvironment: Environment{}, 174 | LaunchEnvironment: Environment{}, 175 | SharedEnvironment: Environment{}, 176 | Exec: Exec{Path: filepath.Join(l.Path, name, "exec.d")}, 177 | } 178 | 179 | f := filepath.Join(l.Path, fmt.Sprintf("%s.toml", name)) 180 | if _, err := toml.DecodeFile(f, &layer); err != nil && !os.IsNotExist(err) { 181 | return Layer{}, fmt.Errorf("unable to decode layer metadata %s\n%w", f, err) 182 | } 183 | 184 | return layer, nil 185 | } 186 | 187 | // BOMBuildPath returns the full path to the build SBoM file for the buildpack 188 | func (l Layers) BuildSBOMPath(bt SBOMFormat) string { 189 | return filepath.Join(l.Path, fmt.Sprintf("build.sbom.%s", bt)) 190 | } 191 | 192 | // BOMLaunchPath returns the full path to the launch SBoM file for the buildpack 193 | func (l Layers) LaunchSBOMPath(bt SBOMFormat) string { 194 | return filepath.Join(l.Path, fmt.Sprintf("launch.sbom.%s", bt)) 195 | } 196 | -------------------------------------------------------------------------------- /layer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb_test 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | 24 | . "github.com/onsi/gomega" 25 | "github.com/sclevine/spec" 26 | 27 | "github.com/harshfelony/libcnb/v2" 28 | ) 29 | 30 | func testLayer(t *testing.T, context spec.G, it spec.S) { 31 | var ( 32 | Expect = NewWithT(t).Expect 33 | 34 | layers libcnb.Layers 35 | path string 36 | ) 37 | 38 | context("Exec", func() { 39 | var exec libcnb.Exec 40 | 41 | it.Before(func() { 42 | exec = libcnb.Exec{Path: "test-path"} 43 | }) 44 | 45 | it("returns filename", func() { 46 | Expect(exec.FilePath("test-name")).To(Equal(filepath.Join("test-path", "test-name"))) 47 | }) 48 | 49 | it("returns process-specific filename", func() { 50 | Expect(exec.ProcessFilePath("test-process", "test-name")). 51 | To(Equal(filepath.Join("test-path", "test-process", "test-name"))) 52 | }) 53 | }) 54 | 55 | context("Reset", func() { 56 | var layer libcnb.Layer 57 | 58 | it.Before(func() { 59 | layers = libcnb.Layers{Path: t.TempDir()} 60 | }) 61 | 62 | context("when there is no previous build", func() { 63 | it.Before(func() { 64 | layer = libcnb.Layer{ 65 | Name: "test-name", 66 | Path: filepath.Join(layers.Path, "test-name"), 67 | LayerTypes: libcnb.LayerTypes{ 68 | Launch: true, 69 | Build: true, 70 | Cache: true, 71 | }, 72 | } 73 | }) 74 | 75 | it("initializes an empty layer", func() { 76 | var err error 77 | layer, err = layer.Reset() 78 | Expect(err).NotTo(HaveOccurred()) 79 | 80 | Expect(layer).To(Equal(libcnb.Layer{ 81 | Name: "test-name", 82 | Path: filepath.Join(layers.Path, "test-name"), 83 | LayerTypes: libcnb.LayerTypes{ 84 | Launch: false, 85 | Build: false, 86 | Cache: false, 87 | }, 88 | SharedEnvironment: libcnb.Environment{}, 89 | BuildEnvironment: libcnb.Environment{}, 90 | LaunchEnvironment: libcnb.Environment{}, 91 | })) 92 | 93 | Expect(filepath.Join(layers.Path, "test-name")).To(BeADirectory()) 94 | }) 95 | }) 96 | 97 | context("when cache is retrieved from previous build", func() { 98 | it.Before(func() { 99 | sharedEnvDir := filepath.Join(layers.Path, "test-name", "env") 100 | Expect(os.MkdirAll(sharedEnvDir, os.ModePerm)).To(Succeed()) 101 | 102 | err := os.WriteFile(filepath.Join(sharedEnvDir, "OVERRIDE_VAR.override"), []byte("override-value"), 0600) 103 | Expect(err).NotTo(HaveOccurred()) 104 | 105 | buildEnvDir := filepath.Join(layers.Path, "test-name", "env.build") 106 | Expect(os.MkdirAll(buildEnvDir, os.ModePerm)).To(Succeed()) 107 | 108 | err = os.WriteFile(filepath.Join(buildEnvDir, "DEFAULT_VAR.default"), []byte("default-value"), 0600) 109 | Expect(err).NotTo(HaveOccurred()) 110 | 111 | err = os.WriteFile(filepath.Join(buildEnvDir, "INVALID_VAR.invalid"), []byte("invalid-value"), 0600) 112 | Expect(err).NotTo(HaveOccurred()) 113 | 114 | launchEnvDir := filepath.Join(layers.Path, "test-name", "env.launch") 115 | Expect(os.MkdirAll(launchEnvDir, os.ModePerm)).To(Succeed()) 116 | 117 | err = os.WriteFile(filepath.Join(launchEnvDir, "APPEND_VAR.append"), []byte("append-value"), 0600) 118 | Expect(err).NotTo(HaveOccurred()) 119 | 120 | err = os.WriteFile(filepath.Join(launchEnvDir, "APPEND_VAR.delim"), []byte("!"), 0600) 121 | Expect(err).NotTo(HaveOccurred()) 122 | 123 | layer = libcnb.Layer{ 124 | Name: "test-name", 125 | Path: filepath.Join(layers.Path, "test-name"), 126 | LayerTypes: libcnb.LayerTypes{ 127 | Launch: true, 128 | Build: true, 129 | Cache: true, 130 | }, 131 | SharedEnvironment: libcnb.Environment{ 132 | "OVERRIDE_VAR.override": "override-value", 133 | }, 134 | BuildEnvironment: libcnb.Environment{ 135 | "DEFAULT_VAR.default": "default-value", 136 | }, 137 | LaunchEnvironment: libcnb.Environment{ 138 | "APPEND_VAR.append": "append-value", 139 | "APPEND_VAR.delim": "!", 140 | }, 141 | Metadata: map[string]interface{}{ 142 | "some-key": "some-value", 143 | }, 144 | } 145 | }) 146 | 147 | context("when Reset is called on a layer", func() { 148 | it("resets all of the layer data and clears the directory", func() { 149 | layer, err := layer.Reset() 150 | Expect(err).NotTo(HaveOccurred()) 151 | 152 | Expect(layer).To(Equal(libcnb.Layer{ 153 | Name: "test-name", 154 | Path: filepath.Join(layers.Path, "test-name"), 155 | LayerTypes: libcnb.LayerTypes{ 156 | Launch: false, 157 | Build: false, 158 | Cache: false, 159 | }, 160 | SharedEnvironment: libcnb.Environment{}, 161 | BuildEnvironment: libcnb.Environment{}, 162 | LaunchEnvironment: libcnb.Environment{}, 163 | })) 164 | 165 | Expect(filepath.Join(layers.Path, "test-name")).To(BeADirectory()) 166 | 167 | files, err := filepath.Glob(filepath.Join(layers.Path, "test-name", "*")) 168 | Expect(err).NotTo(HaveOccurred()) 169 | 170 | Expect(files).To(BeEmpty()) 171 | }) 172 | }) 173 | }) 174 | 175 | context("could not remove files in layer", func() { 176 | it.Before(func() { 177 | Expect(os.Chmod(layers.Path, 0000)).To(Succeed()) 178 | }) 179 | 180 | it.After(func() { 181 | Expect(os.Chmod(layers.Path, 0777)).To(Succeed()) 182 | }) 183 | 184 | it("return an error", func() { 185 | layer := libcnb.Layer{ 186 | Name: "some-layer", 187 | Path: filepath.Join(layers.Path, "some-layer"), 188 | } 189 | 190 | _, err := layer.Reset() 191 | Expect(err).To(MatchError(ContainSubstring("error could not remove file: "))) 192 | Expect(err).To(MatchError(ContainSubstring("permission denied"))) 193 | }) 194 | }) 195 | }) 196 | 197 | context("Layers", func() { 198 | it.Before(func() { 199 | var err error 200 | path, err = os.MkdirTemp("", "layers") 201 | Expect(err).NotTo(HaveOccurred()) 202 | 203 | layers = libcnb.Layers{Path: path} 204 | }) 205 | 206 | it.After(func() { 207 | Expect(os.RemoveAll(path)).To(Succeed()) 208 | }) 209 | 210 | it("initializes layer", func() { 211 | l, err := layers.Layer("test-name") 212 | Expect(err).NotTo(HaveOccurred()) 213 | 214 | Expect(l.LayerTypes.Build).To(BeFalse()) 215 | Expect(l.LayerTypes.Cache).To(BeFalse()) 216 | Expect(l.LayerTypes.Launch).To(BeFalse()) 217 | Expect(l.Metadata).To(BeNil()) 218 | Expect(l.Name).To(Equal("test-name")) 219 | Expect(l.Path).To(Equal(filepath.Join(path, "test-name"))) 220 | Expect(l.BuildEnvironment).To(Equal(libcnb.Environment{})) 221 | Expect(l.LaunchEnvironment).To(Equal(libcnb.Environment{})) 222 | Expect(l.SharedEnvironment).To(Equal(libcnb.Environment{})) 223 | }) 224 | 225 | it("generates SBOM paths", func() { 226 | l, err := layers.Layer("test-name") 227 | Expect(err).NotTo(HaveOccurred()) 228 | 229 | Expect(l.Path).To(Equal(filepath.Join(path, "test-name"))) 230 | Expect(layers.BuildSBOMPath(libcnb.CycloneDXJSON)).To(Equal(filepath.Join(path, "build.sbom.cdx.json"))) 231 | Expect(layers.BuildSBOMPath(libcnb.SPDXJSON)).To(Equal(filepath.Join(path, "build.sbom.spdx.json"))) 232 | Expect(layers.BuildSBOMPath(libcnb.SyftJSON)).To(Equal(filepath.Join(path, "build.sbom.syft.json"))) 233 | Expect(layers.LaunchSBOMPath(libcnb.SyftJSON)).To(Equal(filepath.Join(path, "launch.sbom.syft.json"))) 234 | Expect(l.SBOMPath(libcnb.SyftJSON)).To(Equal(filepath.Join(path, "test-name.sbom.syft.json"))) 235 | }) 236 | 237 | it("maps from string to SBOM Format", func() { 238 | fmt, err := libcnb.SBOMFormatFromString("cdx.json") 239 | Expect(err).ToNot(HaveOccurred()) 240 | Expect(fmt).To(Equal(libcnb.CycloneDXJSON)) 241 | 242 | fmt, err = libcnb.SBOMFormatFromString("spdx.json") 243 | Expect(err).ToNot(HaveOccurred()) 244 | Expect(fmt).To(Equal(libcnb.SPDXJSON)) 245 | 246 | fmt, err = libcnb.SBOMFormatFromString("syft.json") 247 | Expect(err).ToNot(HaveOccurred()) 248 | Expect(fmt).To(Equal(libcnb.SyftJSON)) 249 | 250 | fmt, err = libcnb.SBOMFormatFromString("foobar.json") 251 | Expect(err).To(MatchError("unable to translate from foobar.json to SBOMFormat")) 252 | Expect(fmt).To(Equal(libcnb.UnknownFormat)) 253 | }) 254 | 255 | it("reads existing metadata", func() { 256 | Expect(os.WriteFile( 257 | filepath.Join(path, "test-name.toml"), 258 | []byte(` 259 | [types] 260 | launch = true 261 | build = false 262 | 263 | [metadata] 264 | test-key = "test-value" 265 | `), 266 | 0600), 267 | ).To(Succeed()) 268 | 269 | l, err := layers.Layer("test-name") 270 | Expect(err).NotTo(HaveOccurred()) 271 | 272 | Expect(l.Metadata).To(Equal(map[string]interface{}{"test-key": "test-value"})) 273 | Expect(l.Launch).To(BeTrue()) 274 | Expect(l.Build).To(BeFalse()) 275 | }) 276 | 277 | it("reads existing metadata with launch, build and cache all false", func() { 278 | Expect(os.WriteFile( 279 | filepath.Join(path, "test-name.toml"), 280 | []byte(` 281 | [types] 282 | launch = false 283 | build = false 284 | cache = false 285 | 286 | [metadata] 287 | test-key = "test-value" 288 | `), 289 | 0600), 290 | ).To(Succeed()) 291 | 292 | l, err := layers.Layer("test-name") 293 | Expect(err).NotTo(HaveOccurred()) 294 | 295 | Expect(l.Metadata).To(Equal(map[string]interface{}{"test-key": "test-value"})) 296 | Expect(l.Launch).To(BeFalse()) 297 | Expect(l.Build).To(BeFalse()) 298 | Expect(l.Cache).To(BeFalse()) 299 | }) 300 | }) 301 | } 302 | -------------------------------------------------------------------------------- /log/formatter.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2022 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package log 18 | 19 | import ( 20 | "os" 21 | ) 22 | 23 | //go:generate mockery --name DirectoryContentFormatter --case=underscore 24 | 25 | // DirectoryContentFormatter allows customization of logged directory output 26 | // 27 | // When libcnb logs the contents of a directory, each item in the directory 28 | // is passed through a DirectoryContentFormatter. 29 | // 30 | // DirectoryContentsWriter implements this workflow: 31 | // - call RootPath(string) with the root path that's being walked 32 | // - call Title(string) with the given title, the output is logged 33 | // - for each file in the directory: 34 | // - call File(string, os.FileInfo), the output is logged 35 | // 36 | // # A default implementation is provided that returns a formatter applies no formatting 37 | // 38 | // The returned formatter operates as such: 39 | // 40 | // Title -> returns string followed by `:\n` 41 | // File -> returns file name relative to the root followed by `\n` 42 | // 43 | // A buildpack author could provide their own implementation through 44 | // WithDirectoryContentFormatter when calling Detect or Build. 45 | // 46 | // A custom implementation might log in color or might log additional 47 | // information about each file, like permissions. The implementation can 48 | // also control line endings to force all of the files to be logged on a 49 | // single line, or as multiple lines. 50 | type DirectoryContentFormatter interface { 51 | // File takes the full path and os.FileInfo and returns a display string 52 | File(path string, info os.FileInfo) (string, error) 53 | 54 | // RootPath provides the root path being iterated 55 | RootPath(path string) 56 | 57 | // Title provides a plain string title which can be embellished 58 | Title(title string) string 59 | } 60 | -------------------------------------------------------------------------------- /log/init_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package log_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/sclevine/spec" 23 | "github.com/sclevine/spec/report" 24 | ) 25 | 26 | func TestUnit(t *testing.T) { 27 | suite := spec.New("libcnb/log", spec.Report(report.Terminal{})) 28 | suite("PlainLogger", testLogger) 29 | suite.Run(t) 30 | } 31 | -------------------------------------------------------------------------------- /log/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package log 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | "os" 23 | "strings" 24 | ) 25 | 26 | //go:generate mockery --name Logger --case=underscore 27 | 28 | // Logger is the interface implement by a type that wishes to write log messages generated by libcnb 29 | type Logger interface { 30 | // Debug formats using the default formats for its operands 31 | Debug(a ...interface{}) 32 | 33 | // Debugf formats according to a format specifier 34 | Debugf(format string, a ...interface{}) 35 | 36 | // DebugWriter returns the configured debug writer 37 | DebugWriter() io.Writer 38 | 39 | // IsDebugEnabled indicates whether debug logging is enabled 40 | IsDebugEnabled() bool 41 | } 42 | 43 | // PlainLogger implements Logger and logs messages to a writer. 44 | type PlainLogger struct { 45 | debug io.Writer 46 | } 47 | 48 | // New creates a new instance of PlainLogger. It configures debug logging if $BP_DEBUG or $BP_LOG_LEVEL are set. 49 | func New(debug io.Writer) PlainLogger { 50 | if strings.ToLower(os.Getenv("BP_LOG_LEVEL")) == "debug" || os.Getenv("BP_DEBUG") != "" { 51 | return PlainLogger{debug: debug} 52 | } 53 | 54 | return PlainLogger{} 55 | } 56 | 57 | // NewDiscard creates a new instance of PlainLogger that discards all log messages. Useful in testing. 58 | func NewDiscard() PlainLogger { 59 | return PlainLogger{debug: io.Discard} 60 | } 61 | 62 | // Debug formats using the default formats for its operands and writes to the configured debug writer. Spaces are added 63 | // between operands when neither is a string. 64 | func (l PlainLogger) Debug(a ...interface{}) { 65 | if !l.IsDebugEnabled() { 66 | return 67 | } 68 | 69 | s := fmt.Sprint(a...) 70 | 71 | if !strings.HasSuffix(s, "\n") { 72 | s += "\n" 73 | } 74 | 75 | _, _ = fmt.Fprint(l.debug, s) 76 | } 77 | 78 | // Debugf formats according to a format specifier and writes to the configured debug writer. 79 | func (l PlainLogger) Debugf(format string, a ...interface{}) { 80 | if !l.IsDebugEnabled() { 81 | return 82 | } 83 | 84 | if !strings.HasSuffix(format, "\n") { 85 | format += "\n" 86 | } 87 | 88 | _, _ = fmt.Fprintf(l.debug, format, a...) 89 | } 90 | 91 | // DebugWriter returns the configured debug writer. 92 | func (l PlainLogger) DebugWriter() io.Writer { 93 | if l.IsDebugEnabled() { 94 | return l.debug 95 | } 96 | return io.Discard 97 | } 98 | 99 | // IsDebugEnabled indicates whether debug logging is enabled. 100 | func (l PlainLogger) IsDebugEnabled() bool { 101 | return l.debug != nil 102 | } 103 | -------------------------------------------------------------------------------- /log/logger_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package log_test 18 | 19 | import ( 20 | "bytes" 21 | "io" 22 | "os" 23 | "testing" 24 | 25 | . "github.com/onsi/gomega" 26 | "github.com/sclevine/spec" 27 | 28 | "github.com/harshfelony/libcnb/v2/log" 29 | ) 30 | 31 | func testLogger(t *testing.T, context spec.G, it spec.S) { 32 | var ( 33 | Expect = NewWithT(t).Expect 34 | 35 | b *bytes.Buffer 36 | l log.PlainLogger 37 | ) 38 | 39 | it.Before(func() { 40 | b = bytes.NewBuffer(nil) 41 | }) 42 | 43 | context("without BP_DEBUG", func() { 44 | it.Before(func() { 45 | l = log.New(b) 46 | }) 47 | 48 | it("does not configure debug", func() { 49 | Expect(l.IsDebugEnabled()).To(BeFalse()) 50 | }) 51 | 52 | it("does not return nil debug writer", func() { 53 | Expect(l.DebugWriter()).To(Not(BeNil())) 54 | }) 55 | 56 | it("does not return non-discard writer", func() { 57 | Expect(l.DebugWriter()).To(Equal(io.Discard)) 58 | }) 59 | }) 60 | 61 | context("with BP_DEBUG", func() { 62 | it.Before(func() { 63 | Expect(os.Setenv("BP_DEBUG", "")).To(Succeed()) 64 | l = log.New(b) 65 | }) 66 | 67 | it.After(func() { 68 | Expect(os.Unsetenv("BP_DEBUG")).To(Succeed()) 69 | }) 70 | 71 | it("configures debug", func() { 72 | Expect(l.IsDebugEnabled()).To(BeFalse()) 73 | }) 74 | }) 75 | 76 | context("with BP_LOG_LEVEL set to DEBUG", func() { 77 | it.Before(func() { 78 | Expect(os.Setenv("BP_LOG_LEVEL", "DEBUG")).To(Succeed()) 79 | l = log.New(b) 80 | }) 81 | 82 | it.After(func() { 83 | Expect(os.Unsetenv("BP_LOG_LEVEL")).To(Succeed()) 84 | }) 85 | 86 | it("configures debug", func() { 87 | Expect(l.IsDebugEnabled()).To(BeTrue()) 88 | }) 89 | }) 90 | 91 | context("with debug enabled", func() { 92 | it.Before(func() { 93 | Expect(os.Setenv("BP_LOG_LEVEL", "DEBUG")).To(Succeed()) 94 | l = log.New(b) 95 | }) 96 | 97 | it.After(func() { 98 | Expect(os.Unsetenv("BP_LOG_LEVEL")).To(Succeed()) 99 | }) 100 | 101 | it("writes debug log", func() { 102 | l.Debug("test-message") 103 | Expect(b.String()).To(Equal("test-message\n")) 104 | }) 105 | 106 | it("writes debugf log", func() { 107 | l.Debugf("test-%s", "message") 108 | Expect(b.String()).To(Equal("test-message\n")) 109 | }) 110 | 111 | it("writes debug directly", func() { 112 | _, err := l.DebugWriter().Write([]byte("test-message\n")) 113 | Expect(err).NotTo(HaveOccurred()) 114 | Expect(b.String()).To(Equal("test-message\n")) 115 | }) 116 | 117 | it("indicates that debug is enabled", func() { 118 | Expect(l.IsDebugEnabled()).To(BeTrue()) 119 | }) 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /log/mocks/directory_content_formatter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | fs "io/fs" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // DirectoryContentFormatter is an autogenerated mock type for the DirectoryContentFormatter type 12 | type DirectoryContentFormatter struct { 13 | mock.Mock 14 | } 15 | 16 | // File provides a mock function with given fields: path, info 17 | func (_m *DirectoryContentFormatter) File(path string, info fs.FileInfo) (string, error) { 18 | ret := _m.Called(path, info) 19 | 20 | if len(ret) == 0 { 21 | panic("no return value specified for File") 22 | } 23 | 24 | var r0 string 25 | var r1 error 26 | if rf, ok := ret.Get(0).(func(string, fs.FileInfo) (string, error)); ok { 27 | return rf(path, info) 28 | } 29 | if rf, ok := ret.Get(0).(func(string, fs.FileInfo) string); ok { 30 | r0 = rf(path, info) 31 | } else { 32 | r0 = ret.Get(0).(string) 33 | } 34 | 35 | if rf, ok := ret.Get(1).(func(string, fs.FileInfo) error); ok { 36 | r1 = rf(path, info) 37 | } else { 38 | r1 = ret.Error(1) 39 | } 40 | 41 | return r0, r1 42 | } 43 | 44 | // RootPath provides a mock function with given fields: path 45 | func (_m *DirectoryContentFormatter) RootPath(path string) { 46 | _m.Called(path) 47 | } 48 | 49 | // Title provides a mock function with given fields: title 50 | func (_m *DirectoryContentFormatter) Title(title string) string { 51 | ret := _m.Called(title) 52 | 53 | if len(ret) == 0 { 54 | panic("no return value specified for Title") 55 | } 56 | 57 | var r0 string 58 | if rf, ok := ret.Get(0).(func(string) string); ok { 59 | r0 = rf(title) 60 | } else { 61 | r0 = ret.Get(0).(string) 62 | } 63 | 64 | return r0 65 | } 66 | 67 | // 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. 68 | // The first argument is typically a *testing.T value. 69 | func NewDirectoryContentFormatter(t interface { 70 | mock.TestingT 71 | Cleanup(func()) 72 | }) *DirectoryContentFormatter { 73 | mock := &DirectoryContentFormatter{} 74 | mock.Mock.Test(t) 75 | 76 | t.Cleanup(func() { mock.AssertExpectations(t) }) 77 | 78 | return mock 79 | } 80 | -------------------------------------------------------------------------------- /log/mocks/logger.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | io "io" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // Logger is an autogenerated mock type for the Logger type 12 | type Logger struct { 13 | mock.Mock 14 | } 15 | 16 | // Debug provides a mock function with given fields: a 17 | func (_m *Logger) Debug(a ...interface{}) { 18 | var _ca []interface{} 19 | _ca = append(_ca, a...) 20 | _m.Called(_ca...) 21 | } 22 | 23 | // DebugWriter provides a mock function with given fields: 24 | func (_m *Logger) DebugWriter() io.Writer { 25 | ret := _m.Called() 26 | 27 | if len(ret) == 0 { 28 | panic("no return value specified for DebugWriter") 29 | } 30 | 31 | var r0 io.Writer 32 | if rf, ok := ret.Get(0).(func() io.Writer); ok { 33 | r0 = rf() 34 | } else { 35 | if ret.Get(0) != nil { 36 | r0 = ret.Get(0).(io.Writer) 37 | } 38 | } 39 | 40 | return r0 41 | } 42 | 43 | // Debugf provides a mock function with given fields: format, a 44 | func (_m *Logger) Debugf(format string, a ...interface{}) { 45 | var _ca []interface{} 46 | _ca = append(_ca, format) 47 | _ca = append(_ca, a...) 48 | _m.Called(_ca...) 49 | } 50 | 51 | // IsDebugEnabled provides a mock function with given fields: 52 | func (_m *Logger) IsDebugEnabled() bool { 53 | ret := _m.Called() 54 | 55 | if len(ret) == 0 { 56 | panic("no return value specified for IsDebugEnabled") 57 | } 58 | 59 | var r0 bool 60 | if rf, ok := ret.Get(0).(func() bool); ok { 61 | r0 = rf() 62 | } else { 63 | r0 = ret.Get(0).(bool) 64 | } 65 | 66 | return r0 67 | } 68 | 69 | // 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. 70 | // The first argument is typically a *testing.T value. 71 | func NewLogger(t interface { 72 | mock.TestingT 73 | Cleanup(func()) 74 | }) *Logger { 75 | mock := &Logger{} 76 | mock.Mock.Test(t) 77 | 78 | t.Cleanup(func() { mock.AssertExpectations(t) }) 79 | 80 | return mock 81 | } 82 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | import ( 20 | "fmt" 21 | "path/filepath" 22 | ) 23 | 24 | func main(detect DetectFunc, build BuildFunc, generate GenerateFunc, options ...Option) { 25 | config := NewConfig(options...) 26 | 27 | if len(config.arguments) == 0 { 28 | config.exitHandler.Error(fmt.Errorf("expected command name")) 29 | return 30 | } 31 | 32 | config.extension = build == nil && generate != nil 33 | 34 | switch c := filepath.Base(config.arguments[0]); c { 35 | case "build": 36 | Build(build, config) 37 | case "detect": 38 | Detect(detect, config) 39 | case "generate": 40 | Generate(generate, config) 41 | default: 42 | config.exitHandler.Error(fmt.Errorf("unsupported command %s", c)) 43 | return 44 | } 45 | } 46 | 47 | // BuildpackMain is called by the main function of a buildpack, encapsulating both detection and build in the same binary. 48 | func BuildpackMain(detect DetectFunc, build BuildFunc, options ...Option) { 49 | main(detect, build, nil, options...) 50 | } 51 | 52 | // ExtensionMain is called by the main function of a extension, encapsulating both detection and generation in the same binary. 53 | func ExtensionMain(detect DetectFunc, generate GenerateFunc, options ...Option) { 54 | main(detect, nil, generate, options...) 55 | } 56 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb_test 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | 24 | . "github.com/onsi/gomega" 25 | "github.com/sclevine/spec" 26 | "github.com/stretchr/testify/mock" 27 | 28 | "github.com/harshfelony/libcnb/v2" 29 | "github.com/harshfelony/libcnb/v2/log" 30 | "github.com/harshfelony/libcnb/v2/mocks" 31 | ) 32 | 33 | func testMain(t *testing.T, _ spec.G, it spec.S) { 34 | var ( 35 | Expect = NewWithT(t).Expect 36 | 37 | applicationPath string 38 | buildFunc libcnb.BuildFunc 39 | buildpackPath string 40 | buildpackPlanPath string 41 | detectFunc libcnb.DetectFunc 42 | environmentWriter *mocks.EnvironmentWriter 43 | exitHandler *mocks.ExitHandler 44 | generateFunc libcnb.GenerateFunc 45 | layersPath string 46 | platformPath string 47 | tomlWriter *mocks.TOMLWriter 48 | 49 | workingDir string 50 | ) 51 | 52 | it.Before(func() { 53 | var err error 54 | 55 | applicationPath, err = os.MkdirTemp("", "main-application-path") 56 | Expect(err).NotTo(HaveOccurred()) 57 | applicationPath, err = filepath.EvalSymlinks(applicationPath) 58 | Expect(err).NotTo(HaveOccurred()) 59 | 60 | buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { 61 | return libcnb.NewBuildResult(), nil 62 | } 63 | 64 | buildpackPath, err = os.MkdirTemp("", "main-buildpack-path") 65 | Expect(err).NotTo(HaveOccurred()) 66 | Expect(os.Setenv("CNB_BUILDPACK_DIR", buildpackPath)).To(Succeed()) 67 | 68 | Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), 69 | []byte(` 70 | api = "0.8" 71 | 72 | [buildpack] 73 | id = "test-id" 74 | name = "test-name" 75 | version = "1.1.1" 76 | clear-env = true 77 | 78 | [[order]] 79 | [[order.group]] 80 | id = "test-id" 81 | version = "2.2.2" 82 | optional = true 83 | 84 | [[stacks]] 85 | id = "test-id" 86 | 87 | [metadata] 88 | test-key = "test-value" 89 | `), 90 | 0600), 91 | ).To(Succeed()) 92 | 93 | f, err := os.CreateTemp("", "main-buildpackplan-path") 94 | Expect(err).NotTo(HaveOccurred()) 95 | Expect(f.Close()).NotTo(HaveOccurred()) 96 | buildpackPlanPath = f.Name() 97 | 98 | Expect(os.WriteFile(buildpackPlanPath, 99 | []byte(` 100 | [[entries]] 101 | name = "test-name" 102 | version = "test-version" 103 | 104 | [entries.metadata] 105 | test-key = "test-value" 106 | `), 107 | 0600), 108 | ).To(Succeed()) 109 | 110 | detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { 111 | return libcnb.DetectResult{}, nil 112 | } 113 | 114 | generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) { 115 | return libcnb.GenerateResult{}, nil 116 | } 117 | 118 | environmentWriter = &mocks.EnvironmentWriter{} 119 | environmentWriter.On("Write", mock.Anything, mock.Anything).Return(nil) 120 | 121 | exitHandler = &mocks.ExitHandler{} 122 | exitHandler.On("Error", mock.Anything) 123 | exitHandler.On("Pass", mock.Anything) 124 | exitHandler.On("Fail", mock.Anything) 125 | 126 | layersPath, err = os.MkdirTemp("", "main-layers-path") 127 | Expect(err).NotTo(HaveOccurred()) 128 | 129 | Expect(os.WriteFile(filepath.Join(layersPath, "store.toml"), 130 | []byte(` 131 | [metadata] 132 | test-key = "test-value" 133 | `), 134 | 0600), 135 | ).To(Succeed()) 136 | 137 | platformPath, err = os.MkdirTemp("", "main-platform-path") 138 | Expect(err).NotTo(HaveOccurred()) 139 | 140 | Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha", "metadata"), 0755)).To(Succeed()) 141 | Expect(os.WriteFile( 142 | filepath.Join(platformPath, "bindings", "alpha", "metadata", "test-metadata-key"), 143 | []byte("test-metadata-value"), 144 | 0600, 145 | )).To(Succeed()) 146 | Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha", "secret"), 0755)).To(Succeed()) 147 | Expect(os.WriteFile( 148 | filepath.Join(platformPath, "bindings", "alpha", "secret", "test-secret-key"), 149 | []byte("test-secret-value"), 150 | 0600, 151 | )).To(Succeed()) 152 | 153 | Expect(os.MkdirAll(filepath.Join(platformPath, "env"), 0755)).To(Succeed()) 154 | Expect(os.WriteFile(filepath.Join(platformPath, "env", "TEST_ENV"), []byte("test-value"), 0600)). 155 | To(Succeed()) 156 | 157 | tomlWriter = &mocks.TOMLWriter{} 158 | tomlWriter.On("Write", mock.Anything, mock.Anything).Return(nil) 159 | 160 | Expect(os.Setenv("CNB_STACK_ID", "test-stack-id")).To(Succeed()) 161 | Expect(os.Setenv("CNB_LAYERS_DIR", layersPath)).To(Succeed()) 162 | Expect(os.Setenv("CNB_PLATFORM_DIR", platformPath)).To(Succeed()) 163 | Expect(os.Setenv("CNB_BP_PLAN_PATH", buildpackPlanPath)).To(Succeed()) 164 | Expect(os.Setenv("CNB_BUILD_PLAN_PATH", buildpackPlanPath)).To(Succeed()) 165 | 166 | workingDir, err = os.Getwd() 167 | Expect(err).NotTo(HaveOccurred()) 168 | Expect(os.Chdir(applicationPath)).To(Succeed()) 169 | }) 170 | 171 | it.After(func() { 172 | Expect(os.Chdir(workingDir)).To(Succeed()) 173 | Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed()) 174 | Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed()) 175 | Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed()) 176 | Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed()) 177 | Expect(os.Unsetenv("CNB_LAYERS_DIR")).To(Succeed()) 178 | Expect(os.Unsetenv("CNB_BUILD_PLAN_PATH")).To(Succeed()) 179 | 180 | Expect(os.RemoveAll(applicationPath)).To(Succeed()) 181 | Expect(os.RemoveAll(buildpackPath)).To(Succeed()) 182 | Expect(os.RemoveAll(buildpackPlanPath)).To(Succeed()) 183 | Expect(os.RemoveAll(layersPath)).To(Succeed()) 184 | Expect(os.RemoveAll(platformPath)).To(Succeed()) 185 | }) 186 | 187 | it("encounters the wrong number of arguments", func() { 188 | libcnb.BuildpackMain(detectFunc, buildFunc, 189 | libcnb.WithArguments([]string{}), 190 | libcnb.WithExitHandler(exitHandler), 191 | libcnb.WithLogger(log.NewDiscard()), 192 | ) 193 | 194 | Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("expected command name")) 195 | }) 196 | 197 | it("calls builder for build command", func() { 198 | commandPath := filepath.Join("bin", "build") 199 | 200 | libcnb.BuildpackMain(detectFunc, buildFunc, 201 | libcnb.WithArguments([]string{commandPath}), 202 | libcnb.WithExitHandler(exitHandler), 203 | libcnb.WithLogger(log.NewDiscard()), 204 | ) 205 | 206 | Expect(exitHandler.Calls).To(BeEmpty()) 207 | }) 208 | 209 | it("calls detector for detect command", func() { 210 | detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { 211 | return libcnb.DetectResult{Pass: true}, nil 212 | } 213 | commandPath := filepath.Join("bin", "detect") 214 | 215 | libcnb.BuildpackMain(detectFunc, buildFunc, 216 | libcnb.WithArguments([]string{commandPath}), 217 | libcnb.WithExitHandler(exitHandler), 218 | libcnb.WithLogger(log.NewDiscard()), 219 | ) 220 | }) 221 | 222 | it("calls generator for generate command", func() { 223 | generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) { 224 | return libcnb.GenerateResult{}, nil 225 | } 226 | commandPath := filepath.Join("bin", "generate") 227 | 228 | libcnb.ExtensionMain(nil, generateFunc, 229 | libcnb.WithArguments([]string{commandPath}), 230 | libcnb.WithExitHandler(exitHandler), 231 | libcnb.WithLogger(log.NewDiscard()), 232 | ) 233 | }) 234 | 235 | it("calls exitHandler.Pass() on detection pass", func() { 236 | detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { 237 | return libcnb.DetectResult{Pass: true}, nil 238 | } 239 | commandPath := filepath.Join("bin", "detect") 240 | 241 | libcnb.BuildpackMain(detectFunc, buildFunc, 242 | libcnb.WithArguments([]string{commandPath}), 243 | libcnb.WithExitHandler(exitHandler), 244 | libcnb.WithLogger(log.NewDiscard()), 245 | ) 246 | 247 | Expect(exitHandler.Calls[0].Method).To(BeIdenticalTo("Pass")) 248 | }) 249 | 250 | it("calls exitHandler.Fail() on detection fail", func() { 251 | detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { 252 | return libcnb.DetectResult{Pass: false}, nil 253 | } 254 | commandPath := filepath.Join("bin", "detect") 255 | 256 | libcnb.BuildpackMain(detectFunc, buildFunc, 257 | libcnb.WithArguments([]string{commandPath}), 258 | libcnb.WithExitHandler(exitHandler), 259 | libcnb.WithLogger(log.NewDiscard()), 260 | ) 261 | 262 | Expect(exitHandler.Calls[0].Method).To(BeIdenticalTo("Fail")) 263 | }) 264 | 265 | it("encounters an unknown command", func() { 266 | commandPath := filepath.Join("bin", "test-command") 267 | 268 | libcnb.BuildpackMain(detectFunc, buildFunc, 269 | libcnb.WithArguments([]string{commandPath}), 270 | libcnb.WithExitHandler(exitHandler), 271 | libcnb.WithLogger(log.NewDiscard()), 272 | ) 273 | 274 | Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unsupported command test-command")) 275 | }) 276 | } 277 | -------------------------------------------------------------------------------- /mocks/environment_writer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // EnvironmentWriter is an autogenerated mock type for the EnvironmentWriter type 8 | type EnvironmentWriter struct { 9 | mock.Mock 10 | } 11 | 12 | // Write provides a mock function with given fields: dir, environment 13 | func (_m *EnvironmentWriter) Write(dir string, environment map[string]string) error { 14 | ret := _m.Called(dir, environment) 15 | 16 | if len(ret) == 0 { 17 | panic("no return value specified for Write") 18 | } 19 | 20 | var r0 error 21 | if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok { 22 | r0 = rf(dir, environment) 23 | } else { 24 | r0 = ret.Error(0) 25 | } 26 | 27 | return r0 28 | } 29 | 30 | // 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. 31 | // The first argument is typically a *testing.T value. 32 | func NewEnvironmentWriter(t interface { 33 | mock.TestingT 34 | Cleanup(func()) 35 | }) *EnvironmentWriter { 36 | mock := &EnvironmentWriter{} 37 | mock.Mock.Test(t) 38 | 39 | t.Cleanup(func() { mock.AssertExpectations(t) }) 40 | 41 | return mock 42 | } 43 | -------------------------------------------------------------------------------- /mocks/exec_d.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // ExecD is an autogenerated mock type for the ExecD type 8 | type ExecD struct { 9 | mock.Mock 10 | } 11 | 12 | // Execute provides a mock function with given fields: 13 | func (_m *ExecD) Execute() (map[string]string, error) { 14 | ret := _m.Called() 15 | 16 | if len(ret) == 0 { 17 | panic("no return value specified for Execute") 18 | } 19 | 20 | var r0 map[string]string 21 | var r1 error 22 | if rf, ok := ret.Get(0).(func() (map[string]string, error)); ok { 23 | return rf() 24 | } 25 | if rf, ok := ret.Get(0).(func() map[string]string); ok { 26 | r0 = rf() 27 | } else { 28 | if ret.Get(0) != nil { 29 | r0 = ret.Get(0).(map[string]string) 30 | } 31 | } 32 | 33 | if rf, ok := ret.Get(1).(func() error); ok { 34 | r1 = rf() 35 | } else { 36 | r1 = ret.Error(1) 37 | } 38 | 39 | return r0, r1 40 | } 41 | 42 | // 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. 43 | // The first argument is typically a *testing.T value. 44 | func NewExecD(t interface { 45 | mock.TestingT 46 | Cleanup(func()) 47 | }) *ExecD { 48 | mock := &ExecD{} 49 | mock.Mock.Test(t) 50 | 51 | t.Cleanup(func() { mock.AssertExpectations(t) }) 52 | 53 | return mock 54 | } 55 | -------------------------------------------------------------------------------- /mocks/exec_d_writer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // ExecDWriter is an autogenerated mock type for the ExecDWriter type 8 | type ExecDWriter struct { 9 | mock.Mock 10 | } 11 | 12 | // Write provides a mock function with given fields: value 13 | func (_m *ExecDWriter) Write(value map[string]string) error { 14 | ret := _m.Called(value) 15 | 16 | if len(ret) == 0 { 17 | panic("no return value specified for Write") 18 | } 19 | 20 | var r0 error 21 | if rf, ok := ret.Get(0).(func(map[string]string) error); ok { 22 | r0 = rf(value) 23 | } else { 24 | r0 = ret.Error(0) 25 | } 26 | 27 | return r0 28 | } 29 | 30 | // 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. 31 | // The first argument is typically a *testing.T value. 32 | func NewExecDWriter(t interface { 33 | mock.TestingT 34 | Cleanup(func()) 35 | }) *ExecDWriter { 36 | mock := &ExecDWriter{} 37 | mock.Mock.Test(t) 38 | 39 | t.Cleanup(func() { mock.AssertExpectations(t) }) 40 | 41 | return mock 42 | } 43 | -------------------------------------------------------------------------------- /mocks/exit_handler.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // ExitHandler is an autogenerated mock type for the ExitHandler type 8 | type ExitHandler struct { 9 | mock.Mock 10 | } 11 | 12 | // Error provides a mock function with given fields: _a0 13 | func (_m *ExitHandler) Error(_a0 error) { 14 | _m.Called(_a0) 15 | } 16 | 17 | // Fail provides a mock function with given fields: 18 | func (_m *ExitHandler) Fail() { 19 | _m.Called() 20 | } 21 | 22 | // Pass provides a mock function with given fields: 23 | func (_m *ExitHandler) Pass() { 24 | _m.Called() 25 | } 26 | 27 | // 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. 28 | // The first argument is typically a *testing.T value. 29 | func NewExitHandler(t interface { 30 | mock.TestingT 31 | Cleanup(func()) 32 | }) *ExitHandler { 33 | mock := &ExitHandler{} 34 | mock.Mock.Test(t) 35 | 36 | t.Cleanup(func() { mock.AssertExpectations(t) }) 37 | 38 | return mock 39 | } 40 | -------------------------------------------------------------------------------- /mocks/toml_writer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // TOMLWriter is an autogenerated mock type for the TOMLWriter type 8 | type TOMLWriter struct { 9 | mock.Mock 10 | } 11 | 12 | // Write provides a mock function with given fields: path, value 13 | func (_m *TOMLWriter) Write(path string, value interface{}) error { 14 | ret := _m.Called(path, value) 15 | 16 | if len(ret) == 0 { 17 | panic("no return value specified for Write") 18 | } 19 | 20 | var r0 error 21 | if rf, ok := ret.Get(0).(func(string, interface{}) error); ok { 22 | r0 = rf(path, value) 23 | } else { 24 | r0 = ret.Error(0) 25 | } 26 | 27 | return r0 28 | } 29 | 30 | // 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. 31 | // The first argument is typically a *testing.T value. 32 | func NewTOMLWriter(t interface { 33 | mock.TestingT 34 | Cleanup(func()) 35 | }) *TOMLWriter { 36 | mock := &TOMLWriter{} 37 | mock.Mock.Test(t) 38 | 39 | t.Cleanup(func() { mock.AssertExpectations(t) }) 40 | 41 | return mock 42 | } 43 | -------------------------------------------------------------------------------- /platform.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | import ( 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "io/fs" 24 | "os" 25 | "path/filepath" 26 | "sort" 27 | "strings" 28 | 29 | "github.com/harshfelony/libcnb/v2/internal" 30 | ) 31 | 32 | const ( 33 | // BindingProvider is the key for a binding's provider. 34 | BindingProvider = "provider" 35 | 36 | // BindingType is the key for a binding's type. 37 | BindingType = "type" 38 | 39 | // EnvServiceBindings is the name of the environment variable that contains the path to service bindings directory. 40 | // 41 | // See the Service Binding Specification for Kubernetes for more details - https://k8s-service-bindings.github.io/spec/ 42 | EnvServiceBindings = "SERVICE_BINDING_ROOT" 43 | 44 | // EnvBuildpackDirectory is the name of the environment variable that contains the path to the buildpack 45 | EnvBuildpackDirectory = "CNB_BUILDPACK_DIR" 46 | 47 | // EnvExtensionDirectory is the name of the environment variable that contains the path to the extension 48 | EnvExtensionDirectory = "CNB_EXTENSION_DIR" 49 | 50 | // EnvVcapServices is the name of the environment variable that contains the bindings in cloudfoundry 51 | EnvVcapServices = "VCAP_SERVICES" 52 | 53 | // EnvLayersDirectory is the name of the environment variable that contains the root path to all buildpack layers 54 | EnvLayersDirectory = "CNB_LAYERS_DIR" 55 | 56 | // EnvOutputDirectory is the name of the environment variable that contains the path to the output directory 57 | EnvOutputDirectory = "CNB_OUTPUT_DIR" 58 | 59 | // EnvPlatformDirectory is the name of the environment variable that contains the path to the platform directory 60 | EnvPlatformDirectory = "CNB_PLATFORM_DIR" 61 | 62 | // EnvDetectBuildPlanPath is the name of the environment variable that contains the path to the build plan 63 | EnvDetectPlanPath = "CNB_BUILD_PLAN_PATH" 64 | 65 | // EnvBuildPlanPath is the name of the environment variable that contains the path to the build plan 66 | EnvBuildPlanPath = "CNB_BP_PLAN_PATH" 67 | 68 | // Deprecated: EnvStackID is the name of the environment variable that contains the stack id 69 | EnvStackID = "CNB_STACK_ID" 70 | 71 | // EnvTargetOS contains the name of the os 72 | EnvTargetOS = "CNB_TARGET_OS" 73 | 74 | // EnvTargetArch contains the architecture 75 | EnvTargetArch = "CNB_TARGET_ARCH" 76 | 77 | // EnvTargetOS contains the variant of the architecture 78 | EnvTargetArchVariant = "CNB_TARGET_ARCH_VARIANT" 79 | 80 | // EnvTargetDistroName contains the name of the ditro 81 | EnvTargetDistroName = "CNB_TARGET_DISTRO_NAME" 82 | 83 | // EnvTargetDistroVersion contains the version of the distro 84 | EnvTargetDistroVersion = "CNB_TARGET_DISTRO_VERSION" 85 | 86 | // DefaultPlatformBindingsLocation is the typical location for bindings, which exists under the platform directory 87 | // 88 | // Not guaranteed to exist, but often does. This should only be used as a fallback if EnvServiceBindings and EnvPlatformDirectory are not set 89 | DefaultPlatformBindingsLocation = "/platform/bindings" 90 | ) 91 | 92 | // Binding is a projection of metadata about an external entity to be bound to. 93 | type Binding struct { 94 | 95 | // Name is the name of the binding 96 | Name string 97 | 98 | // Path is the path to the binding directory. 99 | Path string 100 | 101 | // Type is the type of the binding. 102 | Type string 103 | 104 | // Provider is the optional provider of the binding. 105 | Provider string 106 | 107 | // Secret is the secret of the binding. 108 | Secret map[string]string 109 | } 110 | 111 | // NewBinding creates a new Binding initialized with a secret. 112 | func NewBinding(name string, path string, secret map[string]string) Binding { 113 | b := Binding{ 114 | Name: name, 115 | Path: path, 116 | Secret: make(map[string]string), 117 | } 118 | 119 | for k, v := range secret { 120 | switch k { 121 | case BindingType: 122 | b.Type = strings.TrimSpace(v) 123 | case BindingProvider: 124 | b.Provider = strings.TrimSpace(v) 125 | default: 126 | b.Secret[k] = strings.TrimSpace(v) 127 | } 128 | } 129 | 130 | return b 131 | } 132 | 133 | // NewBindingFromPath creates a new binding from the files located at a path. 134 | func NewBindingFromPath(path string) (Binding, error) { 135 | secret, err := internal.NewConfigMapFromPath(path) 136 | if err != nil { 137 | return Binding{}, fmt.Errorf("unable to create new config map from %s\n%w", path, err) 138 | } 139 | 140 | return NewBinding(filepath.Base(path), path, secret), nil 141 | } 142 | 143 | func (b Binding) String() string { 144 | var s []string 145 | for k := range b.Secret { 146 | s = append(s, k) 147 | } 148 | sort.Strings(s) 149 | 150 | return fmt.Sprintf("{Name: %s Path: %s Type: %s Provider: %s Secret: %s}", 151 | b.Name, b.Path, b.Type, b.Provider, s) 152 | } 153 | 154 | // SecretFilePath return the path to a secret file with the given name. 155 | func (b Binding) SecretFilePath(name string) (string, bool) { 156 | if _, ok := b.Secret[name]; !ok { 157 | return "", false 158 | } 159 | 160 | return filepath.Join(b.Path, name), true 161 | } 162 | 163 | // Bindings is a collection of bindings keyed by their name. 164 | type Bindings []Binding 165 | 166 | // NewBindingsFromPath creates a new instance from all the bindings at a given path. 167 | func NewBindingsFromPath(path string) (Bindings, error) { 168 | files, err := os.ReadDir(path) 169 | if err != nil && errors.Is(err, fs.ErrNotExist) { 170 | return Bindings{}, nil 171 | } else if err != nil { 172 | return nil, fmt.Errorf("unable to list directory %s\n%w", path, err) 173 | } 174 | 175 | bindings := Bindings{} 176 | for _, file := range files { 177 | bindingPath := filepath.Join(path, file.Name()) 178 | 179 | if strings.HasPrefix(filepath.Base(bindingPath), ".") { 180 | // ignore hidden files 181 | continue 182 | } 183 | binding, err := NewBindingFromPath(bindingPath) 184 | if err != nil { 185 | return nil, fmt.Errorf("unable to create new binding from %s\n%w", file, err) 186 | } 187 | 188 | bindings = append(bindings, binding) 189 | } 190 | 191 | return bindings, nil 192 | } 193 | 194 | type vcapServicesBinding struct { 195 | Name string `json:"name"` 196 | Label string `json:"label"` 197 | Credentials map[string]interface{} `json:"credentials"` 198 | } 199 | 200 | func toJSONString(input interface{}) (string, error) { 201 | switch in := input.(type) { 202 | case string: 203 | return in, nil 204 | default: 205 | jsonProperty, err := json.Marshal(in) 206 | if err != nil { 207 | return "", err 208 | } 209 | return string(jsonProperty), nil 210 | } 211 | } 212 | 213 | // NewBindingsFromVcapServicesEnv creates a new instance from all the bindings given from the VCAP_SERVICES. 214 | func NewBindingsFromVcapServicesEnv(content string) (Bindings, error) { 215 | var contentTyped map[string][]vcapServicesBinding 216 | 217 | err := json.Unmarshal([]byte(content), &contentTyped) 218 | if err != nil { 219 | return Bindings{}, err 220 | } 221 | 222 | bindings := Bindings{} 223 | for p, bArray := range contentTyped { 224 | for _, b := range bArray { 225 | secret := map[string]string{} 226 | for k, v := range b.Credentials { 227 | secret[k], err = toJSONString(v) 228 | if err != nil { 229 | return nil, err 230 | } 231 | } 232 | bindings = append(bindings, Binding{ 233 | Name: b.Name, 234 | Type: b.Label, 235 | Provider: p, 236 | Secret: secret, 237 | }) 238 | } 239 | } 240 | 241 | return bindings, nil 242 | } 243 | 244 | // NewBindings creates a new bindings from all the bindings at the path defined by $SERVICE_BINDING_ROOT. 245 | // If that isn't defined, bindings are read from /bindings. 246 | // If that isn't defined, bindings are read from $VCAP_SERVICES. 247 | // If that isn't defined, the specified platform path will be used 248 | func NewBindings(platformDir string) (Bindings, error) { 249 | if path, ok := os.LookupEnv(EnvServiceBindings); ok { 250 | return NewBindingsFromPath(path) 251 | } 252 | 253 | if path, ok := os.LookupEnv(EnvPlatformDirectory); ok { 254 | return NewBindingsFromPath(filepath.Join(path, "bindings")) 255 | } 256 | 257 | if content, ok := os.LookupEnv(EnvVcapServices); ok { 258 | return NewBindingsFromVcapServicesEnv(content) 259 | } 260 | 261 | return NewBindingsFromPath(filepath.Join(platformDir, "bindings")) 262 | } 263 | 264 | // Platform is the contents of the platform directory. 265 | type Platform struct { 266 | 267 | // Bindings are the external bindings available to the application. 268 | Bindings Bindings 269 | 270 | // Environment is the environment exposed by the platform. 271 | Environment map[string]string 272 | 273 | // Path is the path to the platform. 274 | Path string 275 | } 276 | -------------------------------------------------------------------------------- /platform_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb_test 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | . "github.com/onsi/gomega" 26 | "github.com/sclevine/spec" 27 | 28 | "github.com/harshfelony/libcnb/v2" 29 | ) 30 | 31 | func testPlatform(t *testing.T, context spec.G, it spec.S) { 32 | var ( 33 | Expect = NewWithT(t).Expect 34 | 35 | path, platformPath string 36 | ) 37 | 38 | it.Before(func() { 39 | var err error 40 | platformPath, err = os.MkdirTemp("", "platform") 41 | path = filepath.Join(platformPath, "bindings") 42 | Expect(err).NotTo(HaveOccurred()) 43 | }) 44 | 45 | it.After(func() { 46 | Expect(os.RemoveAll(path)).To(Succeed()) 47 | }) 48 | 49 | context("Cloudfoundry VCAP_SERVICES", func() { 50 | it("creates a bindings from VCAP_SERVICES", func() { 51 | content, err := os.ReadFile("testdata/vcap_services.json") 52 | Expect(err).NotTo(HaveOccurred()) 53 | t.Setenv(libcnb.EnvVcapServices, string(content)) 54 | 55 | bindings, err := libcnb.NewBindings("") 56 | Expect(err).NotTo(HaveOccurred()) 57 | 58 | Expect(bindings).To(ConsistOf(libcnb.Bindings{ 59 | { 60 | Name: "elephantsql-binding-c6c60", 61 | Type: "elephantsql-type", 62 | Provider: "elephantsql-provider", 63 | Secret: map[string]string{ 64 | "bool": "true", 65 | "int": "1", 66 | "uri": "postgres://exampleuser:examplepass@postgres.example.com:5432/exampleuser", 67 | }, 68 | }, 69 | { 70 | Name: "mysendgrid", 71 | Type: "sendgrid-type", 72 | Provider: "sendgrid-provider", 73 | Secret: map[string]string{ 74 | "username": "QvsXMbJ3rK", 75 | "password": "HCHMOYluTv", 76 | "hostname": "smtp.example.com", 77 | }, 78 | }, 79 | { 80 | Name: "postgres", 81 | Type: "postgres", 82 | Provider: "postgres", 83 | Secret: map[string]string{ 84 | "urls": "{\"example\":\"http://example.com\"}", 85 | "username": "foo", 86 | "password": "bar", 87 | }, 88 | }, 89 | })) 90 | }) 91 | 92 | it("creates empty bindings from empty VCAP_SERVICES", func() { 93 | t.Setenv(libcnb.EnvVcapServices, "{}") 94 | 95 | bindings, err := libcnb.NewBindings("") 96 | Expect(err).NotTo(HaveOccurred()) 97 | 98 | Expect(bindings).To(HaveLen(0)) 99 | }) 100 | }) 101 | 102 | context("Kubernetes Service Bindings", func() { 103 | it.Before(func() { 104 | Expect(os.MkdirAll(filepath.Join(path, "alpha"), 0755)).To(Succeed()) 105 | Expect(os.WriteFile(filepath.Join(path, "alpha", "type"), []byte("test-type"), 0600)).To(Succeed()) 106 | Expect(os.WriteFile(filepath.Join(path, "alpha", "provider"), []byte("test-provider"), 0600)).To(Succeed()) 107 | Expect(os.WriteFile(filepath.Join(path, "alpha", "test-secret-key"), []byte("test-secret-value"), 0600)).To(Succeed()) 108 | 109 | Expect(os.MkdirAll(filepath.Join(path, "bravo"), 0755)).To(Succeed()) 110 | Expect(os.WriteFile(filepath.Join(path, "bravo", "type"), []byte("test-type"), 0600)).To(Succeed()) 111 | Expect(os.WriteFile(filepath.Join(path, "bravo", "provider"), []byte("test-provider"), 0600)).To(Succeed()) 112 | Expect(os.WriteFile(filepath.Join(path, "bravo", "test-secret-key"), []byte("test-secret-value"), 0600)).To(Succeed()) 113 | 114 | Expect(os.MkdirAll(filepath.Join(path, ".hidden", "metadata"), 0755)).To(Succeed()) 115 | Expect(os.WriteFile(filepath.Join(path, ".hidden", "metadata", "kind"), []byte("test-kind"), 0600)).To(Succeed()) 116 | Expect(os.WriteFile(filepath.Join(path, ".hiddenFile"), []byte("test-kind"), 0600)).To(Succeed()) 117 | }) 118 | 119 | context("Binding", func() { 120 | it("creates an empty binding", func() { 121 | Expect(libcnb.NewBinding("test-name", "test-path", map[string]string{ 122 | libcnb.BindingType: "test-type", 123 | libcnb.BindingProvider: "test-provider", 124 | "test-key": "test-value", 125 | })).To(Equal(libcnb.Binding{ 126 | Name: "test-name", 127 | Path: "test-path", 128 | Type: "test-type", 129 | Provider: "test-provider", 130 | Secret: map[string]string{ 131 | "test-key": "test-value", 132 | }, 133 | })) 134 | }) 135 | 136 | it("creates a binding from a path", func() { 137 | path := filepath.Join(path, "alpha") 138 | 139 | binding, err := libcnb.NewBindingFromPath(path) 140 | Expect(binding, err).To(Equal(libcnb.Binding{ 141 | Name: filepath.Base(path), 142 | Path: path, 143 | Type: "test-type", 144 | Provider: "test-provider", 145 | Secret: map[string]string{"test-secret-key": "test-secret-value"}, 146 | })) 147 | 148 | secretFilePath, ok := binding.SecretFilePath("test-secret-key") 149 | Expect(ok).To(BeTrue()) 150 | Expect(secretFilePath).To(Equal(filepath.Join(path, "test-secret-key"))) 151 | }) 152 | 153 | it("sanitizes secrets", func() { 154 | path := filepath.Join(path, "alpha") 155 | 156 | b, err := libcnb.NewBindingFromPath(path) 157 | Expect(err).NotTo(HaveOccurred()) 158 | 159 | Expect(b.String()).To(Equal(fmt.Sprintf("{Name: alpha Path: %s Type: test-type Provider: test-provider Secret: [test-secret-key]}", path))) 160 | }) 161 | }) 162 | 163 | context("Bindings", func() { 164 | it("creates a bindings from a path", func() { 165 | Expect(libcnb.NewBindingsFromPath(path)).To(Equal(libcnb.Bindings{ 166 | libcnb.Binding{ 167 | Name: "alpha", 168 | Path: filepath.Join(path, "alpha"), 169 | Type: "test-type", 170 | Provider: "test-provider", 171 | Secret: map[string]string{"test-secret-key": "test-secret-value"}, 172 | }, 173 | libcnb.Binding{ 174 | Name: "bravo", 175 | Path: filepath.Join(path, "bravo"), 176 | Type: "test-type", 177 | Provider: "test-provider", 178 | Secret: map[string]string{"test-secret-key": "test-secret-value"}, 179 | }, 180 | })) 181 | }) 182 | 183 | it("creates an empty binding if path does not exist", func() { 184 | Expect(libcnb.NewBindingsFromPath("/path/doesnt/exist")).To(Equal(libcnb.Bindings{})) 185 | }) 186 | 187 | it("returns empty bindings if SERVICE_BINDING_ROOT and CNB_PLATFORM_DIR are not set and /platform/bindings does not exist", func() { 188 | Expect(libcnb.NewBindings(libcnb.DefaultPlatformBindingsLocation)).To(Equal(libcnb.Bindings{})) 189 | }) 190 | 191 | context("from environment", func() { 192 | it.After(func() { 193 | Expect(os.Unsetenv(libcnb.EnvServiceBindings)) 194 | Expect(os.Unsetenv("CNB_PLATFORM_DIR")) 195 | }) 196 | 197 | it("creates bindings from path in SERVICE_BINDING_ROOT if both set", func() { 198 | Expect(os.Setenv(libcnb.EnvServiceBindings, path)) 199 | Expect(os.Setenv("CNB_PLATFORM_DIR", "/does/not/exist")) 200 | 201 | Expect(libcnb.NewBindings(libcnb.DefaultPlatformBindingsLocation)).To(Equal(libcnb.Bindings{ 202 | libcnb.Binding{ 203 | Name: "alpha", 204 | Path: filepath.Join(path, "alpha"), 205 | Type: "test-type", 206 | Provider: "test-provider", 207 | Secret: map[string]string{"test-secret-key": "test-secret-value"}, 208 | }, 209 | libcnb.Binding{ 210 | Name: "bravo", 211 | Path: filepath.Join(path, "bravo"), 212 | Type: "test-type", 213 | Provider: "test-provider", 214 | Secret: map[string]string{"test-secret-key": "test-secret-value"}, 215 | }, 216 | })) 217 | }) 218 | 219 | it("creates bindings from path in SERVICE_BINDING_ROOT if SERVICE_BINDING_ROOT not set", func() { 220 | Expect(os.Setenv("CNB_PLATFORM_DIR", filepath.Dir(path))) 221 | 222 | Expect(libcnb.NewBindings(libcnb.DefaultPlatformBindingsLocation)).To(Equal(libcnb.Bindings{ 223 | libcnb.Binding{ 224 | Name: "alpha", 225 | Path: filepath.Join(path, "alpha"), 226 | Type: "test-type", 227 | Provider: "test-provider", 228 | Secret: map[string]string{"test-secret-key": "test-secret-value"}, 229 | }, 230 | libcnb.Binding{ 231 | Name: "bravo", 232 | Path: filepath.Join(path, "bravo"), 233 | Type: "test-type", 234 | Provider: "test-provider", 235 | Secret: map[string]string{"test-secret-key": "test-secret-value"}, 236 | }, 237 | })) 238 | }) 239 | }) 240 | }) 241 | }) 242 | } 243 | -------------------------------------------------------------------------------- /process.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | // Process represents metadata about a type of command that can be run. 20 | type Process struct { 21 | // Type is the type of the process. 22 | Type string `toml:"type"` 23 | 24 | // Command is the command of the process. 25 | Command []string `toml:"command"` 26 | 27 | // Arguments are arguments to the command. 28 | Arguments []string `toml:"args"` 29 | 30 | // WorkingDirectory is a directory to execute the command in, removes the need to use a shell environment to CD into working directory 31 | WorkingDirectory string `toml:"working-dir,omitempty"` 32 | 33 | // Default can be set to true to indicate that the process 34 | // type being defined should be the default process type for the app image. 35 | Default bool `toml:"default,omitempty"` 36 | } 37 | -------------------------------------------------------------------------------- /slice.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | // Slice represents metadata about a slice. 20 | type Slice struct { 21 | 22 | // Paths are the contents of the slice. 23 | Paths []string `toml:"paths"` 24 | } 25 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package libcnb 18 | 19 | // Store represents the contents of store.toml 20 | type Store struct { 21 | 22 | // Metadata represents the persistent metadata. 23 | Metadata map[string]interface{} `toml:"metadata"` 24 | } 25 | -------------------------------------------------------------------------------- /testdata/vcap_services.json: -------------------------------------------------------------------------------- 1 | { 2 | "elephantsql-provider": [ 3 | { 4 | "name": "elephantsql-binding-c6c60", 5 | "binding_guid": "44ceb72f-100b-4f50-87a2-7809c8b42b8d", 6 | "binding_name": "elephantsql-binding-c6c60", 7 | "instance_guid": "391308e8-8586-4c42-b464-c7831aa2ad22", 8 | "instance_name": "elephantsql-c6c60", 9 | "label": "elephantsql-type", 10 | "tags": [ 11 | "postgres", 12 | "postgresql", 13 | "relational" 14 | ], 15 | "plan": "turtle", 16 | "credentials": { 17 | "uri": "postgres://exampleuser:examplepass@postgres.example.com:5432/exampleuser", 18 | "int": 1, 19 | "bool": true 20 | }, 21 | "syslog_drain_url": null, 22 | "volume_mounts": [] 23 | } 24 | ], 25 | "sendgrid-provider": [ 26 | { 27 | "name": "mysendgrid", 28 | "binding_guid": "6533b1b6-7916-488d-b286-ca33d3fa0081", 29 | "binding_name": null, 30 | "instance_guid": "8c907d0f-ec0f-44e4-87cf-e23c9ba3925d", 31 | "instance_name": "mysendgrid", 32 | "label": "sendgrid-type", 33 | "tags": [ 34 | "smtp" 35 | ], 36 | "plan": "free", 37 | "credentials": { 38 | "hostname": "smtp.example.com", 39 | "username": "QvsXMbJ3rK", 40 | "password": "HCHMOYluTv" 41 | }, 42 | "syslog_drain_url": null, 43 | "volume_mounts": [] 44 | } 45 | ], 46 | "postgres": [ 47 | { 48 | "name": "postgres", 49 | "label": "postgres", 50 | "plan": "default", 51 | "tags": [ 52 | "postgres" 53 | ], 54 | "binding_guid": "6533b1b6-7916-488d-b286-ca33d3fa0081", 55 | "binding_name": null, 56 | "instance_guid": "8c907d0f-ec0f-44e4-87cf-e23c9ba3925d", 57 | "credentials": { 58 | "username": "foo", 59 | "password": "bar", 60 | "urls": { 61 | "example": "http://example.com" 62 | } 63 | }, 64 | "syslog_drain_url": null, 65 | "volume_mounts": [] 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/harshfelony/libcnb/tools/v2 2 | 3 | go 1.22.1 4 | 5 | toolchain go1.23.2 6 | 7 | require golang.org/x/tools v0.26.0 8 | 9 | require github.com/golangci/golangci-lint v1.61.0 10 | 11 | require ( 12 | 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 13 | 4d63.com/gochecknoglobals v0.2.1 // indirect 14 | github.com/4meepo/tagalign v1.3.4 // indirect 15 | github.com/Abirdcfly/dupword v0.1.1 // indirect 16 | github.com/Antonboom/errname v0.1.13 // indirect 17 | github.com/Antonboom/nilnil v0.1.9 // indirect 18 | github.com/Antonboom/testifylint v1.4.3 // indirect 19 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 20 | github.com/Crocmagnon/fatcontext v0.5.2 // indirect 21 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 22 | github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect 23 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 24 | github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect 25 | github.com/alecthomas/go-check-sumtype v0.1.4 // indirect 26 | github.com/alexkohler/nakedret/v2 v2.0.4 // indirect 27 | github.com/alexkohler/prealloc v1.0.0 // indirect 28 | github.com/alingse/asasalint v0.0.11 // indirect 29 | github.com/ashanbrown/forbidigo v1.6.0 // indirect 30 | github.com/ashanbrown/makezero v1.1.1 // indirect 31 | github.com/beorn7/perks v1.0.1 // indirect 32 | github.com/bkielbasa/cyclop v1.2.1 // indirect 33 | github.com/blizzy78/varnamelen v0.8.0 // indirect 34 | github.com/bombsimon/wsl/v4 v4.4.1 // indirect 35 | github.com/breml/bidichk v0.2.7 // indirect 36 | github.com/breml/errchkjson v0.3.6 // indirect 37 | github.com/butuzov/ireturn v0.3.0 // indirect 38 | github.com/butuzov/mirror v1.2.0 // indirect 39 | github.com/catenacyber/perfsprint v0.7.1 // indirect 40 | github.com/ccojocar/zxcvbn-go v1.0.2 // indirect 41 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 42 | github.com/charithe/durationcheck v0.0.10 // indirect 43 | github.com/chavacava/garif v0.1.0 // indirect 44 | github.com/ckaznocha/intrange v0.2.0 // indirect 45 | github.com/curioswitch/go-reassign v0.2.0 // indirect 46 | github.com/daixiang0/gci v0.13.5 // indirect 47 | github.com/davecgh/go-spew v1.1.1 // indirect 48 | github.com/denis-tingaikin/go-header v0.5.0 // indirect 49 | github.com/ettle/strcase v0.2.0 // indirect 50 | github.com/fatih/color v1.17.0 // indirect 51 | github.com/fatih/structtag v1.2.0 // indirect 52 | github.com/firefart/nonamedreturns v1.0.5 // indirect 53 | github.com/fsnotify/fsnotify v1.5.4 // indirect 54 | github.com/fzipp/gocyclo v0.6.0 // indirect 55 | github.com/ghostiam/protogetter v0.3.6 // indirect 56 | github.com/go-critic/go-critic v0.11.4 // indirect 57 | github.com/go-toolsmith/astcast v1.1.0 // indirect 58 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 59 | github.com/go-toolsmith/astequal v1.2.0 // indirect 60 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 61 | github.com/go-toolsmith/astp v1.1.0 // indirect 62 | github.com/go-toolsmith/strparse v1.1.0 // indirect 63 | github.com/go-toolsmith/typep v1.1.0 // indirect 64 | github.com/go-viper/mapstructure/v2 v2.1.0 // indirect 65 | github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect 66 | github.com/gobwas/glob v0.2.3 // indirect 67 | github.com/gofrs/flock v0.12.1 // indirect 68 | github.com/golang/protobuf v1.5.3 // indirect 69 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect 70 | github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 // indirect 71 | github.com/golangci/misspell v0.6.0 // indirect 72 | github.com/golangci/modinfo v0.3.4 // indirect 73 | github.com/golangci/plugin-module-register v0.1.1 // indirect 74 | github.com/golangci/revgrep v0.5.3 // indirect 75 | github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect 76 | github.com/google/go-cmp v0.6.0 // indirect 77 | github.com/gordonklaus/ineffassign v0.1.0 // indirect 78 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 79 | github.com/gostaticanalysis/comment v1.4.2 // indirect 80 | github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect 81 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 82 | github.com/hashicorp/go-version v1.7.0 // indirect 83 | github.com/hashicorp/hcl v1.0.0 // indirect 84 | github.com/hexops/gotextdiff v1.0.3 // indirect 85 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 86 | github.com/jgautheron/goconst v1.7.1 // indirect 87 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 88 | github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect 89 | github.com/jjti/go-spancheck v0.6.2 // indirect 90 | github.com/julz/importas v0.1.0 // indirect 91 | github.com/karamaru-alpha/copyloopvar v1.1.0 // indirect 92 | github.com/kisielk/errcheck v1.7.0 // indirect 93 | github.com/kkHAIKE/contextcheck v1.1.5 // indirect 94 | github.com/kulti/thelper v0.6.3 // indirect 95 | github.com/kunwardeep/paralleltest v1.0.10 // indirect 96 | github.com/kyoh86/exportloopref v0.1.11 // indirect 97 | github.com/lasiar/canonicalheader v1.1.1 // indirect 98 | github.com/ldez/gomoddirectives v0.2.4 // indirect 99 | github.com/ldez/tagliatelle v0.5.0 // indirect 100 | github.com/leonklingele/grouper v1.1.2 // indirect 101 | github.com/lufeee/execinquery v1.2.1 // indirect 102 | github.com/macabu/inamedparam v0.1.3 // indirect 103 | github.com/magiconair/properties v1.8.6 // indirect 104 | github.com/maratori/testableexamples v1.0.0 // indirect 105 | github.com/maratori/testpackage v1.1.1 // indirect 106 | github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect 107 | github.com/mattn/go-colorable v0.1.13 // indirect 108 | github.com/mattn/go-isatty v0.0.20 // indirect 109 | github.com/mattn/go-runewidth v0.0.9 // indirect 110 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 111 | github.com/mgechev/revive v1.3.9 // indirect 112 | github.com/mitchellh/go-homedir v1.1.0 // indirect 113 | github.com/mitchellh/mapstructure v1.5.0 // indirect 114 | github.com/moricho/tparallel v0.3.2 // indirect 115 | github.com/nakabonne/nestif v0.3.1 // indirect 116 | github.com/nishanths/exhaustive v0.12.0 // indirect 117 | github.com/nishanths/predeclared v0.2.2 // indirect 118 | github.com/nunnatsa/ginkgolinter v0.16.2 // indirect 119 | github.com/olekukonko/tablewriter v0.0.5 // indirect 120 | github.com/pelletier/go-toml v1.9.5 // indirect 121 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 122 | github.com/pmezard/go-difflib v1.0.0 // indirect 123 | github.com/polyfloyd/go-errorlint v1.6.0 // indirect 124 | github.com/prometheus/client_golang v1.12.1 // indirect 125 | github.com/prometheus/client_model v0.2.0 // indirect 126 | github.com/prometheus/common v0.32.1 // indirect 127 | github.com/prometheus/procfs v0.7.3 // indirect 128 | github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect 129 | github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect 130 | github.com/quasilyte/gogrep v0.5.0 // indirect 131 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 132 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 133 | github.com/ryancurrah/gomodguard v1.3.5 // indirect 134 | github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect 135 | github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect 136 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect 137 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 138 | github.com/sashamelentyev/usestdlibvars v1.27.0 // indirect 139 | github.com/securego/gosec/v2 v2.21.2 // indirect 140 | github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect 141 | github.com/sirupsen/logrus v1.9.3 // indirect 142 | github.com/sivchari/containedctx v1.0.3 // indirect 143 | github.com/sivchari/tenv v1.10.0 // indirect 144 | github.com/sonatard/noctx v0.0.2 // indirect 145 | github.com/sourcegraph/go-diff v0.7.0 // indirect 146 | github.com/spf13/afero v1.11.0 // indirect 147 | github.com/spf13/cast v1.5.0 // indirect 148 | github.com/spf13/cobra v1.8.1 // indirect 149 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 150 | github.com/spf13/pflag v1.0.5 // indirect 151 | github.com/spf13/viper v1.12.0 // indirect 152 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 153 | github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect 154 | github.com/stretchr/objx v0.5.2 // indirect 155 | github.com/stretchr/testify v1.9.0 // indirect 156 | github.com/subosito/gotenv v1.4.1 // indirect 157 | github.com/tdakkota/asciicheck v0.2.0 // indirect 158 | github.com/tetafro/godot v1.4.17 // indirect 159 | github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect 160 | github.com/timonwong/loggercheck v0.9.4 // indirect 161 | github.com/tomarrell/wrapcheck/v2 v2.9.0 // indirect 162 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 163 | github.com/ultraware/funlen v0.1.0 // indirect 164 | github.com/ultraware/whitespace v0.1.1 // indirect 165 | github.com/uudashr/gocognit v1.1.3 // indirect 166 | github.com/xen0n/gosmopolitan v1.2.2 // indirect 167 | github.com/yagipy/maintidx v1.0.0 // indirect 168 | github.com/yeya24/promlinter v0.3.0 // indirect 169 | github.com/ykadowak/zerologlint v0.1.5 // indirect 170 | gitlab.com/bosi/decorder v0.4.2 // indirect 171 | go-simpler.org/musttag v0.12.2 // indirect 172 | go-simpler.org/sloglint v0.7.2 // indirect 173 | go.uber.org/atomic v1.7.0 // indirect 174 | go.uber.org/automaxprocs v1.5.3 // indirect 175 | go.uber.org/multierr v1.6.0 // indirect 176 | go.uber.org/zap v1.24.0 // indirect 177 | golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect 178 | golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect 179 | golang.org/x/mod v0.21.0 // indirect 180 | golang.org/x/sync v0.8.0 // indirect 181 | golang.org/x/sys v0.26.0 // indirect 182 | golang.org/x/text v0.18.0 // indirect 183 | google.golang.org/protobuf v1.34.2 // indirect 184 | gopkg.in/ini.v1 v1.67.0 // indirect 185 | gopkg.in/yaml.v2 v2.4.0 // indirect 186 | gopkg.in/yaml.v3 v3.0.1 // indirect 187 | honnef.co/go/tools v0.5.1 // indirect 188 | mvdan.cc/gofumpt v0.7.0 // indirect 189 | mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect 190 | ) 191 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 8 | _ "golang.org/x/tools/cmd/goimports" 9 | ) 10 | --------------------------------------------------------------------------------