/
require.go
263 lines (233 loc) · 8.58 KB
/
require.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
package modconfig
import (
"fmt"
"log/slog"
"sort"
"github.com/Masterminds/semver/v3"
"github.com/hashicorp/hcl/v2"
"github.com/turbot/pipe-fittings/app_specific"
"github.com/turbot/pipe-fittings/constants"
"github.com/turbot/pipe-fittings/hclhelpers"
"github.com/turbot/pipe-fittings/ociinstaller"
"github.com/turbot/pipe-fittings/schema"
"github.com/turbot/steampipe-plugin-sdk/v5/sperr"
)
// Require is a struct representing mod dependencies
type Require struct {
Plugins []*PluginVersion `hcl:"plugin,block"`
DeprecatedSteampipeVersionString string `hcl:"steampipe,optional"`
Steampipe *SteampipeRequire `hcl:"steampipe,block"`
Flowpipe *FlowpipeRequire `hcl:"flowpipe,block"`
Mods []*ModVersionConstraint `hcl:"mod,block"`
// map keyed by name [and alias]
modMap map[string]*ModVersionConstraint
// range of the require block body
DeclRange hcl.Range
// range of the require block type
TypeRange hcl.Range
}
func NewRequire() *Require {
return &Require{
modMap: make(map[string]*ModVersionConstraint),
}
}
func (r *Require) Clone() *Require {
require := NewRequire()
require.Steampipe = r.Steampipe
require.Plugins = r.Plugins
require.Mods = r.Mods
require.DeclRange = r.DeclRange
require.TypeRange = r.TypeRange
// we need to shallow copy the map
// if we don't, when the other one gets
// modified - this one gets as well
for k, mvc := range r.modMap {
require.modMap[k] = mvc
}
return require
}
func (r *Require) initialise(modBlock *hcl.Block) hcl.Diagnostics {
// This will actually be called twice - once when we load the mod definition,
// and again when we load the mod resources (and set the mod metadata, references etc)
// If we have already initialised, return (we can tell by checking the DeclRange)
if !r.DeclRange.Empty() {
return nil
}
var diags hcl.Diagnostics
// handle deprecated properties
moreDiags := r.handleDeprecations()
diags = append(diags, moreDiags...)
// find the require block
requireBlock := hclhelpers.FindFirstChildBlock(modBlock, schema.BlockTypeRequire)
if requireBlock == nil {
// was this the legacy 'requires' block?
requireBlock = hclhelpers.FindFirstChildBlock(modBlock, schema.BlockTypeLegacyRequires)
}
if requireBlock == nil {
// nothing else to populate
return nil
}
// set our Ranges
r.DeclRange = hclhelpers.BlockRange(requireBlock)
r.TypeRange = requireBlock.TypeRange
// build maps of plugin and mod blocks
pluginBlockMap := hclhelpers.BlocksToMap(hclhelpers.FindChildBlocks(requireBlock, schema.BlockTypePlugin))
modBlockMap := hclhelpers.BlocksToMap(hclhelpers.FindChildBlocks(requireBlock, schema.BlockTypeMod))
if r.Steampipe != nil {
moreDiags := r.Steampipe.initialise(requireBlock)
diags = append(diags, moreDiags...)
}
if r.Flowpipe != nil {
moreDiags := r.Flowpipe.initialise(requireBlock)
diags = append(diags, moreDiags...)
}
for _, p := range r.Plugins {
moreDiags := p.Initialise(pluginBlockMap[p.RawName])
diags = append(diags, moreDiags...)
}
for _, m := range r.Mods {
moreDiags := m.Initialise(modBlockMap[m.Name])
diags = append(diags, moreDiags...)
if !diags.HasErrors() {
// key map entry by name [and alias]
r.modMap[m.Name] = m
}
}
return diags
}
func (r *Require) handleDeprecations() hcl.Diagnostics {
var diags hcl.Diagnostics
// the 'steampipe' property is deprecated and replace with a steampipe block
if r.DeprecatedSteampipeVersionString != "" {
// if there is both a steampipe block and property, fail
if r.Steampipe != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Both 'steampipe' block and deprecated 'steampipe' property are set",
Subject: &r.DeclRange,
})
} else {
r.Steampipe = &SteampipeRequire{MinVersionString: r.DeprecatedSteampipeVersionString}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Property 'steampipe' is deprecated for mod require block - use a steampipe block instead",
Subject: &r.DeclRange,
},
)
}
}
return diags
}
func (r *Require) validateAppVersion(modName string) error {
if steampipeVersionConstraint := r.SteampipeVersionConstraint(); steampipeVersionConstraint != nil {
if !steampipeVersionConstraint.Check(app_specific.AppVersion) {
return fmt.Errorf("App version %s does not satisfy %s which requires version %s", app_specific.AppVersion.String(), modName, r.Steampipe.MinVersionString)
}
}
return nil
}
// validatePluginVersions validates that for every plugin requirement there's at least one plugin installed
func (r *Require) validatePluginVersions(modName string, plugins PluginVersionMap) []error {
if len(r.Plugins) == 0 {
return nil
}
// if this is a steampipe backend and there is no plugin map, it must be a pre-0.22 version which does not return plugin versions
if plugins.Backend == constants.SteampipeBackendName && plugins.AvailablePlugins == nil {
slog.Warn("Mod plugin requirements cannot be validated. Steampipe backend does not provide plugin version information. Upgrade Steampipe to enable plugin version validation.", "mod", modName)
return nil
}
var validationErrors []error
for _, requiredPlugin := range r.Plugins {
if err := r.searchInstalledPluginForRequirement(modName, requiredPlugin, plugins); err != nil {
validationErrors = append(validationErrors, err)
}
}
return validationErrors
}
// searchInstalledPluginForRequirement returns plugin validation errors if no plugin is found which satisfies
// the mod requirement. If plugin is found nil error is returned.
func (r *Require) searchInstalledPluginForRequirement(modName string, requirement *PluginVersion, plugins PluginVersionMap) error {
for installedName, installed := range plugins.AvailablePlugins {
org, name, _ := ociinstaller.NewSteampipeImageRef(installedName).GetOrgNameAndStream()
if org != requirement.Org || name != requirement.Name {
// no point checking - different plugin
continue
}
// if org and name matches but the plugin is built locally, return without any validation error
if installed.IsLocal() {
return nil
}
// if org and name matches, check whether the version constraint is satisfied
if requirement.Constraint.Check(installed.Semver()) {
// constraint is satisfied
return nil
}
}
// validation failed - return error
return sperr.New("%s backend '%s' does not provide a plugin which satisfies requirement '%s@%s' - required by '%s'", plugins.Backend, plugins.Database, requirement.RawName, requirement.MinVersionString, modName)
}
// AddModDependencies adds all the mod in newModVersions to our list of mods, using the following logic
// - if a mod with same name, [alias] and constraint exists, it is not added
// - if a mod with same name [and alias] and different constraint exist, it is replaced
func (r *Require) AddModDependencies(newModVersions map[string]*ModVersionConstraint) {
// rebuild the Mods array
// first rebuild the mod map
for name, newVersion := range newModVersions {
r.modMap[name] = newVersion
}
// now update the mod array from the map
var newMods = make([]*ModVersionConstraint, len(r.modMap))
idx := 0
for _, requiredVersion := range r.modMap {
newMods[idx] = requiredVersion
idx++
}
// sort by name
sort.Sort(ModVersionConstraintCollection(newMods))
// write back
r.Mods = newMods
}
func (r *Require) RemoveModDependencies(versions map[string]*ModVersionConstraint) {
// first rebuild the mod map
for name := range versions {
delete(r.modMap, name)
}
// now update the mod array from the map
var newMods = make([]*ModVersionConstraint, len(r.modMap))
idx := 0
for _, requiredVersion := range r.modMap {
newMods[idx] = requiredVersion
idx++
}
// sort by name
sort.Sort(ModVersionConstraintCollection(newMods))
// write back
r.Mods = newMods
}
func (r *Require) RemoveAllModDependencies() {
r.Mods = nil
}
func (r *Require) GetModDependency(name string /*,alias string*/) *ModVersionConstraint {
return r.modMap[name]
}
func (r *Require) ContainsMod(requiredModVersion *ModVersionConstraint) bool {
if c := r.GetModDependency(requiredModVersion.Name); c != nil {
return c.Equals(requiredModVersion)
}
return false
}
func (r *Require) Empty() bool {
return r.SteampipeVersionConstraint() == nil && len(r.Mods) == 0 && len(r.Plugins) == 0
}
func (r *Require) SteampipeVersionConstraint() *semver.Constraints {
if r.Steampipe == nil {
return nil
}
return r.Steampipe.Constraint
}
func (r *Require) FlowpipeVersionConstraint() *semver.Constraints {
if r.Flowpipe == nil {
return nil
}
return r.Flowpipe.Constraint
}