github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/types/maintenance.go (about)

     1  /*
     2  Copyright 2022 Gravitational, Inc.
     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 types
    18  
    19  import (
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/gravitational/trace"
    24  
    25  	"github.com/gravitational/teleport/api/utils"
    26  )
    27  
    28  const (
    29  	// UpgraderKindKuberController is a short name used to identify the kube-controller-based
    30  	// external upgrader variant.
    31  	UpgraderKindKubeController = "kube"
    32  
    33  	// UpgraderKindSystemdUnit is a short name used to identify the systemd-unit-based
    34  	// external upgrader variant.
    35  	UpgraderKindSystemdUnit = "unit"
    36  )
    37  
    38  var validWeekdays = [7]time.Weekday{
    39  	time.Sunday,
    40  	time.Monday,
    41  	time.Tuesday,
    42  	time.Wednesday,
    43  	time.Thursday,
    44  	time.Friday,
    45  	time.Saturday,
    46  }
    47  
    48  // parseWeekday attempts to interpret a string as a time.Weekday. In the interest of flexibility,
    49  // parsing is case-insensitive and supports the common three-letter shorthand accepted by many
    50  // common scheduling utilites (e.g. contab, systemd timers).
    51  func parseWeekday(s string) (day time.Weekday, ok bool) {
    52  	for _, w := range validWeekdays {
    53  		if strings.EqualFold(w.String(), s) || strings.EqualFold(w.String()[:3], s) {
    54  			return w, true
    55  		}
    56  	}
    57  
    58  	return time.Sunday, false
    59  }
    60  
    61  // generator builds a closure that iterates valid maintenance config from the current day onward. Used in
    62  // schedule export logic and tests.
    63  func (w *AgentUpgradeWindow) generator(from time.Time) func() (start time.Time, end time.Time) {
    64  	from = from.UTC()
    65  	next := time.Date(
    66  		from.Year(),
    67  		from.Month(),
    68  		from.Day(),
    69  		int(w.UTCStartHour%24),
    70  		0, // min
    71  		0, // sec
    72  		0, // nsec
    73  		time.UTC,
    74  	)
    75  
    76  	var weekdays []time.Weekday
    77  	for _, d := range w.Weekdays {
    78  		if p, ok := parseWeekday(d); ok {
    79  			weekdays = append(weekdays, p)
    80  		}
    81  	}
    82  
    83  	return func() (start time.Time, end time.Time) {
    84  		for { // safe because invalid weekdays have been filtered out
    85  			start = next
    86  			end = start.Add(time.Hour)
    87  
    88  			next = next.AddDate(0, 0, 1)
    89  
    90  			if len(weekdays) == 0 {
    91  				return
    92  			}
    93  
    94  			for _, day := range weekdays {
    95  				if start.Weekday() == day {
    96  					return
    97  				}
    98  			}
    99  		}
   100  	}
   101  }
   102  
   103  // Export exports the next `n` upgrade windows as a schedule object, starting from `from`.
   104  func (w *AgentUpgradeWindow) Export(from time.Time, n int) AgentUpgradeSchedule {
   105  	gen := w.generator(from)
   106  
   107  	sched := AgentUpgradeSchedule{
   108  		Windows: make([]ScheduledAgentUpgradeWindow, 0, n),
   109  	}
   110  	for i := 0; i < n; i++ {
   111  		start, stop := gen()
   112  		sched.Windows = append(sched.Windows, ScheduledAgentUpgradeWindow{
   113  			Start: start.UTC(),
   114  			Stop:  stop.UTC(),
   115  		})
   116  	}
   117  
   118  	return sched
   119  }
   120  
   121  func (s *AgentUpgradeSchedule) Clone() *AgentUpgradeSchedule {
   122  	return utils.CloneProtoMsg(s)
   123  }
   124  
   125  // NewClusterMaintenanceConfig creates a new maintenance config with no parameters set.
   126  func NewClusterMaintenanceConfig() ClusterMaintenanceConfig {
   127  	var cmc ClusterMaintenanceConfigV1
   128  	cmc.setStaticFields()
   129  	return &cmc
   130  }
   131  
   132  // ClusterMaintenanceConfig represents a singleton config object used to schedule maintenance
   133  // windows. Currently this config object's only purpose is to configure a global agent
   134  // upgrade window, used to coordinate upgrade timing for non-control-plane agents.
   135  type ClusterMaintenanceConfig interface {
   136  	Resource
   137  
   138  	// GetNonce gets the nonce of the maintenance config.
   139  	GetNonce() uint64
   140  
   141  	// WithNonce creates a shallow copy with a new nonce.
   142  	WithNonce(nonce uint64) any
   143  
   144  	// GetAgentUpgradeWindow gets the agent upgrade window.
   145  	GetAgentUpgradeWindow() (win AgentUpgradeWindow, ok bool)
   146  
   147  	// SetAgentUpgradeWindow sets the agent upgrade window.
   148  	SetAgentUpgradeWindow(win AgentUpgradeWindow)
   149  
   150  	// WithinUpgradeWindow returns true if the time is within the configured
   151  	// upgrade window.
   152  	WithinUpgradeWindow(t time.Time) bool
   153  
   154  	CheckAndSetDefaults() error
   155  }
   156  
   157  func (m *ClusterMaintenanceConfigV1) setStaticFields() {
   158  	if m.Version == "" {
   159  		m.Version = V1
   160  	}
   161  
   162  	if m.Kind == "" {
   163  		m.Kind = KindClusterMaintenanceConfig
   164  	}
   165  
   166  	if m.Metadata.Name == "" {
   167  		m.Metadata.Name = MetaNameClusterMaintenanceConfig
   168  	}
   169  }
   170  
   171  func (m *ClusterMaintenanceConfigV1) CheckAndSetDefaults() error {
   172  	m.setStaticFields()
   173  
   174  	if err := m.ResourceHeader.CheckAndSetDefaults(); err != nil {
   175  		return trace.Wrap(err)
   176  	}
   177  
   178  	if m.Version != V1 {
   179  		return trace.BadParameter("unexpected maintenance config resource version %q (expected %q)", m.Version, V1)
   180  	}
   181  
   182  	if m.Kind == MetaNameClusterMaintenanceConfig {
   183  		// normalize easy mixup
   184  		m.Kind = KindClusterMaintenanceConfig
   185  	}
   186  
   187  	if m.Kind != KindClusterMaintenanceConfig {
   188  		return trace.BadParameter("unexpected maintenance config kind %q (expected %q)", m.Kind, KindClusterMaintenanceConfig)
   189  	}
   190  
   191  	if m.Metadata.Name == KindClusterMaintenanceConfig {
   192  		// normalize easy mixup
   193  		m.Metadata.Name = MetaNameClusterMaintenanceConfig
   194  	}
   195  
   196  	if m.Metadata.Name != MetaNameClusterMaintenanceConfig {
   197  		return trace.BadParameter("unexpected maintenance config name %q (expected %q)", m.Metadata.Name, MetaNameClusterMaintenanceConfig)
   198  	}
   199  
   200  	if m.Spec.AgentUpgrades != nil {
   201  		if h := m.Spec.AgentUpgrades.UTCStartHour; h > 23 {
   202  			return trace.BadParameter("agent upgrade window utc start hour must be in range 0..23, got %d", h)
   203  		}
   204  
   205  		for _, day := range m.Spec.AgentUpgrades.Weekdays {
   206  			if _, ok := parseWeekday(day); !ok {
   207  				return trace.BadParameter("invalid weekday in agent upgrade window: %q", day)
   208  			}
   209  		}
   210  	}
   211  
   212  	return nil
   213  }
   214  
   215  func (m *ClusterMaintenanceConfigV1) GetNonce() uint64 {
   216  	return m.Nonce
   217  }
   218  
   219  func (m *ClusterMaintenanceConfigV1) WithNonce(nonce uint64) any {
   220  	shallowCopy := *m
   221  	shallowCopy.Nonce = nonce
   222  	return &shallowCopy
   223  }
   224  
   225  func (m *ClusterMaintenanceConfigV1) GetAgentUpgradeWindow() (win AgentUpgradeWindow, ok bool) {
   226  	if m.Spec.AgentUpgrades == nil {
   227  		return AgentUpgradeWindow{}, false
   228  	}
   229  
   230  	return *m.Spec.AgentUpgrades, true
   231  }
   232  
   233  func (m *ClusterMaintenanceConfigV1) SetAgentUpgradeWindow(win AgentUpgradeWindow) {
   234  	m.Spec.AgentUpgrades = &win
   235  }
   236  
   237  // WithinUpgradeWindow returns true if the time is within the configured
   238  // upgrade window.
   239  func (m *ClusterMaintenanceConfigV1) WithinUpgradeWindow(t time.Time) bool {
   240  	upgradeWindow, ok := m.GetAgentUpgradeWindow()
   241  	if !ok {
   242  		return false
   243  	}
   244  
   245  	if len(upgradeWindow.Weekdays) == 0 {
   246  		if int(upgradeWindow.UTCStartHour) == t.Hour() {
   247  			return true
   248  		}
   249  	}
   250  
   251  	weekday := t.Weekday().String()
   252  	for _, upgradeWeekday := range upgradeWindow.Weekdays {
   253  		if weekday == upgradeWeekday {
   254  			if int(upgradeWindow.UTCStartHour) == t.Hour() {
   255  				return true
   256  			}
   257  		}
   258  	}
   259  	return false
   260  }