forked from paketo-buildpacks/packit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
service.go
206 lines (173 loc) · 6.15 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
package postal
import (
"fmt"
"io"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/paketo-buildpacks/packit"
"github.com/paketo-buildpacks/packit/cargo"
"github.com/paketo-buildpacks/packit/postal/internal"
"github.com/paketo-buildpacks/packit/vacation"
)
//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)
}
//go:generate faux --interface MappingResolver --output fakes/mapping_resolver.go
// MappingResolver serves as the interface that looks up platform binding provided
// dependency mappings given a SHA256 and a path to search for bindings
type MappingResolver interface {
FindDependencyMapping(SHA256, bindingPath string) (string, error)
}
// Service provides a mechanism for resolving and installing dependencies given
// a Transport.
type Service struct {
transport Transport
mappingResolver MappingResolver
}
// NewService creates an instance of a Servicel given a Transport.
func NewService(transport Transport) Service {
return Service{
transport: transport,
mappingResolver: internal.NewDependencyMappingResolver(),
}
}
func (s Service) WithDependencyMappingResolver(mappingResolver MappingResolver) Service {
s.mappingResolver = mappingResolver
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{}, fmt.Errorf(
"failed to satisfy %q dependency version constraint %q: no compatible versions. Supported versions are: [%s]",
id,
version,
strings.Join(supportedVersions, ", "),
)
}
sort.Slice(compatibleVersions, func(i, j int) bool {
iVersion := semver.MustParse(compatibleVersions[i].Version)
jVersion := semver.MustParse(compatibleVersions[j].Version)
return iVersion.GreaterThan(jVersion)
})
return compatibleVersions[0], nil
}
// 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. 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 {
dependencyMappingURI, err := s.mappingResolver.FindDependencyMapping(dependency.SHA256, filepath.Join(platformPath, "bindings"))
if err != nil {
return fmt.Errorf("failure checking out the bindings")
}
if dependencyMappingURI != "" {
dependency.URI = dependencyMappingURI
}
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, dependency.SHA256)
err = vacation.NewArchive(validatedReader).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 fmt.Errorf("checksum does not match: %s", err)
}
return nil
}
// Install will invoke Deliver with a hardcoded value of /platform for the platform path.
//
// Deprecated: Use Deliver instead.
func (s Service) Install(dependency Dependency, cnbPath, layerPath string) error {
return s.Deliver(dependency, cnbPath, layerPath, "/platform")
}
// GenerateBillOfMaterials will generate a list of BOMEntry values given a
// collection of Dependency values.
func (s Service) GenerateBillOfMaterials(dependencies ...Dependency) []packit.BOMEntry {
var entries []packit.BOMEntry
for _, dependency := range dependencies {
entry := packit.BOMEntry{
Name: dependency.Name,
Metadata: map[string]interface{}{
"sha256": dependency.SHA256,
"stacks": dependency.Stacks,
"uri": dependency.URI,
"version": dependency.Version,
},
}
if (dependency.DeprecationDate != time.Time{}) {
entry.Metadata["deprecation-date"] = dependency.DeprecationDate
}
entries = append(entries, entry)
}
return entries
}