-
Notifications
You must be signed in to change notification settings - Fork 26
/
service.go
380 lines (317 loc) · 11 KB
/
service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
package postal
import (
"errors"
"fmt"
"io"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/paketo-buildpacks/packit/v2"
"github.com/paketo-buildpacks/packit/v2/cargo"
"github.com/paketo-buildpacks/packit/v2/postal/internal"
"github.com/paketo-buildpacks/packit/v2/servicebindings"
"github.com/paketo-buildpacks/packit/v2/vacation"
//nolint Ignore SA1019, usage of deprecated package within a deprecated test case
"github.com/paketo-buildpacks/packit/v2/paketosbom"
)
//go:generate faux --interface Transport --output fakes/transport.go
// Transport serves as the interface for types that can fetch dependencies
// given a location uri using either the http:// or file:// scheme.
type Transport interface {
Drop(root, uri string) (io.ReadCloser, error)
}
// MappingResolver serves as the interface that looks up platform binding provided
// dependency mappings given a SHA256
//
//go:generate faux --interface MappingResolver --output fakes/mapping_resolver.go
type MappingResolver interface {
FindDependencyMapping(checksum, platformDir string) (string, error)
}
// MirrorResolver serves as the interface that looks for a dependency mirror via
// environment variable or binding
//
//go:generate faux --interface MirrorResolver --output fakes/mirror_resolver.go
type MirrorResolver interface {
FindDependencyMirror(uri, platformDir string) (string, error)
}
// ErrNoDeps is a typed error indicating that no dependencies were resolved during Service.Resolve()
//
// errors can be tested against this type with: errors.As()
type ErrNoDeps struct {
id string
version string
stack string
supportedVersions []string
}
// Error implements the error.Error interface
func (e *ErrNoDeps) Error() string {
return fmt.Sprintf("failed to satisfy %q dependency version constraint %q: no compatible versions on %q stack. Supported versions are: [%s]",
e.id,
e.version,
e.stack,
strings.Join(e.supportedVersions, ", "),
)
}
// Service provides a mechanism for resolving and installing dependencies given
// a Transport.
type Service struct {
transport Transport
mappingResolver MappingResolver
mirrorResolver MirrorResolver
}
// NewService creates an instance of a Service given a Transport.
func NewService(transport Transport) Service {
return Service{
transport: transport,
mappingResolver: internal.NewDependencyMappingResolver(
servicebindings.NewResolver(),
),
mirrorResolver: internal.NewDependencyMirrorResolver(
servicebindings.NewResolver(),
),
}
}
func (s Service) WithDependencyMappingResolver(mappingResolver MappingResolver) Service {
s.mappingResolver = mappingResolver
return s
}
func (s Service) WithDependencyMirrorResolver(mirrorResolver MirrorResolver) Service {
s.mirrorResolver = mirrorResolver
return s
}
// Resolve will pick the best matching dependency given a path to a
// buildpack.toml file, and the id, version, and stack value of a dependency.
// The version value is treated as a SemVer constraint and will pick the
// version that matches that constraint best. If the version is given as
// "default", the default version for the dependency with the given id will be
// used. If there is no default version for that dependency, a wildcard
// constraint will be used.
func (s Service) Resolve(path, id, version, stack string) (Dependency, error) {
dependencies, defaultVersion, err := parseBuildpack(path, id)
if err != nil {
return Dependency{}, err
}
if version == "" {
version = "default"
}
if version == "default" {
version = "*"
if defaultVersion != "" {
version = defaultVersion
}
}
// Handle the pessmistic operator (~>)
var re = regexp.MustCompile(`~>`)
if re.MatchString(version) {
res := re.ReplaceAllString(version, "")
parts := strings.Split(res, ".")
// if the version contains a major, minor, and patch use "~" Tilde Range Comparison
// if the version contains a major and minor only, or a major version only use "^" Caret Range Comparison
if len(parts) == 3 {
version = "~" + res
} else {
version = "^" + res
}
}
var compatibleVersions []Dependency
versionConstraint, err := semver.NewConstraint(version)
if err != nil {
return Dependency{}, err
}
var supportedVersions []string
for _, dependency := range dependencies {
if dependency.ID != id || !stacksInclude(dependency.Stacks, stack) {
continue
}
sVersion, err := semver.NewVersion(dependency.Version)
if err != nil {
return Dependency{}, err
}
if versionConstraint.Check(sVersion) {
compatibleVersions = append(compatibleVersions, dependency)
}
supportedVersions = append(supportedVersions, dependency.Version)
}
if len(compatibleVersions) == 0 {
return Dependency{}, &ErrNoDeps{id, version, stack, supportedVersions}
}
stacksForVersion := map[string][]string{}
for _, dep := range compatibleVersions {
stacksForVersion[dep.Version] = append(stacksForVersion[dep.Version], dep.Stacks...)
}
for version, stacks := range stacksForVersion {
count := stringSliceElementCount(stacks, "*")
if count > 1 {
return Dependency{}, fmt.Errorf("multiple dependencies support wildcard stack for version: %q", version)
}
}
sort.Slice(compatibleVersions, func(i, j int) bool {
iDep := compatibleVersions[i]
jDep := compatibleVersions[j]
jVersion := semver.MustParse(jDep.Version)
iVersion := semver.MustParse(iDep.Version)
if !iVersion.Equal(jVersion) {
return iVersion.GreaterThan(jVersion)
}
iStacks := iDep.Stacks
jStacks := jDep.Stacks
// If either dependency supports the wildcard stack, it has lower
// priority than a dependency that only supports a more specific stack.
// This is true regardless of whether or not the dependency with
// wildcard stack support also supports other stacks
//
// If is an error to have multiple dependencies with the same version
// and wildcard stack support.
// This is tested for above, and we would not enter this sort function
// in this case
if stringSliceContains(iStacks, "*") {
return false
}
if stringSliceContains(jStacks, "*") {
return true
}
// As mentioned above, this isn't a valid path to encounter because
// only one dependency may have support for wildcard stacks for a given
// version. We could panic, but it is preferable to return an invalid
// sort order instead.
//
// This is untested as this path is not possible to encounter.
return true
})
return compatibleVersions[0], nil
}
func stringSliceContains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
func stringSliceElementCount(slice []string, str string) int {
count := 0
for _, s := range slice {
if s == str {
count++
}
}
return count
}
// Deliver will fetch and expand a dependency into a layer path location. The
// location of the CNBPath is given so that dependencies that may be included
// in a buildpack when packaged for offline consumption can be retrieved. If
// there is a dependency mapping for the specified dependency, Deliver will use
// the given dependency mapping URI to fetch the dependency. If there is a
// dependency mirror for the specified dependency, Deliver will use the mirror
// URI to fetch the dependency. If both a dependency mapping and mirror are BOTH
// present, the mapping will take precedence over the mirror.The dependency is
// validated against the checksum value provided on the Dependency and will error
// if there are inconsistencies in the fetched result.
func (s Service) Deliver(dependency Dependency, cnbPath, layerPath, platformPath string) error {
dependencyChecksum := dependency.Checksum
if dependency.SHA256 != "" {
dependencyChecksum = fmt.Sprintf("sha256:%s", dependency.SHA256)
}
dependencyMirrorURI, err := s.mirrorResolver.FindDependencyMirror(dependency.URI, platformPath)
if err != nil {
return fmt.Errorf("failure checking for dependency mirror: %s", err)
}
dependencyMappingURI, err := s.mappingResolver.FindDependencyMapping(dependencyChecksum, platformPath)
if err != nil {
return fmt.Errorf("failure checking for dependency mappings: %s", err)
}
if dependencyMappingURI != "" {
dependency.URI = dependencyMappingURI
} else if dependencyMirrorURI != "" {
dependency.URI = dependencyMirrorURI
}
bundle, err := s.transport.Drop(cnbPath, dependency.URI)
if err != nil {
return fmt.Errorf("failed to fetch dependency: %s", err)
}
defer bundle.Close()
validatedReader := cargo.NewValidatedReader(bundle, dependencyChecksum)
name := dependency.Name
if name == "" {
name = filepath.Base(dependency.URI)
}
err = vacation.NewArchive(validatedReader).WithName(name).StripComponents(dependency.StripComponents).Decompress(layerPath)
if err != nil {
return err
}
ok, err := validatedReader.Valid()
if err != nil {
return fmt.Errorf("failed to validate dependency: %s", err)
}
if !ok {
return errors.New("failed to validate dependency: checksum does not match")
}
return nil
}
// GenerateBillOfMaterials will generate a list of BOMEntry values given a
// collection of Dependency values.
//
// Deprecated: use sbom.GenerateFromDependency instead.
func (s Service) GenerateBillOfMaterials(dependencies ...Dependency) []packit.BOMEntry {
var entries []packit.BOMEntry
for _, dependency := range dependencies {
checksum := Checksum(dependency.SHA256)
if len(dependency.Checksum) > 0 {
checksum = Checksum(dependency.Checksum)
}
hash := checksum.Hash()
paketoSbomAlgorithm, err := paketosbom.GetBOMChecksumAlgorithm(checksum.Algorithm())
// GetBOMChecksumAlgorithm will set algorithm to UNKNOWN if there is an error
if err != nil || hash == "" {
paketoSbomAlgorithm = paketosbom.UNKNOWN
hash = ""
}
sourceChecksum := Checksum(dependency.SourceSHA256)
if len(dependency.Checksum) > 0 {
sourceChecksum = Checksum(dependency.SourceChecksum)
}
sourceHash := sourceChecksum.Hash()
paketoSbomSrcAlgorithm, err := paketosbom.GetBOMChecksumAlgorithm(sourceChecksum.Algorithm())
// GetBOMChecksumAlgorithm will set algorithm to UNKNOWN if there is an error
if err != nil || sourceHash == "" {
paketoSbomSrcAlgorithm = paketosbom.UNKNOWN
sourceHash = ""
}
paketoBomMetadata := paketosbom.BOMMetadata{
Checksum: paketosbom.BOMChecksum{
Algorithm: paketoSbomAlgorithm,
Hash: hash,
},
URI: dependency.URI,
Version: dependency.Version,
Source: paketosbom.BOMSource{
Checksum: paketosbom.BOMChecksum{
Algorithm: paketoSbomSrcAlgorithm,
Hash: sourceHash,
},
URI: dependency.Source,
},
}
if dependency.CPE != "" {
paketoBomMetadata.CPE = dependency.CPE
}
if (dependency.DeprecationDate != time.Time{}) {
paketoBomMetadata.DeprecationDate = dependency.DeprecationDate
}
if dependency.Licenses != nil {
paketoBomMetadata.Licenses = dependency.Licenses
}
if dependency.PURL != "" {
paketoBomMetadata.PURL = dependency.PURL
}
entry := packit.BOMEntry{
Name: dependency.Name,
Metadata: paketoBomMetadata,
}
entries = append(entries, entry)
}
return entries
}