github.com/kyma-project/kyma-environment-broker@v0.0.1/common/orchestration/resolver.go (about) 1 package orchestration 2 3 import ( 4 "context" 5 "fmt" 6 "regexp" 7 "sync" 8 "time" 9 10 "github.com/kyma-project/kyma-environment-broker/common/gardener" 11 "github.com/kyma-project/kyma-environment-broker/common/runtime" 12 "github.com/sirupsen/logrus" 13 14 brokerapi "github.com/pivotal-cf/brokerapi/v8/domain" 15 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 17 "k8s.io/client-go/dynamic" 18 ) 19 20 // RuntimeLister is the interface to get runtime objects from KEB 21 // 22 //go:generate mockery --name=RuntimeLister --output=. --outpkg=orchestration --case=underscore --structname RuntimeListerMock --filename runtime_lister_mock.go 23 type RuntimeLister interface { 24 ListAllRuntimes() ([]runtime.RuntimeDTO, error) 25 } 26 27 // GardenerRuntimeResolver is the default resolver which implements the RuntimeResolver interface. 28 // This resolver uses the Shoot resources on the Gardener cluster to resolve the runtime targets. 29 // 30 // Naive implementation, listing all the shoots and perfom filtering on the result. 31 // The logic could be optimized with k8s client cache using shoot lister / indexer. 32 // The implementation is thread safe, i.e. it is safe to call Resolve() from multiple threads concurrently. 33 type GardenerRuntimeResolver struct { 34 gardenerClient dynamic.Interface 35 gardenerNamespace string 36 runtimeLister RuntimeLister 37 runtimes map[string]runtime.RuntimeDTO 38 mutex sync.RWMutex 39 logger logrus.FieldLogger 40 } 41 42 const ( 43 globalAccountLabel = "account" 44 subAccountLabel = "subaccount" 45 runtimeIDAnnotation = "kcp.provisioner.kyma-project.io/runtime-id" 46 maintenanceWindowFormat = "150405-0700" 47 ) 48 49 // NewGardenerRuntimeResolver constructs a GardenerRuntimeResolver with the mandatory input parameters. 50 func NewGardenerRuntimeResolver(gardenerClient dynamic.Interface, gardenerNamespace string, lister RuntimeLister, logger logrus.FieldLogger) *GardenerRuntimeResolver { 51 return &GardenerRuntimeResolver{ 52 gardenerClient: gardenerClient, 53 gardenerNamespace: gardenerNamespace, 54 runtimeLister: lister, 55 runtimes: map[string]runtime.RuntimeDTO{}, 56 logger: logger.WithField("orchestration", "resolver"), 57 } 58 } 59 60 // Resolve given an input slice of target specs to include and exclude, returns back a list of unique Runtime objects 61 func (resolver *GardenerRuntimeResolver) Resolve(targets TargetSpec) ([]Runtime, error) { 62 runtimeIncluded := map[string]bool{} 63 runtimeExcluded := map[string]bool{} 64 runtimes := []Runtime{} 65 shoots, err := resolver.getAllShoots() 66 if err != nil { 67 return nil, fmt.Errorf("while listing gardener shoots in namespace %s: %w", resolver.gardenerNamespace, err) 68 } 69 err = resolver.syncRuntimeOperations() 70 if err != nil { 71 return nil, fmt.Errorf("while syncing runtimes: %w", err) 72 } 73 74 // Assemble IDs of runtimes to exclude 75 for _, rt := range targets.Exclude { 76 runtimesToExclude, err := resolver.resolveRuntimeTarget(rt, shoots) 77 if err != nil { 78 return nil, err 79 } 80 for _, r := range runtimesToExclude { 81 runtimeExcluded[r.RuntimeID] = true 82 } 83 } 84 85 // Include runtimes which are not excluded 86 for _, rt := range targets.Include { 87 runtimesToAdd, err := resolver.resolveRuntimeTarget(rt, shoots) 88 if err != nil { 89 return nil, err 90 } 91 for _, r := range runtimesToAdd { 92 if !runtimeExcluded[r.RuntimeID] && !runtimeIncluded[r.RuntimeID] { 93 runtimeIncluded[r.RuntimeID] = true 94 runtimes = append(runtimes, r) 95 } 96 } 97 } 98 99 return runtimes, nil 100 } 101 102 func (resolver *GardenerRuntimeResolver) getAllShoots() ([]unstructured.Unstructured, error) { 103 ctx := context.Background() 104 shootList, err := resolver.gardenerClient.Resource(gardener.ShootResource).Namespace(resolver.gardenerNamespace).List(ctx, metav1.ListOptions{}) 105 if err != nil { 106 return nil, err 107 } 108 109 return shootList.Items, nil 110 } 111 112 func (resolver *GardenerRuntimeResolver) syncRuntimeOperations() error { 113 runtimes, err := resolver.runtimeLister.ListAllRuntimes() 114 if err != nil { 115 return err 116 } 117 resolver.mutex.Lock() 118 defer resolver.mutex.Unlock() 119 120 for _, rt := range runtimes { 121 resolver.runtimes[rt.RuntimeID] = rt 122 } 123 124 return nil 125 } 126 127 func (resolver *GardenerRuntimeResolver) getRuntime(runtimeID string) (runtime.RuntimeDTO, bool) { 128 resolver.mutex.RLock() 129 defer resolver.mutex.RUnlock() 130 rt, ok := resolver.runtimes[runtimeID] 131 132 return rt, ok 133 } 134 135 func (resolver *GardenerRuntimeResolver) resolveRuntimeTarget(rt RuntimeTarget, shoots []unstructured.Unstructured) ([]Runtime, error) { 136 runtimes := []Runtime{} 137 // Iterate over all shoots. Evaluate target specs. If multiple are specified, all must match for a given shoot. 138 for _, s := range shoots { 139 shoot := &gardener.Shoot{s} 140 runtimeID := shoot.GetAnnotations()[runtimeIDAnnotation] 141 if runtimeID == "" { 142 resolver.logger.Errorf("Failed to get runtimeID from %s annotation for Shoot %s", runtimeIDAnnotation, shoot.GetName()) 143 continue 144 } 145 r, ok := resolver.getRuntime(runtimeID) 146 if !ok { 147 resolver.logger.Errorf("Couldn't find runtime for runtimeID %s", runtimeID) 148 continue 149 } 150 151 lastOp := r.LastOperation() 152 // Skip runtimes for which the last operation is 153 // - not succeeded provision or unsuspension 154 // - suspension 155 // - deprovision 156 if lastOp.Type == runtime.Deprovision || lastOp.Type == runtime.Suspension || (lastOp.Type == runtime.Provision || lastOp.Type == runtime.Unsuspension) && lastOp.State != string(brokerapi.Succeeded) { 157 resolver.logger.Infof("Skipping Shoot %s (runtimeID: %s, instanceID %s) due to %s state: %s", shoot.GetName(), runtimeID, r.InstanceID, lastOp.Type, lastOp.State) 158 continue 159 } 160 maintenanceWindowBegin, err := time.Parse(maintenanceWindowFormat, shoot.GetSpecMaintenanceTimeWindowBegin()) 161 if err != nil { 162 resolver.logger.Errorf("Failed to parse maintenanceWindowBegin value %s of shoot %s ", shoot.GetSpecMaintenanceTimeWindowBegin(), shoot.GetName()) 163 continue 164 } 165 maintenanceWindowEnd, err := time.Parse(maintenanceWindowFormat, shoot.GetSpecMaintenanceTimeWindowEnd()) 166 if err != nil { 167 resolver.logger.Errorf("Failed to parse maintenanceWindowEnd value %s of shoot %s ", shoot.GetSpecMaintenanceTimeWindowEnd(), shoot.GetName()) 168 continue 169 } 170 171 // Match exact shoot by runtimeID 172 if rt.RuntimeID != "" { 173 if rt.RuntimeID == runtimeID { 174 runtimes = append(runtimes, resolver.runtimeFromDTO(r, shoot.GetName(), maintenanceWindowBegin, maintenanceWindowEnd)) 175 } 176 continue 177 } 178 179 // Match exact shoot by instanceID 180 if rt.InstanceID != "" { 181 if rt.InstanceID != r.InstanceID { 182 continue 183 } 184 } 185 186 // Match exact shoot by name 187 if rt.Shoot != "" && rt.Shoot != shoot.GetName() { 188 continue 189 } 190 191 // Perform match against a specific PlanName 192 if rt.PlanName != "" { 193 if rt.PlanName != r.ServicePlanName { 194 continue 195 } 196 } 197 198 // Perform match against GlobalAccount regexp 199 if rt.GlobalAccount != "" { 200 matched, err := regexp.MatchString(rt.GlobalAccount, shoot.GetLabels()[globalAccountLabel]) 201 if err != nil || !matched { 202 continue 203 } 204 } 205 206 // Perform match against SubAccount regexp 207 if rt.SubAccount != "" { 208 matched, err := regexp.MatchString(rt.SubAccount, shoot.GetLabels()[subAccountLabel]) 209 if err != nil || !matched { 210 continue 211 } 212 } 213 214 // Perform match against Region regexp 215 if rt.Region != "" { 216 matched, err := regexp.MatchString(rt.Region, shoot.GetSpecRegion()) 217 if err != nil || !matched { 218 continue 219 } 220 } 221 222 // Check if target: all is specified 223 if rt.Target != "" && rt.Target != TargetAll { 224 continue 225 } 226 227 runtimes = append(runtimes, resolver.runtimeFromDTO(r, shoot.GetName(), maintenanceWindowBegin, maintenanceWindowEnd)) 228 } 229 230 return runtimes, nil 231 } 232 233 func (*GardenerRuntimeResolver) runtimeFromDTO(runtime runtime.RuntimeDTO, shootName string, windowBegin, windowEnd time.Time) Runtime { 234 return Runtime{ 235 InstanceID: runtime.InstanceID, 236 RuntimeID: runtime.RuntimeID, 237 GlobalAccountID: runtime.GlobalAccountID, 238 SubAccountID: runtime.SubAccountID, 239 Plan: runtime.ServicePlanName, 240 Region: runtime.ProviderRegion, 241 ShootName: shootName, 242 MaintenanceWindowBegin: windowBegin, 243 MaintenanceWindowEnd: windowEnd, 244 MaintenanceDays: []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}, 245 } 246 }