pkg/providers/tinkerbell/validate.go (293 lines of code) (raw):
package tinkerbell
import (
"errors"
"fmt"
"path"
"strings"
tinkv1alpha1 "github.com/tinkerbell/tink/pkg/apis/core/v1alpha1"
"github.com/aws/eks-anywhere/pkg/api/v1alpha1"
"github.com/aws/eks-anywhere/pkg/networkutils"
"github.com/aws/eks-anywhere/pkg/providers/tinkerbell/hardware"
"github.com/aws/eks-anywhere/pkg/semver"
)
func validateOsFamily(spec *ClusterSpec) error {
controlPlaneRef := spec.Cluster.Spec.ControlPlaneConfiguration.MachineGroupRef
controlPlaneOsFamily := spec.MachineConfigs[controlPlaneRef.Name].OSFamily()
if spec.Cluster.Spec.ExternalEtcdConfiguration != nil {
etcdMachineRef := spec.Cluster.Spec.ExternalEtcdConfiguration.MachineGroupRef
if spec.MachineConfigs[etcdMachineRef.Name].OSFamily() != controlPlaneOsFamily {
return errors.New("etcd osFamily cannot be different from control plane osFamily")
}
}
if controlPlaneOsFamily == v1alpha1.Bottlerocket {
if err := validateK8sVersionForBottleRocketOS(string(spec.Cluster.Spec.KubernetesVersion)); err != nil {
return fmt.Errorf("machineGroupRef %s: %v", controlPlaneRef.Name, err)
}
}
for _, group := range spec.Cluster.Spec.WorkerNodeGroupConfigurations {
groupRef := group.MachineGroupRef
if spec.MachineConfigs[groupRef.Name].OSFamily() != controlPlaneOsFamily {
return errors.New("worker node group osFamily cannot be different from control plane osFamily")
}
if group.KubernetesVersion != nil && *group.KubernetesVersion != "" && spec.MachineConfigs[groupRef.Name].OSFamily() == v1alpha1.Bottlerocket {
if err := validateK8sVersionForBottleRocketOS(string(*group.KubernetesVersion)); err != nil {
return fmt.Errorf("machineGroupRef %s: %v", groupRef.Name, err)
}
}
}
if controlPlaneOsFamily != v1alpha1.Bottlerocket && spec.DatacenterConfig.Spec.OSImageURL == "" && spec.ControlPlaneMachineConfig().Spec.OSImageURL == "" {
return errors.New("please use bottlerocket as osFamily for auto-importing or provide a valid osImageURL")
}
return nil
}
func validateK8sVersionForBottleRocketOS(kubernetesVersion string) error {
kubeVersionSemver, err := semver.New(kubernetesVersion + ".0")
if err != nil {
return fmt.Errorf("converting kubeVersion %v to semver %v", kubernetesVersion, err)
}
kube128Semver, _ := semver.New(string(v1alpha1.Kube128) + ".0")
if kubeVersionSemver.GreaterThan(kube128Semver) {
return errors.New("tinkerbell provider does not support K8s version 1.29+ for BottleRocket OS")
}
return nil
}
func validateUpgradeRolloutStrategy(spec *ClusterSpec) error {
cpUpgradeRolloutStrategyType := v1alpha1.RollingUpdateStrategyType
if spec.ControlPlaneConfiguration().UpgradeRolloutStrategy != nil {
cpUpgradeRolloutStrategyType = spec.ControlPlaneConfiguration().UpgradeRolloutStrategy.Type
controlPlaneRef := spec.ControlPlaneConfiguration().MachineGroupRef
controlPlaneOsFamily := spec.MachineConfigs[controlPlaneRef.Name].OSFamily()
if controlPlaneOsFamily != v1alpha1.Ubuntu && cpUpgradeRolloutStrategyType == v1alpha1.InPlaceStrategyType {
return errors.New("InPlace upgrades are only supported on the Ubuntu OS family")
}
}
for _, group := range spec.Cluster.Spec.WorkerNodeGroupConfigurations {
wnUpgradeRolloutStrategyType := v1alpha1.RollingUpdateStrategyType
groupRef := group.MachineGroupRef
if group.UpgradeRolloutStrategy != nil {
wnUpgradeRolloutStrategyType = group.UpgradeRolloutStrategy.Type
if spec.MachineConfigs[groupRef.Name].OSFamily() != v1alpha1.Ubuntu && wnUpgradeRolloutStrategyType == v1alpha1.InPlaceStrategyType {
return errors.New("InPlace upgrades are only supported on the Ubuntu OS family")
}
}
if wnUpgradeRolloutStrategyType != cpUpgradeRolloutStrategyType {
return errors.New("cannot specify different upgrade rollout strategy types for control plane and worker node group configurations")
}
}
return nil
}
func validateAutoScalerDisabledForInPlace(spec *ClusterSpec) error {
cpUpgradeRolloutStrategyType := spec.ControlPlaneConfiguration().UpgradeRolloutStrategy
// We do not support different strategy types for Inplace between CP and worker nodes so it is okay to check only CP
if cpUpgradeRolloutStrategyType == nil || cpUpgradeRolloutStrategyType.Type != v1alpha1.InPlaceStrategyType {
return nil
}
for _, wng := range spec.Cluster.Spec.WorkerNodeGroupConfigurations {
if wng.AutoScalingConfiguration != nil {
return errors.New("austoscaler configuration not supported with InPlace upgrades")
}
}
return nil
}
func validateOSImageURL(spec *ClusterSpec) error {
dcOSImageURL := spec.DatacenterConfig.Spec.OSImageURL
for _, mc := range spec.MachineConfigs {
if mc.Spec.OSImageURL != "" && dcOSImageURL != "" {
return errors.New("cannot specify OSImageURL on both TinkerbellMachineConfig's and TinkerbellDatacenterConfig")
}
if mc.Spec.OSImageURL == "" && dcOSImageURL == "" && mc.Spec.OSFamily != v1alpha1.Bottlerocket {
return fmt.Errorf("missing OSImageURL on TinkerbellMachineConfig '%s'", mc.ObjectMeta.Name)
}
}
return validateK8sVersionInOSImageURLs(spec)
}
func validateK8sVersionInOSImageURLs(spec *ClusterSpec) error {
// If the user specifies the OSImageURL via the datacenter config then ensure all kube versions specified
// on the cluster config are specified in the OSImageURL as the user could technically use a single image.
//
// When the user specifies OSImageURLs on each individual machine config (typical for modular upgrades) ensure
// each machine config OSImageURL specifies the Kubernetes version. We don't explicitly take into consideration
// the fact control plane, etcd and worker node groups can all reference the same machine config. If 2 components
// specify different kube versions this will ensure both are present in the image URL (as above).
if spec.DatacenterConfig.Spec.OSImageURL != "" {
kvs := spec.Cluster.KubernetesVersions()
for _, v := range kvs {
if !containsK8sVersion(spec.DatacenterConfig.Spec.OSImageURL, string(v)) {
return fmt.Errorf("missing kube version from OSImageURL: url=%v, version=%v",
spec.DatacenterConfig.Spec.OSImageURL, v)
}
}
} else {
// For Bottlerocket we vend images but we still allow the user to specify them if they wish. We only want
// to default machine config OSImageURLs if the datacenter config doesn't specify one and we default
// to whatever is in the bundle.
//
// TODO: Investigate how we could refactor our logic to make this unnecessary.
//
// We validate elsewhere that all machine configs specify the same OSFamily so we can rely on the
// control plane machine config only for the need to default OSImageURLs.
if spec.ControlPlaneMachineConfig().OSFamily() == v1alpha1.Bottlerocket {
defaultBottlerocketOSImageURLs(spec)
}
if !containsK8sVersion(spec.ControlPlaneMachineConfig().Spec.OSImageURL, string(spec.Cluster.Spec.KubernetesVersion)) {
return fmt.Errorf("missing kube version from control plane machine config OSImageURL: url=%v, version=%v",
spec.ControlPlaneMachineConfig().Spec.OSImageURL, spec.Cluster.Spec.KubernetesVersion)
}
for _, wng := range spec.WorkerNodeGroupConfigurations() {
url := spec.MachineConfigs[wng.MachineGroupRef.Name].Spec.OSImageURL
version := spec.Cluster.Spec.KubernetesVersion
if wng.KubernetesVersion != nil && *wng.KubernetesVersion != "" {
version = *wng.KubernetesVersion
}
if !containsK8sVersion(url, string(version)) {
return fmt.Errorf("missing kube version from worker node group machine config OSImageURL: url=%v, version=%v",
url, version)
}
}
}
return nil
}
func defaultBottlerocketOSImageURLs(spec *ClusterSpec) {
if spec.ControlPlaneMachineConfig().Spec.OSImageURL == "" {
spec.ControlPlaneMachineConfig().Spec.OSImageURL = spec.RootVersionsBundle().EksD.Raw.Bottlerocket.URI
}
for _, wng := range spec.WorkerNodeGroupConfigurations() {
mc := spec.MachineConfigs[wng.MachineGroupRef.Name]
version := spec.Cluster.Spec.KubernetesVersion
if wng.KubernetesVersion != nil {
version = *wng.KubernetesVersion
}
if mc.Spec.OSImageURL == "" {
mc.Spec.OSImageURL = spec.VersionsBundle(version).EksD.Raw.Bottlerocket.URI
}
}
}
func containsK8sVersion(imageURL, k8sVersion string) bool {
versionExtractor := strings.NewReplacer("-", "", ".", "", "_", "")
osImageURL := versionExtractor.Replace(imageURL)
kubeVersion := versionExtractor.Replace(k8sVersion)
// we set the containsK8sVersion to false if the OS image URL does not contain the specified kubernetes version.
// For ex if the kubernetes version is 1.23,
// the image url should include 1.23 or 1-23, 1_23 or 123 i.e. ubuntu-1-23.gz or similar in the string.
return strings.Contains(osImageURL, kubeVersion)
}
func validateISOURL(spec *ClusterSpec) error {
if spec.DatacenterConfig.Spec.HookIsoURL != "" {
if path.Ext(spec.DatacenterConfig.Spec.HookIsoURL) != ".iso" {
return fmt.Errorf("incorrect iso url specified: please specify the iso url with '.iso' extension")
}
}
return nil
}
func validateMachineRefExists(
ref *v1alpha1.Ref,
machineConfigs map[string]*v1alpha1.TinkerbellMachineConfig,
) error {
if _, ok := machineConfigs[ref.Name]; !ok {
return fmt.Errorf("missing machine config ref: kind=%v; name=%v", ref.Kind, ref.Name)
}
return nil
}
func validateMachineConfigNamespacesMatchDatacenterConfig(
datacenterConfig *v1alpha1.TinkerbellDatacenterConfig,
machineConfigs map[string]*v1alpha1.TinkerbellMachineConfig,
) error {
for _, machineConfig := range machineConfigs {
if machineConfig.Namespace != datacenterConfig.Namespace {
return fmt.Errorf(
"TinkerbellMachineConfig's namespace must match TinkerbellDatacenterConfig's namespace: %v",
machineConfig.Name,
)
}
}
return nil
}
func validateIPUnused(client networkutils.NetClient, ip string) error {
if networkutils.IsIPInUse(client, ip) {
return fmt.Errorf("ip in use: %v", ip)
}
return nil
}
func validatePortsAvailable(client networkutils.NetClient, host string) error {
unavailablePorts := getPortsUnavailable(client, host)
if len(unavailablePorts) != 0 {
return fmt.Errorf("localhost ports [%v] are already in use, please ensure these ports are available", strings.Join(unavailablePorts, ", "))
}
return nil
}
func getPortsUnavailable(client networkutils.NetClient, host string) []string {
ports := []string{"80", "42113", "50061"}
var unavailablePorts []string
for _, port := range ports {
if networkutils.IsPortInUse(client, host, port) {
unavailablePorts = append(unavailablePorts, port)
}
}
return unavailablePorts
}
// minimumHardwareRequirement defines the minimum requirement for a hardware selector.
type minimumHardwareRequirement struct {
// MinCount is the minimum number of hardware required to satisfy the requirement
MinCount int
// Selector defines what labels should be present on Hardware to consider it eligable for
// this requirement.
Selector v1alpha1.HardwareSelector
// count is used internally by validation to sum the actual available hardware.
count int
}
// MinimumHardwareRequirements is a collection of minimumHardwareRequirement instances.
// it stores requirements in a map where the key is derived from selectors. This ensures selectors
// specifying the same key-value pairs are combined.
type MinimumHardwareRequirements map[string]*minimumHardwareRequirement
// Add a minimumHardwareRequirement to r.
func (r *MinimumHardwareRequirements) Add(selector v1alpha1.HardwareSelector, min int) error {
name, err := selector.ToString()
if err != nil {
return err
}
(*r)[name] = &minimumHardwareRequirement{
MinCount: min,
Selector: selector,
}
return nil
}
// validateMinimumHardwareRequirements validates all requirements can be satisfied using hardware
// registered with catalogue.
func validateMinimumHardwareRequirements(requirements MinimumHardwareRequirements, catalogue *hardware.Catalogue) error {
// Count all hardware that meets the selector requirements for each requirement.
// This does not consider whether or not a piece of hardware is selectable by multiple
// selectors. That requires a different validation ideally run before this one.
for _, h := range catalogue.AllHardware() {
for _, r := range requirements {
if hardware.LabelsMatchSelector(r.Selector, h.Labels) {
r.count++
}
}
}
// Validate counts of hardware meet the minimum required count.
for name, r := range requirements {
if r.count < r.MinCount {
return fmt.Errorf(
"minimum hardware count not met for selector '%v': have %v, require %v",
name,
r.count,
r.MinCount,
)
}
}
return nil
}
// validateHardwareSatisfiesOnlyOneSelector ensures hardware in allHardware meets one and only one
// selector in selectors. selectors uses the selectorSet construct to ensure we don't
// operate on duplicate selectors given a selector can be re-used among groups as they may reference
// the same TinkerbellMachineConfig.
func validateHardwareSatisfiesOnlyOneSelector(allHardware []*tinkv1alpha1.Hardware, selectors selectorSet) error {
for _, h := range allHardware {
if matches := getMatchingHardwareSelectors(h, selectors); len(matches) > 1 {
slctrStrs, err := getHardwareSelectorsAsStrings(matches)
if err != nil {
return err
}
return fmt.Errorf(
"hardware must only satisfy 1 selector: hardware name '%v'; selectors '%v'",
h.Name,
strings.Join(slctrStrs, ", "),
)
}
}
return nil
}
// selectorSet defines a set of selectors. Selectors should be added using the Add method to ensure
// deterministic key generation. The construct is useful to avoid treating selectors that are the
// same as different.
type selectorSet map[string]v1alpha1.HardwareSelector
// Add adds selector to ss.
func (ss *selectorSet) Add(selector v1alpha1.HardwareSelector) error {
slctrStr, err := selector.ToString()
if err != nil {
return err
}
(*ss)[slctrStr] = selector
return nil
}
func getMatchingHardwareSelectors(
hw *tinkv1alpha1.Hardware,
selectors selectorSet,
) []v1alpha1.HardwareSelector {
var satisfies []v1alpha1.HardwareSelector
for _, selector := range selectors {
if hardware.LabelsMatchSelector(selector, hw.Labels) {
satisfies = append(satisfies, selector)
}
}
return satisfies
}
func getHardwareSelectorsAsStrings(selectors []v1alpha1.HardwareSelector) ([]string, error) {
var slctrs []string
for _, selector := range selectors {
s, err := selector.ToString()
if err != nil {
return nil, err
}
slctrs = append(slctrs, s)
}
return slctrs, nil
}