github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/boskos/mason/mason.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package mason
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"sync"
    25  	"time"
    26  
    27  	"gopkg.in/yaml.v2"
    28  
    29  	"github.com/sirupsen/logrus"
    30  	"k8s.io/test-infra/boskos/common"
    31  	"k8s.io/test-infra/boskos/storage"
    32  )
    33  
    34  const (
    35  	// LeasedResources is a common.UserData entry
    36  	LeasedResources = "leasedResources"
    37  )
    38  
    39  // Masonable should be implemented by all configurations
    40  type Masonable interface {
    41  	Construct(context.Context, common.Resource, common.TypeToResources) (*common.UserData, error)
    42  }
    43  
    44  // ConfigConverter converts a string into a Masonable
    45  type ConfigConverter func(string) (Masonable, error)
    46  
    47  type boskosClient interface {
    48  	Acquire(rtype, state, dest string) (*common.Resource, error)
    49  	AcquireByState(state, dest string, names []string) ([]common.Resource, error)
    50  	ReleaseOne(name, dest string) error
    51  	UpdateOne(name, state string, userData *common.UserData) error
    52  	SyncAll() error
    53  	UpdateAll(dest string) error
    54  	ReleaseAll(dest string) error
    55  }
    56  
    57  // Mason uses config to convert dirty resources to usable one
    58  type Mason struct {
    59  	client                             boskosClient
    60  	cleanerCount                       int
    61  	storage                            Storage
    62  	pending, fulfilled, cleaned        chan requirements
    63  	boskosWaitPeriod, boskosSyncPeriod time.Duration
    64  	wg                                 sync.WaitGroup
    65  	configConverters                   map[string]ConfigConverter
    66  	cancel                             context.CancelFunc
    67  }
    68  
    69  // requirements for a given resource
    70  type requirements struct {
    71  	resource    common.Resource
    72  	needs       common.ResourceNeeds
    73  	fulfillment common.TypeToResources
    74  }
    75  
    76  func (r requirements) isFulFilled() bool {
    77  	for rType, count := range r.needs {
    78  		resources, ok := r.fulfillment[rType]
    79  		if !ok {
    80  			return false
    81  		}
    82  		if len(resources) != count {
    83  			return false
    84  		}
    85  	}
    86  	return true
    87  }
    88  
    89  // ParseConfig reads data stored in given config path
    90  // In: configPath - path to the config file
    91  // Out: A list of ResourceConfig object on success, or nil on error.
    92  func ParseConfig(configPath string) ([]common.ResourcesConfig, error) {
    93  	if _, err := os.Stat(configPath); os.IsNotExist(err) {
    94  		return nil, err
    95  	}
    96  	file, err := ioutil.ReadFile(configPath)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  
   101  	var data common.MasonConfig
   102  	err = yaml.Unmarshal(file, &data)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	return data.Configs, nil
   107  }
   108  
   109  // ValidateConfig validates config with existing resources
   110  // In: configs   - a list of resources configs
   111  //     resources - a list of resources
   112  // Out: nil on success, error on failure
   113  func ValidateConfig(configs []common.ResourcesConfig, resources []common.Resource) error {
   114  	resourcesNeeds := map[string]int{}
   115  	actualResources := map[string]int{}
   116  
   117  	configNames := map[string]map[string]int{}
   118  	for _, c := range configs {
   119  		_, alreadyExists := configNames[c.Name]
   120  		if alreadyExists {
   121  			return fmt.Errorf("config %s already exists", c.Name)
   122  		}
   123  		configNames[c.Name] = c.Needs
   124  	}
   125  
   126  	for _, res := range resources {
   127  		_, useConfig := configNames[res.Type]
   128  		if useConfig {
   129  			c, ok := configNames[res.Type]
   130  			if !ok {
   131  				err := fmt.Errorf("resource type %s does not have associated config", res.Type)
   132  				logrus.WithError(err).Error("using useconfig implies associated config")
   133  				return err
   134  			}
   135  			// Updating resourceNeeds
   136  			for k, v := range c {
   137  				resourcesNeeds[k] += v
   138  			}
   139  		}
   140  		actualResources[res.Type]++
   141  	}
   142  
   143  	for rType, needs := range resourcesNeeds {
   144  		actual, ok := actualResources[rType]
   145  		if !ok {
   146  			err := fmt.Errorf("need for resource %s that does not exist", rType)
   147  			logrus.WithError(err).Errorf("invalid configuration")
   148  			return err
   149  		}
   150  		if needs > actual {
   151  			err := fmt.Errorf("not enough resource of type %s for provisioning", rType)
   152  			logrus.WithError(err).Errorf("invalid configuration")
   153  			return err
   154  		}
   155  	}
   156  	return nil
   157  }
   158  
   159  // NewMason creates and initialized a new Mason object
   160  // In: rtypes            - A list of resource types to act on
   161  //     cleanerCount      - Number of cleaning threads
   162  //     client            - boskos client
   163  //     waitPeriod        - time to wait before a new boskos operation (acquire mostly)
   164  //     syncPeriod        - time to wait before syncing resource information to boskos
   165  // Out: A Pointer to a Mason Object
   166  func NewMason(cleanerCount int, client boskosClient, waitPeriod, syncPeriod time.Duration) *Mason {
   167  	return &Mason{
   168  		client:           client,
   169  		cleanerCount:     cleanerCount,
   170  		storage:          *newStorage(storage.NewMemoryStorage()),
   171  		pending:          make(chan requirements),
   172  		cleaned:          make(chan requirements, cleanerCount+1),
   173  		fulfilled:        make(chan requirements, cleanerCount+1),
   174  		boskosWaitPeriod: waitPeriod,
   175  		boskosSyncPeriod: syncPeriod,
   176  		configConverters: map[string]ConfigConverter{},
   177  	}
   178  }
   179  
   180  func checkUserData(res common.Resource) (common.LeasedResources, error) {
   181  	var leasedResources common.LeasedResources
   182  	if res.UserData == nil {
   183  		err := fmt.Errorf("user data is empty")
   184  		logrus.WithError(err).Errorf("failed to extract %s", LeasedResources)
   185  		return nil, err
   186  	}
   187  
   188  	if err := res.UserData.Extract(LeasedResources, &leasedResources); err != nil {
   189  		logrus.WithError(err).Errorf("failed to extract %s", LeasedResources)
   190  		return nil, err
   191  	}
   192  	return leasedResources, nil
   193  }
   194  
   195  // RegisterConfigConverter is used to register a new Masonable interface
   196  // In: name - identifier for Masonable implementation
   197  //     fn   - function that will parse the configuration string and return a Masonable interface
   198  //
   199  // Out: nil on success, error otherwise
   200  func (m *Mason) RegisterConfigConverter(name string, fn ConfigConverter) error {
   201  	_, ok := m.configConverters[name]
   202  	if ok {
   203  		return fmt.Errorf("a converter for %s already exists", name)
   204  	}
   205  	m.configConverters[name] = fn
   206  	return nil
   207  }
   208  
   209  func (m *Mason) convertConfig(configEntry *common.ResourcesConfig) (Masonable, error) {
   210  	fn, ok := m.configConverters[configEntry.Config.Type]
   211  	if !ok {
   212  		return nil, fmt.Errorf("config type %s is not supported", configEntry.Name)
   213  	}
   214  	return fn(configEntry.Config.Content)
   215  }
   216  
   217  func (m *Mason) garbageCollect(req requirements) {
   218  	names := []string{req.resource.Name}
   219  
   220  	for _, resources := range req.fulfillment {
   221  		for _, r := range resources {
   222  			names = append(names, r.Name)
   223  		}
   224  	}
   225  
   226  	for _, name := range names {
   227  		if err := m.client.ReleaseOne(name, common.Dirty); err != nil {
   228  			logrus.WithError(err).Errorf("Unable to release leased resource %s", name)
   229  		}
   230  	}
   231  }
   232  
   233  func (m *Mason) cleanAll(ctx context.Context) {
   234  	defer func() {
   235  		logrus.Info("Exiting cleanAll Thread")
   236  		m.wg.Done()
   237  	}()
   238  	for {
   239  		select {
   240  		case <-ctx.Done():
   241  			return
   242  		case req := <-m.fulfilled:
   243  			if err := m.cleanOne(ctx, &req.resource, req.fulfillment); err != nil {
   244  				logrus.WithError(err).Errorf("unable to clean resource %s", req.resource.Name)
   245  				m.garbageCollect(req)
   246  			} else {
   247  				m.cleaned <- req
   248  			}
   249  		}
   250  	}
   251  }
   252  
   253  func (m *Mason) cleanOne(ctx context.Context, res *common.Resource, leasedResources common.TypeToResources) error {
   254  	configEntry, err := m.storage.GetConfig(res.Type)
   255  	if err != nil {
   256  		logrus.WithError(err).Errorf("failed to get config for resource %s", res.Type)
   257  		return err
   258  	}
   259  	config, err := m.convertConfig(&configEntry)
   260  	if err != nil {
   261  		logrus.WithError(err).Errorf("failed to convert config type %s - \n%s", configEntry.Config.Type, configEntry.Config.Content)
   262  		return err
   263  	}
   264  
   265  	errChan := make(chan error)
   266  	var userData *common.UserData
   267  
   268  	go func() {
   269  		var err error
   270  		userData, err = config.Construct(ctx, *res, leasedResources.Copy())
   271  		errChan <- err
   272  	}()
   273  
   274  	select {
   275  	case err = <-errChan:
   276  		if err != nil {
   277  			logrus.WithError(err).Errorf("failed to construct resource %s", res.Name)
   278  			return err
   279  		}
   280  	case <-ctx.Done():
   281  		return ctx.Err()
   282  	}
   283  
   284  	if err := m.client.UpdateOne(res.Name, res.State, userData); err != nil {
   285  		logrus.WithError(err).Error("unable to update user data")
   286  		return err
   287  	}
   288  	if res.UserData == nil {
   289  		res.UserData = userData
   290  	} else {
   291  		res.UserData.Update(userData)
   292  	}
   293  	logrus.Infof("Resource %s is cleaned", res.Name)
   294  	return nil
   295  }
   296  
   297  func (m *Mason) freeAll(ctx context.Context) {
   298  	defer func() {
   299  		logrus.Info("Exiting freeAll Thread")
   300  		m.wg.Done()
   301  	}()
   302  	for {
   303  		select {
   304  		case <-ctx.Done():
   305  			return
   306  		case req := <-m.cleaned:
   307  			if err := m.freeOne(&req.resource); err != nil {
   308  				logrus.WithError(err).Errorf("failed to free up resource %s", req.resource.Name)
   309  				m.garbageCollect(req)
   310  			}
   311  		}
   312  	}
   313  }
   314  
   315  func (m *Mason) freeOne(res *common.Resource) error {
   316  	leasedResources, err := checkUserData(*res)
   317  	if err != nil {
   318  		return err
   319  	}
   320  	// TODO: Implement a ReleaseMultiple in a transaction to prevent orphans
   321  	// Finally return the resource as free
   322  	if err := m.client.ReleaseOne(res.Name, common.Free); err != nil {
   323  		logrus.WithError(err).Errorf("failed to release resource %s", res.Name)
   324  		return err
   325  	}
   326  	// And release leased resources as res.Name state
   327  	for _, name := range leasedResources {
   328  		if err := m.client.ReleaseOne(name, res.Name); err != nil {
   329  			logrus.WithError(err).Errorf("unable to release %s to state %s", name, res.Name)
   330  			return err
   331  		}
   332  	}
   333  	logrus.Infof("Resource %s has been freed", res.Name)
   334  	return nil
   335  }
   336  
   337  func (m *Mason) recycleAll(ctx context.Context) {
   338  	defer func() {
   339  		logrus.Info("Exiting recycleAll Thread")
   340  		m.wg.Done()
   341  	}()
   342  	tick := time.NewTicker(m.boskosWaitPeriod).C
   343  	for {
   344  		select {
   345  		case <-ctx.Done():
   346  			return
   347  		case <-tick:
   348  			configs, err := m.storage.GetConfigs()
   349  			if err != nil {
   350  				logrus.WithError(err).Error("unable to get configuration")
   351  				continue
   352  			}
   353  			var configTypes []string
   354  			for _, c := range configs {
   355  				configTypes = append(configTypes, c.Name)
   356  			}
   357  			for _, r := range configTypes {
   358  				if res, err := m.client.Acquire(r, common.Dirty, common.Cleaning); err != nil {
   359  					logrus.WithError(err).Debug("boskos acquire failed!")
   360  				} else {
   361  					if req, err := m.recycleOne(res); err != nil {
   362  						logrus.WithError(err).Errorf("unable to recycle resource %s", res.Name)
   363  						if err := m.client.ReleaseOne(res.Name, common.Dirty); err != nil {
   364  							logrus.WithError(err).Errorf("Unable to release resources %s", res.Name)
   365  						}
   366  					} else {
   367  						m.pending <- *req
   368  					}
   369  				}
   370  			}
   371  		}
   372  	}
   373  }
   374  
   375  func (m *Mason) recycleOne(res *common.Resource) (*requirements, error) {
   376  	logrus.Infof("Resource %s is being recycled", res.Name)
   377  	configEntry, err := m.storage.GetConfig(res.Type)
   378  	if err != nil {
   379  		logrus.WithError(err).Errorf("could not get config for resource type %s", res.Type)
   380  		return nil, err
   381  	}
   382  
   383  	leasedResources, _ := checkUserData(*res)
   384  	if leasedResources != nil {
   385  		resources, err := m.client.AcquireByState(res.Name, common.Leased, leasedResources)
   386  		if err != nil {
   387  			logrus.WithError(err).Warningf("could not acquire any leased resources for %s", res.Name)
   388  		}
   389  
   390  		for _, r := range resources {
   391  			if err := m.client.ReleaseOne(r.Name, common.Dirty); err != nil {
   392  				logrus.WithError(err).Warningf("could not release resource %s", r.Name)
   393  			}
   394  		}
   395  		// Deleting Leased Resources
   396  		res.UserData.Delete(LeasedResources)
   397  		if err := m.client.UpdateOne(res.Name, res.State, common.UserDataFromMap(map[string]string{LeasedResources: ""})); err != nil {
   398  			logrus.WithError(err).Errorf("could not update resource %s with freed leased resources", res.Name)
   399  		}
   400  	}
   401  
   402  	return &requirements{
   403  		fulfillment: common.TypeToResources{},
   404  		needs:       configEntry.Needs,
   405  		resource:    *res,
   406  	}, nil
   407  }
   408  
   409  func (m *Mason) syncAll(ctx context.Context) {
   410  	defer func() {
   411  		logrus.Info("Exiting UpdateAll Thread")
   412  		m.wg.Done()
   413  	}()
   414  	tick := time.NewTicker(m.boskosSyncPeriod).C
   415  	for {
   416  		select {
   417  		case <-ctx.Done():
   418  			return
   419  		case <-tick:
   420  			if err := m.client.SyncAll(); err != nil {
   421  				logrus.WithError(err).Errorf("failed to sync resources")
   422  			}
   423  		}
   424  	}
   425  }
   426  
   427  func (m *Mason) fulfillAll(ctx context.Context) {
   428  	defer func() {
   429  		logrus.Info("Exiting fulfillAll Thread")
   430  		m.wg.Done()
   431  	}()
   432  	for {
   433  		select {
   434  		case <-ctx.Done():
   435  			return
   436  		case req := <-m.pending:
   437  			if err := m.fulfillOne(ctx, &req); err != nil {
   438  				m.garbageCollect(req)
   439  			} else {
   440  				m.fulfilled <- req
   441  			}
   442  		}
   443  	}
   444  }
   445  
   446  func (m *Mason) fulfillOne(ctx context.Context, req *requirements) error {
   447  	// Making a copy
   448  	needs := common.ResourceNeeds{}
   449  	for k, v := range req.needs {
   450  		needs[k] = v
   451  	}
   452  	tick := time.NewTicker(m.boskosWaitPeriod).C
   453  	for rType := range needs {
   454  		for needs[rType] > 0 {
   455  			select {
   456  			case <-ctx.Done():
   457  				return ctx.Err()
   458  			case <-tick:
   459  				m.updateResources(req)
   460  				if res, err := m.client.Acquire(rType, common.Free, common.Leased); err != nil {
   461  					logrus.WithError(err).Debug("boskos acquire failed!")
   462  				} else {
   463  					req.fulfillment[rType] = append(req.fulfillment[rType], *res)
   464  					needs[rType]--
   465  				}
   466  			}
   467  		}
   468  	}
   469  	if req.isFulFilled() {
   470  		var leasedResources common.LeasedResources
   471  		for _, lr := range req.fulfillment {
   472  			for _, r := range lr {
   473  				leasedResources = append(leasedResources, r.Name)
   474  			}
   475  		}
   476  		userData := &common.UserData{}
   477  		if err := userData.Set(LeasedResources, &leasedResources); err != nil {
   478  			logrus.WithError(err).Errorf("failed to add %s user data", LeasedResources)
   479  			return err
   480  		}
   481  		if err := m.client.UpdateOne(req.resource.Name, req.resource.State, userData); err != nil {
   482  			logrus.WithError(err).Errorf("Unable to update resource %s", req.resource.Name)
   483  			return err
   484  		}
   485  		if req.resource.UserData == nil {
   486  			req.resource.UserData = userData
   487  		} else {
   488  			req.resource.UserData.Update(userData)
   489  		}
   490  
   491  		logrus.Infof("requirements for release %s is fulfilled", req.resource.Name)
   492  		return nil
   493  	}
   494  	return nil
   495  }
   496  
   497  func (m *Mason) updateResources(req *requirements) {
   498  	var resources []common.Resource
   499  	resources = append(resources, req.resource)
   500  	for _, leasedResources := range req.fulfillment {
   501  		resources = append(resources, leasedResources...)
   502  	}
   503  	for _, r := range resources {
   504  		if err := m.client.UpdateOne(r.Name, r.State, nil); err != nil {
   505  			logrus.WithError(err).Warningf("failed to update resource %s", r.Name)
   506  		}
   507  	}
   508  }
   509  
   510  // UpdateConfigs updates configs from storage path
   511  // In: storagePath - the path to read the config file from
   512  // Out: nil on success error otherwise
   513  func (m *Mason) UpdateConfigs(storagePath string) error {
   514  	configs, err := ParseConfig(storagePath)
   515  	if err != nil {
   516  		logrus.WithError(err).Error("unable to parse config")
   517  		return err
   518  	}
   519  	return m.storage.SyncConfigs(configs)
   520  }
   521  
   522  func (m *Mason) start(ctx context.Context, fn func(context.Context)) {
   523  	go func() {
   524  		fn(ctx)
   525  	}()
   526  	m.wg.Add(1)
   527  }
   528  
   529  // Start Mason
   530  func (m *Mason) Start() {
   531  	ctx, cancel := context.WithCancel(context.Background())
   532  	m.cancel = cancel
   533  	m.start(ctx, m.syncAll)
   534  	m.start(ctx, m.recycleAll)
   535  	m.start(ctx, m.fulfillAll)
   536  	for i := 0; i < m.cleanerCount; i++ {
   537  		m.start(ctx, m.cleanAll)
   538  	}
   539  	m.start(ctx, m.freeAll)
   540  	logrus.Info("Mason started")
   541  }
   542  
   543  // Stop Mason
   544  func (m *Mason) Stop() {
   545  	logrus.Info("Stopping Mason")
   546  	m.cancel()
   547  	m.wg.Wait()
   548  	close(m.pending)
   549  	close(m.cleaned)
   550  	close(m.fulfilled)
   551  	m.client.ReleaseAll(common.Dirty)
   552  	logrus.Info("Mason stopped")
   553  }