/
required_capability.go
210 lines (188 loc) · 6.66 KB
/
required_capability.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
package component
import (
"context"
"fmt"
v1beta12 "halkyon.io/api/capability/v1beta1"
"halkyon.io/api/component/v1beta1"
beta1 "halkyon.io/api/v1beta1"
framework "halkyon.io/operator-framework"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"strings"
)
type requiredCapability struct {
base
capabilityConfig v1beta1.RequiredCapabilityConfig
}
var _ framework.DependentResource = &requiredCapability{}
var capabilityGVK = v1beta12.SchemeGroupVersion.WithKind(v1beta12.Kind)
func newRequiredCapability(owner *v1beta1.Component, capConfig v1beta1.RequiredCapabilityConfig) requiredCapability {
config := framework.NewConfig(capabilityGVK)
config.CheckedForReadiness = true
config.Created = false
config.Updated = true
config.TypeName = "Required Capability"
c := requiredCapability{base: newConfiguredBaseDependent(owner, config), capabilityConfig: capConfig}
c.NameFn = c.Name
return c
}
func (res requiredCapability) Build(empty bool) (runtime.Object, error) {
if empty {
return &v1beta12.Capability{}, nil
}
// we don't want to be building anything: the capability is under halkyon's control, it's not up to the component to create it
return nil, nil
}
func (res requiredCapability) Update(toUpdate runtime.Object) (bool, runtime.Object, error) {
c := toUpdate.(*v1beta12.Capability)
return res.updateIfNeeded(c)
}
func (res requiredCapability) updateIfNeeded(c *v1beta12.Capability) (bool, runtime.Object, error) {
updated := false
// examine all given parameters that starts by `halkyon` as these denote parameters to pass to the underlying plugin
// as opposed to parameters used to match a capability
wanted := res.capabilityConfig.Spec.Parameters
for _, parameter := range wanted {
name := parameter.Name
if strings.HasPrefix(name, "halkyon.") {
value := parameter.Value
// try to see if that parameter was already set for that capability
found := false
for i, pair := range c.Spec.Parameters {
if pair.Name == name {
if pair.Value != value {
updated = true
c.Spec.Parameters[i] = beta1.NameValuePair{Name: name, Value: value}
}
found = true
break
}
}
// if we didn't find the parameter, add it
if !found {
updated = true
c.Spec.Parameters = append(c.Spec.Parameters, beta1.NameValuePair{Name: name, Value: value})
}
}
}
return updated, c, nil
}
func (res requiredCapability) Name() string {
return res.capabilityConfig.Name
}
func (res requiredCapability) NameFrom(underlying runtime.Object) string {
return underlying.(*v1beta12.Capability).Name
}
func (res requiredCapability) GetCondition(underlying runtime.Object, err error) *beta1.DependentCondition {
return framework.DefaultCustomizedGetConditionFor(res, err, underlying, func(underlying runtime.Object, cond *beta1.DependentCondition) {
c := underlying.(*v1beta12.Capability)
if c.Status.Reason != v1beta12.CapabilityReady {
cond.Type = beta1.DependentPending
}
cond.Message = c.Status.Message
})
}
func (res requiredCapability) Fetch() (runtime.Object, error) {
config := res.capabilityConfig
spec := config.Spec
selector := selectorFor(spec)
var result *v1beta12.Capability
component := res.ownerAsComponent()
// if the component defines a bound value, try to retrieve it and check that it conforms to requirements
if len(config.BoundTo) > 0 {
result = &v1beta12.Capability{}
_, err := framework.Helper.Fetch(config.BoundTo, component.Namespace, result)
if err != nil {
return nil, err
}
// if the referenced capability matches, return it
foundSpec := result.Spec
if matches(spec, foundSpec) {
updated, result, err := res.updateIfNeeded(result)
if err != nil {
return nil, err
}
if updated {
err := framework.Helper.Client.Update(context.Background(), result)
if err != nil {
return nil, err
}
}
return result, nil
}
return nil, fmt.Errorf("specified '%s' bound to capability doesn't match %v requirements, was: %v", config.BoundTo, selector, selectorFor(foundSpec))
}
// retrieve names of matching capabilities along with last (and hopefully, only) matching one
names, result, err := capabilitiesNameMatching(spec)
if err != nil {
return nil, err
}
// otherwise, check if we can auto-bind to an available capability
if config.AutoBindable {
if len(names) > 1 {
return nil, fmt.Errorf("cannot autobind because several capabilities match %v: '%s', use explicit binding instead", selector, strings.Join(names, ", "))
}
if result != nil {
requires := component.Spec.Capabilities.Requires
for i, require := range requires {
if require.Name == config.Name {
requires[i].BoundTo = result.Name
break
}
}
updated, result, err := res.updateIfNeeded(result)
if err != nil {
return nil, err
}
if updated {
err := framework.Helper.Client.Update(context.Background(), result)
if err != nil {
return nil, err
}
}
return result, nil
}
}
switch len(names) {
case 0:
err = fmt.Errorf("no capability matching '%v' was found", selector)
case 1:
err = fmt.Errorf("no capability bound, found one matching candidate: '%s'", result.Name)
default:
err = fmt.Errorf("no capability bound, several matching candidates were found: '%s'", strings.Join(names, ", "))
}
return nil, err
}
func selectorFor(spec v1beta12.CapabilitySpec) fields.Selector {
selector := fields.AndSelectors(fields.OneTermEqualSelector("spec.category", spec.Category.String()), fields.OneTermEqualSelector("spec.type", spec.Type.String()))
if len(spec.Version) > 0 {
selector = fields.AndSelectors(selector, fields.OneTermEqualSelector("spec.version", spec.Version))
}
return selector
}
func capabilitiesNameMatching(spec v1beta12.CapabilitySpec) (names []string, lastMatching *v1beta12.Capability, err error) {
matching := &v1beta12.CapabilityList{}
err = framework.Helper.Client.List(context.TODO(), &client.ListOptions{ /*FieldSelector: selector*/ }, matching)
if err != nil {
return nil, nil, err
}
capabilityNb := len(matching.Items)
names = make([]string, 0, capabilityNb)
for _, capability := range matching.Items {
if matches(spec, capability.Spec) {
names = append(names, capability.Name)
lastMatching = &capability
}
}
return names, lastMatching, nil
}
func matches(requested, actual v1beta12.CapabilitySpec) bool {
// first check that category and type match
if requested.Category.Equals(actual.Category) && requested.Type.Equals(actual.Type) {
// if we're asking for a specific version then we need to provide a capability with that version
// todo: implement range matching on version?
return len(requested.Version) == 0 || requested.Version == actual.Version
}
return false
}