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 }