/
resolver.go
246 lines (215 loc) · 8.19 KB
/
resolver.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
package orchestration
import (
"context"
"fmt"
"regexp"
"sync"
"time"
"github.com/kyma-project/control-plane/components/kyma-environment-broker/common/gardener"
"github.com/kyma-project/control-plane/components/kyma-environment-broker/common/runtime"
"github.com/sirupsen/logrus"
brokerapi "github.com/pivotal-cf/brokerapi/v8/domain"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
)
// RuntimeLister is the interface to get runtime objects from KEB
//
//go:generate mockery --name=RuntimeLister --output=. --outpkg=orchestration --case=underscore --structname RuntimeListerMock --filename runtime_lister_mock.go
type RuntimeLister interface {
ListAllRuntimes() ([]runtime.RuntimeDTO, error)
}
// GardenerRuntimeResolver is the default resolver which implements the RuntimeResolver interface.
// This resolver uses the Shoot resources on the Gardener cluster to resolve the runtime targets.
//
// Naive implementation, listing all the shoots and perfom filtering on the result.
// The logic could be optimized with k8s client cache using shoot lister / indexer.
// The implementation is thread safe, i.e. it is safe to call Resolve() from multiple threads concurrently.
type GardenerRuntimeResolver struct {
gardenerClient dynamic.Interface
gardenerNamespace string
runtimeLister RuntimeLister
runtimes map[string]runtime.RuntimeDTO
mutex sync.RWMutex
logger logrus.FieldLogger
}
const (
globalAccountLabel = "account"
subAccountLabel = "subaccount"
runtimeIDAnnotation = "kcp.provisioner.kyma-project.io/runtime-id"
maintenanceWindowFormat = "150405-0700"
)
// NewGardenerRuntimeResolver constructs a GardenerRuntimeResolver with the mandatory input parameters.
func NewGardenerRuntimeResolver(gardenerClient dynamic.Interface, gardenerNamespace string, lister RuntimeLister, logger logrus.FieldLogger) *GardenerRuntimeResolver {
return &GardenerRuntimeResolver{
gardenerClient: gardenerClient,
gardenerNamespace: gardenerNamespace,
runtimeLister: lister,
runtimes: map[string]runtime.RuntimeDTO{},
logger: logger.WithField("orchestration", "resolver"),
}
}
// Resolve given an input slice of target specs to include and exclude, returns back a list of unique Runtime objects
func (resolver *GardenerRuntimeResolver) Resolve(targets TargetSpec) ([]Runtime, error) {
runtimeIncluded := map[string]bool{}
runtimeExcluded := map[string]bool{}
runtimes := []Runtime{}
shoots, err := resolver.getAllShoots()
if err != nil {
return nil, fmt.Errorf("while listing gardener shoots in namespace %s: %w", resolver.gardenerNamespace, err)
}
err = resolver.syncRuntimeOperations()
if err != nil {
return nil, fmt.Errorf("while syncing runtimes: %w", err)
}
// Assemble IDs of runtimes to exclude
for _, rt := range targets.Exclude {
runtimesToExclude, err := resolver.resolveRuntimeTarget(rt, shoots)
if err != nil {
return nil, err
}
for _, r := range runtimesToExclude {
runtimeExcluded[r.RuntimeID] = true
}
}
// Include runtimes which are not excluded
for _, rt := range targets.Include {
runtimesToAdd, err := resolver.resolveRuntimeTarget(rt, shoots)
if err != nil {
return nil, err
}
for _, r := range runtimesToAdd {
if !runtimeExcluded[r.RuntimeID] && !runtimeIncluded[r.RuntimeID] {
runtimeIncluded[r.RuntimeID] = true
runtimes = append(runtimes, r)
}
}
}
return runtimes, nil
}
func (resolver *GardenerRuntimeResolver) getAllShoots() ([]unstructured.Unstructured, error) {
ctx := context.Background()
shootList, err := resolver.gardenerClient.Resource(gardener.ShootResource).Namespace(resolver.gardenerNamespace).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}
return shootList.Items, nil
}
func (resolver *GardenerRuntimeResolver) syncRuntimeOperations() error {
runtimes, err := resolver.runtimeLister.ListAllRuntimes()
if err != nil {
return err
}
resolver.mutex.Lock()
defer resolver.mutex.Unlock()
for _, rt := range runtimes {
resolver.runtimes[rt.RuntimeID] = rt
}
return nil
}
func (resolver *GardenerRuntimeResolver) getRuntime(runtimeID string) (runtime.RuntimeDTO, bool) {
resolver.mutex.RLock()
defer resolver.mutex.RUnlock()
rt, ok := resolver.runtimes[runtimeID]
return rt, ok
}
func (resolver *GardenerRuntimeResolver) resolveRuntimeTarget(rt RuntimeTarget, shoots []unstructured.Unstructured) ([]Runtime, error) {
runtimes := []Runtime{}
// Iterate over all shoots. Evaluate target specs. If multiple are specified, all must match for a given shoot.
for _, s := range shoots {
shoot := &gardener.Shoot{s}
runtimeID := shoot.GetAnnotations()[runtimeIDAnnotation]
if runtimeID == "" {
resolver.logger.Errorf("Failed to get runtimeID from %s annotation for Shoot %s", runtimeIDAnnotation, shoot.GetName())
continue
}
r, ok := resolver.getRuntime(runtimeID)
if !ok {
resolver.logger.Errorf("Couldn't find runtime for runtimeID %s", runtimeID)
continue
}
lastOp := r.LastOperation()
// Skip runtimes for which the last operation is
// - not succeeded provision or unsuspension
// - suspension
// - deprovision
if lastOp.Type == runtime.Deprovision || lastOp.Type == runtime.Suspension || (lastOp.Type == runtime.Provision || lastOp.Type == runtime.Unsuspension) && lastOp.State != string(brokerapi.Succeeded) {
resolver.logger.Infof("Skipping Shoot %s (runtimeID: %s, instanceID %s) due to %s state: %s", shoot.GetName(), runtimeID, r.InstanceID, lastOp.Type, lastOp.State)
continue
}
maintenanceWindowBegin, err := time.Parse(maintenanceWindowFormat, shoot.GetSpecMaintenanceTimeWindowBegin())
if err != nil {
resolver.logger.Errorf("Failed to parse maintenanceWindowBegin value %s of shoot %s ", shoot.GetSpecMaintenanceTimeWindowBegin(), shoot.GetName())
continue
}
maintenanceWindowEnd, err := time.Parse(maintenanceWindowFormat, shoot.GetSpecMaintenanceTimeWindowEnd())
if err != nil {
resolver.logger.Errorf("Failed to parse maintenanceWindowEnd value %s of shoot %s ", shoot.GetSpecMaintenanceTimeWindowEnd(), shoot.GetName())
continue
}
// Match exact shoot by runtimeID
if rt.RuntimeID != "" {
if rt.RuntimeID == runtimeID {
runtimes = append(runtimes, resolver.runtimeFromDTO(r, shoot.GetName(), maintenanceWindowBegin, maintenanceWindowEnd))
}
continue
}
// Match exact shoot by instanceID
if rt.InstanceID != "" {
if rt.InstanceID != r.InstanceID {
continue
}
}
// Match exact shoot by name
if rt.Shoot != "" && rt.Shoot != shoot.GetName() {
continue
}
// Perform match against a specific PlanName
if rt.PlanName != "" {
if rt.PlanName != r.ServicePlanName {
continue
}
}
// Perform match against GlobalAccount regexp
if rt.GlobalAccount != "" {
matched, err := regexp.MatchString(rt.GlobalAccount, shoot.GetLabels()[globalAccountLabel])
if err != nil || !matched {
continue
}
}
// Perform match against SubAccount regexp
if rt.SubAccount != "" {
matched, err := regexp.MatchString(rt.SubAccount, shoot.GetLabels()[subAccountLabel])
if err != nil || !matched {
continue
}
}
// Perform match against Region regexp
if rt.Region != "" {
matched, err := regexp.MatchString(rt.Region, shoot.GetSpecRegion())
if err != nil || !matched {
continue
}
}
// Check if target: all is specified
if rt.Target != "" && rt.Target != TargetAll {
continue
}
runtimes = append(runtimes, resolver.runtimeFromDTO(r, shoot.GetName(), maintenanceWindowBegin, maintenanceWindowEnd))
}
return runtimes, nil
}
func (*GardenerRuntimeResolver) runtimeFromDTO(runtime runtime.RuntimeDTO, shootName string, windowBegin, windowEnd time.Time) Runtime {
return Runtime{
InstanceID: runtime.InstanceID,
RuntimeID: runtime.RuntimeID,
GlobalAccountID: runtime.GlobalAccountID,
SubAccountID: runtime.SubAccountID,
Plan: runtime.ServicePlanName,
Region: runtime.ProviderRegion,
ShootName: shootName,
MaintenanceWindowBegin: windowBegin,
MaintenanceWindowEnd: windowEnd,
MaintenanceDays: []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"},
}
}