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  }