go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gce/api/config/v1/amount.go (about)

     1  // Copyright 2019 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package config
    16  
    17  import (
    18  	"sort"
    19  	"time"
    20  
    21  	"go.chromium.org/luci/common/errors"
    22  	"go.chromium.org/luci/config/validation"
    23  )
    24  
    25  // getAmount returns the amount to use given the proposed amount and time.
    26  // Assumes this amount has been validated.
    27  func (a *Amount) getAmount(proposed int32, now time.Time) (int32, error) {
    28  	for _, s := range a.GetChange() {
    29  		start, err := s.mostRecentStart(now)
    30  		if err != nil {
    31  			return 0, errors.Annotate(err, "invalid start time").Err()
    32  		}
    33  		if start.Before(now) || start.Equal(now) {
    34  			sec, err := s.Length.ToSeconds()
    35  			if err != nil {
    36  				return 0, errors.Annotate(err, "invalid length").Err()
    37  			}
    38  			end := start.Add(time.Second * time.Duration(sec))
    39  			if now.Before(end) {
    40  				if proposed < s.Min {
    41  					return s.Min, nil
    42  				}
    43  				if proposed > s.Max {
    44  					return s.Max, nil
    45  				}
    46  				return proposed, nil
    47  			}
    48  		}
    49  	}
    50  	if proposed < a.GetMin() {
    51  		return a.GetMin(), nil
    52  	}
    53  	if proposed > a.GetMax() {
    54  		return a.GetMax(), nil
    55  	}
    56  	return proposed, nil
    57  }
    58  
    59  // Validate validates this amount.
    60  func (a *Amount) Validate(c *validation.Context) {
    61  	if a.GetMin() < 0 {
    62  		c.Errorf("minimum amount must be non-negative")
    63  	}
    64  	if a.GetMax() < 0 {
    65  		c.Errorf("maximum amount must be non-negative")
    66  	}
    67  	if a.GetMin() > a.GetMax() {
    68  		c.Errorf("minimum amount must not exceed maximum amount")
    69  	}
    70  	a.validateSchedules(c)
    71  }
    72  
    73  // indexedSchedule encapsulates *Schedules for sorting.
    74  type indexedSchedule struct {
    75  	*Schedule
    76  	// index is the original index of the *Schedule before sorting.
    77  	index int
    78  	// sortKey is the time.Time by which []indexedSchedules should be sorted.
    79  	sortKey time.Time
    80  }
    81  
    82  // validateSchedules validates the schedules in this amount.
    83  func (a *Amount) validateSchedules(c *validation.Context) {
    84  	// The algorithm works with any time 7 days greater than the zero time.
    85  	now := time.Date(2018, 1, 1, 12, 0, 0, 0, time.UTC)
    86  	schs := make([]indexedSchedule, len(a.GetChange()))
    87  	for i, s := range a.GetChange() {
    88  		c.Enter("change %d", i)
    89  		s.Validate(c)
    90  		c.Exit()
    91  		t, err := s.mostRecentStart(now)
    92  		if err != nil {
    93  			// s.Validate(c) already emitted this error.
    94  			return
    95  		}
    96  		schs[i] = indexedSchedule{
    97  			Schedule: s,
    98  			index:    i,
    99  			sortKey:  t,
   100  		}
   101  	}
   102  	sort.Slice(schs, func(i, j int) bool { return schs[i].sortKey.Before(schs[j].sortKey) })
   103  	prevEnd := time.Time{}
   104  	for i := 0; i < len(schs); i++ {
   105  		c.Enter("change %d", schs[i].index)
   106  		start := schs[i].sortKey
   107  		if schs[i].sortKey.Before(prevEnd) {
   108  			// Implies intervals are half-open: [start, end). prevEnd
   109  			// is initialized to the zero time, therefore this can't
   110  			// succeed when i is 0, meaning i-1 is never -1.
   111  			c.Errorf("start time is before change %d", schs[i-1].index)
   112  		}
   113  		sec, err := schs[i].Schedule.Length.ToSeconds()
   114  		if err != nil {
   115  			c.Exit()
   116  			return
   117  		}
   118  		prevEnd = start.Add(time.Second * time.Duration(sec))
   119  		c.Exit()
   120  	}
   121  	if len(schs) > 0 {
   122  		// Schedules are relative to the week.
   123  		// Check for a conflict between the last and first.
   124  		c.Enter("change %d", schs[0].index)
   125  		// Treat the first schedule as starting in a week. This checks for a conflict
   126  		// between the last configured schedule and the first configured schedule.
   127  		start := schs[0].sortKey.Add(time.Hour * time.Duration(24*7))
   128  		if start.Before(prevEnd) {
   129  			c.Errorf("start time is before change %d", schs[len(schs)-1].index)
   130  		}
   131  	}
   132  }