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 }