/*
Copyright 2019 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package nodeup

import (
	"context"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path"
	"strconv"
	"strings"
	"time"

	awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
	"github.com/aws/aws-sdk-go-v2/service/autoscaling"
	ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
	"go.uber.org/multierr"
	"k8s.io/klog/v2"
	"k8s.io/kops/nodeup/pkg/model"
	"k8s.io/kops/nodeup/pkg/model/networking"
	api "k8s.io/kops/pkg/apis/kops"
	kopsmodel "k8s.io/kops/pkg/apis/kops/model"
	"k8s.io/kops/pkg/apis/nodeup"
	"k8s.io/kops/pkg/assets"
	"k8s.io/kops/pkg/bootstrap"
	"k8s.io/kops/pkg/bootstrap/awsbootstrap"
	"k8s.io/kops/pkg/bootstrap/pkibootstrap"
	"k8s.io/kops/pkg/configserver"
	"k8s.io/kops/pkg/kopscontrollerclient"
	"k8s.io/kops/pkg/wellknownports"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/azure/azuremetadata"
	"k8s.io/kops/upup/pkg/fi/cloudup/do/dometadata"
	"k8s.io/kops/upup/pkg/fi/cloudup/gce/tpm/gcetpmsigner"
	"k8s.io/kops/upup/pkg/fi/cloudup/hetzner/hetznermetadata"
	"k8s.io/kops/upup/pkg/fi/cloudup/linode/linodemetadata"
	"k8s.io/kops/upup/pkg/fi/cloudup/openstack/openstackmetadata"
	"k8s.io/kops/upup/pkg/fi/cloudup/scaleway/scalewaymetadata"
	"k8s.io/kops/upup/pkg/fi/nodeup/awsup"
	"k8s.io/kops/upup/pkg/fi/nodeup/local"
	"k8s.io/kops/upup/pkg/fi/nodeup/nodetasks"
	"k8s.io/kops/upup/pkg/fi/secrets"
	"k8s.io/kops/upup/pkg/fi/utils"
	"k8s.io/kops/util/pkg/architectures"
	"k8s.io/kops/util/pkg/distributions"
	"k8s.io/kops/util/pkg/vfs"
)

// MaxTaskDuration is the amount of time to keep trying for; we retry for a long time - there is not really any great fallback
const MaxTaskDuration = 365 * 24 * time.Hour

// NodeUpCommand is the configuration for nodeup
type NodeUpCommand struct {
	CacheDir       string
	ConfigLocation string
	Target         string
}

// Run is responsible for perform the nodeup process
func (c *NodeUpCommand) Run(out io.Writer) error {
	ctx := context.Background()

	var bootConfig nodeup.BootConfig
	if c.ConfigLocation != "" {
		b, err := vfs.Context.ReadFile(c.ConfigLocation)
		if err != nil {
			return fmt.Errorf("error loading configuration %q: %v", c.ConfigLocation, err)
		}

		err = utils.YamlUnmarshal(b, &bootConfig)
		if err != nil {
			return fmt.Errorf("error parsing configuration %q: %v", c.ConfigLocation, err)
		}
	} else {
		return fmt.Errorf("ConfigLocation is required")
	}

	if c.CacheDir == "" {
		return fmt.Errorf("CacheDir is required")
	}

	region, err := getRegion(ctx, &bootConfig)
	if err != nil {
		return err
	}

	var configBase vfs.Path

	// If we're using a config server instead of vfs, nodeConfig will hold our configuration
	var nodeConfig *nodeup.NodeConfig

	if bootConfig.ConfigServer != nil && len(bootConfig.ConfigServer.Servers) > 0 {
		response, err := getNodeConfigFromServers(ctx, &bootConfig, region)
		if err != nil {
			return fmt.Errorf("failed to get node config from server: %w", err)
		}
		nodeConfig = response.NodeConfig
	} else if fi.ValueOf(bootConfig.ConfigBase) != "" {
		var err error
		configBase, err = vfs.Context.BuildVfsPath(*bootConfig.ConfigBase)
		if err != nil {
			return fmt.Errorf("cannot parse ConfigBase %q: %v", *bootConfig.ConfigBase, err)
		}
	} else {
		return fmt.Errorf("ConfigBase or ConfigServer is required")
	}

	var nodeupConfig nodeup.Config
	var nodeupConfigHash [32]byte
	switch {
	case nodeConfig != nil:
		if err := utils.YamlUnmarshal([]byte(nodeConfig.NodeupConfig), &nodeupConfig); err != nil {
			return fmt.Errorf("error parsing BootConfig config response: %v", err)
		}
		nodeupConfigHash = sha256.Sum256([]byte(nodeConfig.NodeupConfig))
		if nodeupConfig.CAs == nil {
			nodeupConfig.CAs = make(map[string]string)
		}
		nodeupConfig.CAs[fi.CertificateIDCA] = bootConfig.ConfigServer.CACertificates
	case bootConfig.InstanceGroupName != "":
		nodeupConfigLocation := configBase.Join("igconfig", bootConfig.InstanceGroupRole.ToLowerString(), bootConfig.InstanceGroupName, "nodeupconfig.yaml")

		b, err := nodeupConfigLocation.ReadFile(ctx)
		if err != nil {
			return fmt.Errorf("error loading NodeupConfig %q: %v", nodeupConfigLocation, err)
		}

		if err = utils.YamlUnmarshal(b, &nodeupConfig); err != nil {
			return fmt.Errorf("error parsing NodeupConfig %q: %v", nodeupConfigLocation, err)
		}
		nodeupConfigHash = sha256.Sum256(b)
	default:
		return fmt.Errorf("no instance group defined in nodeup config")
	}

	if bootConfig.NodeupConfigHash != "" {
		if want, got := bootConfig.NodeupConfigHash, base64.StdEncoding.EncodeToString(nodeupConfigHash[:]); got != want {
			return fmt.Errorf("nodeup config hash mismatch (was %q, expected %q)", got, want)
		}
	}

	err = evaluateSpec(&nodeupConfig, bootConfig.CloudProvider, region)
	if err != nil {
		return err
	}

	architecture, err := architectures.FindArchitecture()
	if err != nil {
		return fmt.Errorf("error determining OS architecture: %v", err)
	}

	distribution, err := distributions.FindDistribution("/")
	if err != nil {
		return fmt.Errorf("error determining OS distribution: %v", err)
	}

	configAssets := nodeupConfig.Assets[architecture]
	assetStore := fi.NewAssetStore(c.CacheDir)
	for _, asset := range configAssets {
		err := assetStore.Add(ctx, asset)
		if err != nil {
			return fmt.Errorf("error adding asset %q: %v", asset, err)
		}
	}

	// cloud holds the AWS clients, on AWS only.
	var cloud *awsup.Cloud

	if bootConfig.CloudProvider == api.CloudProviderAWS {
		cloud, err = awsup.NewCloud(ctx, region)
		if err != nil {
			return err
		}
	}

	modelContext := &model.NodeupModelContext{
		Cloud:        cloud,
		Architecture: architecture,
		Assets:       assetStore,
		ConfigBase:   configBase,
		Distribution: distribution,
		BootConfig:   &bootConfig,
		NodeupConfig: &nodeupConfig,
	}

	var secretStore fi.SecretStoreReader
	var keyStore fi.KeystoreReader
	switch {
	case nodeConfig != nil:
		modelContext.SecretStore = configserver.NewSecretStore(nodeConfig.NodeSecrets)
	case nodeupConfig.ConfigStore != nil && nodeupConfig.ConfigStore.Secrets != "":
		klog.Infof("Building SecretStore at %q", nodeupConfig.ConfigStore.Secrets)
		p, err := vfs.Context.BuildVfsPath(nodeupConfig.ConfigStore.Secrets)
		if err != nil {
			return fmt.Errorf("error building secret store path: %v", err)
		}

		secretStore = secrets.NewVFSSecretStoreReader(p)
		modelContext.SecretStore = secretStore
	default:
		return fmt.Errorf("SecretStore not set")
	}

	if nodeConfig != nil {
		modelContext.KeyStore = configserver.NewKeyStore()
	} else if nodeupConfig.ConfigStore.Keypairs != "" {
		klog.Infof("Building KeyStore at %q", nodeupConfig.ConfigStore.Keypairs)
		p, err := vfs.Context.BuildVfsPath(nodeupConfig.ConfigStore.Keypairs)
		if err != nil {
			return fmt.Errorf("error building key store path: %v", err)
		}

		modelContext.KeyStore = fi.NewVFSKeystoreReader(p)
		keyStore = modelContext.KeyStore
	} else {
		return fmt.Errorf("KeyStore not set")
	}

	if err := modelContext.Init(); err != nil {
		return err
	}

	switch bootConfig.CloudProvider {
	case api.CloudProviderAWS:
		instanceIDBytes, err := vfs.Context.ReadFile("metadata://aws/meta-data/instance-id")
		if err != nil {
			return fmt.Errorf("error reading instance-id from AWS metadata: %v", err)
		}
		modelContext.InstanceID = string(instanceIDBytes)

		// Check if WarmPool is enabled first, to avoid additional API calls
		if len(modelContext.NodeupConfig.WarmPoolImages) > 0 {
			modelContext.ConfigurationMode, err = getAWSConfigurationMode(ctx, modelContext)
			if err != nil {
				return err
			}
		}

		modelContext.MachineType, err = getMachineType(ctx)
		if err != nil {
			return fmt.Errorf("failed to get machine type: %w", err)
		}

		// If Nvidia is enabled in the cluster, check if this instance has support for it.
		nvidia := modelContext.NodeupConfig.ContainerdConfig.NvidiaGPU
		if nvidia != nil && fi.ValueOf(nvidia.Enabled) {
			// Get the instance type's detailed information.
			instanceType, err := cloud.GetMachineTypeInfo(ctx, ec2types.InstanceType(modelContext.MachineType))
			if err != nil {
				return err
			}

			if instanceType.GPU {
				klog.Info("instance supports GPU acceleration")
				modelContext.GPUVendor = architectures.GPUVendorNvidia
			}
		}
	case api.CloudProviderOpenstack:
		// NvidiaGPU possible to enable only in instance group level in OpenStack. When we assume that GPU is supported
		if nodeupConfig.NvidiaGPU != nil && fi.ValueOf(nodeupConfig.NvidiaGPU.Enabled) {
			klog.Info("instance supports GPU acceleration")
			modelContext.GPUVendor = architectures.GPUVendorNvidia
		}
	}

	if err := loadKernelModules(modelContext, distribution); err != nil {
		return err
	}

	loader := &Loader{}
	loader.Builders = append(loader.Builders, &model.DiscoveryService{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.EtcHostsBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.NTPBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.DirectoryBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.UpdateServiceBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.VolumesBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.ContainerdBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.ChannelsBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.CloudConfigBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.FileAssetsBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.HookBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.KubeletBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.KubectlBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.LogrotateBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.ManifestsBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.PackagesBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.NvidiaBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.GVisorBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.SecretBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.FirewallBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.SysctlBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.KubeAPIServerBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.KubeControllerManagerBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.KubeSchedulerBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.EtcdManagerTLSBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.KubeProxyBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.KopsControllerBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.WarmPoolBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.PrefixBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.NerdctlBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.CrictlBuilder{NodeupModelContext: modelContext})
	// Cloud-specific configuration
	loader.Builders = append(loader.Builders, &model.AzureBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &model.LinodeBuilder{NodeupModelContext: modelContext})

	loader.Builders = append(loader.Builders, &networking.CommonBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &networking.CalicoBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &networking.CiliumBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &networking.KindnetBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &networking.AmazonVPCRoutedENIBuilder{NodeupModelContext: modelContext})
	loader.Builders = append(loader.Builders, &networking.KuberouterBuilder{NodeupModelContext: modelContext})

	loader.Builders = append(loader.Builders, &model.BootstrapClientBuilder{NodeupModelContext: modelContext})
	taskMap, err := loader.Build()
	if err != nil {
		return fmt.Errorf("error building loader: %v", err)
	}

	for _, image := range nodeupConfig.Images[architecture] {
		if len(image.Sources) == 0 {
			return fmt.Errorf("image has no sources: %v", image)
		}
		u, err := url.Parse(image.Sources[0])
		if err != nil {
			return fmt.Errorf("invalid image source URL %q: %w", image.Sources[0], err)
		}
		key := "SideloadImage/" + path.Base(u.Path)
		if _, ok := taskMap[key]; ok {
			return fmt.Errorf("duplicate image task %q", key)
		}
		taskMap[key] = &nodetasks.LoadImageTask{
			Sources: image.Sources,
			Hash:    image.Hash,
		}
	}

	var target fi.NodeupTarget

	switch c.Target {
	case "direct":
		target = &local.LocalTarget{
			CacheDir: c.CacheDir,
			Cloud:    cloud,
		}
	case "dryrun":
		assetBuilder := assets.NewAssetBuilder(vfs.Context, nil, false)
		target = fi.NewNodeupDryRunTarget(assetBuilder, out)
	default:
		return fmt.Errorf("unsupported target type %q", c.Target)
	}

	context, err := fi.NewNodeupContext(ctx, target, keyStore, &bootConfig, &nodeupConfig, taskMap)
	if err != nil {
		klog.Exitf("error building context: %v", err)
	}

	var options fi.RunTasksOptions
	options.InitDefaults()

	err = context.RunTasks(options)
	if err != nil {
		klog.Exitf("error running tasks: %v", err)
	}

	err = target.Finish(taskMap)
	if err != nil {
		klog.Exitf("error closing target: %v", err)
	}

	if nodeupConfig.EnableLifecycleHook {
		if bootConfig.CloudProvider == api.CloudProviderAWS {
			err := completeWarmingLifecycleAction(ctx, cloud, modelContext)
			if err != nil {
				return fmt.Errorf("failed to complete lifecylce action: %w", err)
			}
		}
	}
	return nil
}

func getMachineType(ctx context.Context) (string, error) {
	config, err := awsconfig.LoadDefaultConfig(ctx)
	if err != nil {
		return "", fmt.Errorf("failed to load AWS config: %w", err)
	}

	metadata := imds.NewFromConfig(config)

	// Get the actual instance type by querying the EC2 instance metadata service.
	result, err := metadata.GetMetadata(ctx, &imds.GetMetadataInput{
		Path: "instance-type",
	})
	if err != nil {
		return "", fmt.Errorf("failed to get instance metadata type: %w", err)
	}
	defer result.Content.Close()
	instanceTypeName, err := io.ReadAll(result.Content)
	if err != nil {
		return "", fmt.Errorf("failed to read instance metadata response: %w", err)
	}
	return string(instanceTypeName), err
}

func completeWarmingLifecycleAction(ctx context.Context, cloud *awsup.Cloud, modelContext *model.NodeupModelContext) error {
	asgName := modelContext.BootConfig.InstanceGroupName + "." + modelContext.NodeupConfig.ClusterName
	hookName := "kops-warmpool"
	hooks, err := cloud.DescribeLifecycleHooks(ctx, &autoscaling.DescribeLifecycleHooksInput{
		AutoScalingGroupName: &asgName,
		LifecycleHookNames:   []string{hookName},
	})
	if err != nil {
		return fmt.Errorf("failed to find lifecycle hook %q: %w", hookName, err)
	}

	if len(hooks.LifecycleHooks) > 0 {
		klog.Info("Found ASG lifecycle hook")
		_, err := cloud.CompleteLifecycleAction(ctx, &autoscaling.CompleteLifecycleActionInput{
			AutoScalingGroupName:  &asgName,
			InstanceId:            &modelContext.InstanceID,
			LifecycleHookName:     &hookName,
			LifecycleActionResult: new("CONTINUE"),
		})
		if err != nil {
			return fmt.Errorf("failed to complete lifecycle hook %q for %q: %v", hookName, modelContext.InstanceID, err)
		}
		klog.Info("Lifecycle action completed")
	} else {
		klog.Info("No ASG lifecycle hook found")
	}
	return nil
}

func evaluateSpec(nodeupConfig *nodeup.Config, cloudProvider api.CloudProviderID, region string) error {
	hostnameOverride, err := evaluateHostnameOverride(cloudProvider, nodeupConfig.UseIPBasedNodeNames, region)
	if err != nil {
		return err
	}

	nodeupConfig.KubeletConfig.HostnameOverride = hostnameOverride

	if nodeupConfig.KubeProxy != nil {
		nodeupConfig.KubeProxy.HostnameOverride = hostnameOverride
		nodeupConfig.KubeProxy.BindAddress, err = evaluateBindAddress(nodeupConfig.KubeProxy.BindAddress)
		if err != nil {
			return err
		}
	}

	return nil
}

func evaluateHostnameOverride(cloudProvider api.CloudProviderID, useIPBasedNodeNames bool, region string) (string, error) {
	switch cloudProvider {
	case api.CloudProviderAWS:
		instanceIDBytes, err := vfs.Context.ReadFile("metadata://aws/meta-data/instance-id")
		if err != nil {
			return "", fmt.Errorf("error reading instance-id from AWS metadata: %v", err)
		}
		instanceID := string(instanceIDBytes)

		if !useIPBasedNodeNames {
			return instanceID, nil
		}

		// The node name is the DNS name that EC2 generates for IP-named instances, built from the
		// primary private IPv4 address. kops-controller derives it with the same formula when
		// issuing certificates, so the two always agree. IMDS local-hostname is not usable for
		// this: with a custom DHCP domain it differs from the generated name.
		//
		// An instance launched with a resource-based hostname keeps a resource-based name (it can
		// only change while the instance is stopped), so an IP-based node name would not match its
		// EC2 hostname; fail rather than join a misconfigured instance.
		hostnameBytes, err := vfs.Context.ReadFile("metadata://aws/meta-data/local-hostname")
		if err != nil {
			return "", fmt.Errorf("error reading local-hostname from AWS metadata: %v", err)
		}
		if strings.HasPrefix(string(hostnameBytes), instanceID) {
			return "", fmt.Errorf("instance %s was launched with a resource-based hostname; useIPBasedNodeNames requires subnets that assign IP-based hostnames", instanceID)
		}

		localIPv4Bytes, err := vfs.Context.ReadFile("metadata://aws/meta-data/local-ipv4")
		if err != nil {
			return "", fmt.Errorf("error reading local-ipv4 from AWS metadata: %v", err)
		}
		localIPv4 := string(localIPv4Bytes)
		if net.ParseIP(localIPv4).To4() == nil {
			return "", fmt.Errorf("local-ipv4 from AWS metadata is not a valid IPv4 address: %q", localIPv4)
		}

		return awsbootstrap.PrivateDNSName(localIPv4, region), nil

	case api.CloudProviderGCE:
		// This lets us tolerate broken hostnames (i.e. systemd)
		b, err := vfs.Context.ReadFile("metadata://gce/instance/hostname")
		if err != nil {
			return "", fmt.Errorf("error reading hostname from GCE metadata: %v", err)
		}

		// We only want to use the first portion of the fully-qualified name
		// e.g. foo.c.project.internal => foo
		fullyQualified := string(b)
		bareHostname := strings.Split(fullyQualified, ".")[0]
		return bareHostname, nil
	case api.CloudProviderDO:
		vBytes, err := vfs.Context.ReadFile("metadata://digitalocean/interfaces/private/0/ipv4/address")
		if err != nil {
			return "", fmt.Errorf("error reading droplet private IP from DigitalOcean metadata: %v", err)
		}

		hostname := string(vBytes)
		if hostname == "" {
			return "", errors.New("private IP for digitalocean droplet was empty")
		}

		return hostname, nil
	case api.CloudProviderLinode:
		label, err := linodemetadata.GetMetadataValue(context.TODO(), "label")
		if err != nil {
			return "", fmt.Errorf("error reading hostname from Linode metadata: %v", err)
		}

		// Linode cloud-init does not set the OS hostname.
		// Set it here so the OS hostname matches the kubelet hostname override.
		if err := os.WriteFile("/etc/hostname", []byte(label+"\n"), 0o644); err != nil { //nolint:gosec // /etc/hostname is conventionally world-readable system configuration.
			klog.Warningf("Failed to write /etc/hostname: %v", err)
		} else if err := exec.Command("hostname", label).Run(); err != nil {
			klog.Warningf("Failed to set runtime hostname %q: %v", label, err)
		} else {
			klog.Infof("Set OS hostname to %q", label)
		}

		return label, nil
	}

	return "", nil
}

func evaluateBindAddress(bindAddress string) (string, error) {
	if bindAddress == "" {
		return "", nil
	}
	if bindAddress == "@aws" {
		vBytes, err := vfs.Context.ReadFile("metadata://aws/meta-data/local-ipv4")
		if err != nil {
			return "", fmt.Errorf("error reading local IP from AWS metadata: %v", err)
		}

		// The local-ipv4 gets it's IP from the AWS.
		// For now just choose the first one.
		ips := strings.Fields(string(vBytes))
		if len(ips) == 0 {
			klog.Warningf("Local IP from AWS metadata service was empty")
			return "", nil
		}

		ip := ips[0]
		klog.Infof("Using IP from AWS metadata service: %s", ip)

		return ip, nil
	}

	if net.ParseIP(bindAddress) == nil {
		return "", fmt.Errorf("bindAddress is not valid IP address")
	}
	return bindAddress, nil
}

// modprobe will exec `modprobe <module>`
func modprobe(module string) error {
	klog.Infof("Doing modprobe for module %v", module)
	out, err := exec.Command("/sbin/modprobe", module).CombinedOutput()
	outString := string(out)
	if err != nil {
		return fmt.Errorf("modprobe for module %q failed (%v): %s", module, err, outString)
	}
	if outString != "" {
		klog.Infof("Output from modprobe %s:\n%s", module, outString)
	}
	return nil
}

// loadKernelModules is a hack to force br_netfilter to be loaded
// and used by some components to load its recommended modules.
// TODO: Move to tasks architecture
func loadKernelModules(context *model.NodeupModelContext, distribution distributions.Distribution) error {
	if context.NodeupConfig.Networking.Kindnet != nil {
		err := modprobe("nfnetlink_queue")
		if err != nil {
			klog.Warningf("error loading nfnetlink_queue module: %v", err)
		}
	} else {
		err := modprobe("br_netfilter")
		if err != nil {
			// TODO: Return error in 1.11 (too risky for 1.10)
			klog.Warningf("error loading br_netfilter module: %v", err)
		}
	}
	if distribution.ForceNftables() {
		// Distributions like RHEL10+ use nftables exclusively.
		// - nf_tables / nf_conntrack: required by CNI plugins that shell out
		//   to iptables-nft.
		// - ip_set: Calico's Felix unconditionally starts an `ipsetsManager`
		//   that shells out to `ipset list -name` during dataplane resync,
		//   even when NFTablesMode=Enabled. On RHEL10 family kernels the
		//   ip_set module isn't auto-loaded, so the ipset call returns
		//   EINVAL and Felix panics, crashlooping calico-node.
		for _, mod := range []string{"nf_tables", "nf_conntrack", "ip_set"} {
			if err := modprobe(mod); err != nil {
				klog.Warningf("error loading %s module: %v", mod, err)
			}
		}
	} else {
		switch distribution {
		case distributions.DistributionRocky9:
			// Rocky 9 doesn't load nf_conntrack by default, and it's required for kube-proxy:
			// "Error running ProxyServer" err="open /proc/sys/net/netfilter/nf_conntrack_max: no such file or directory"
			// "command failed" err="open /proc/sys/net/netfilter/nf_conntrack_max: no such file or directory"
			err := modprobe("nf_conntrack")
			if err != nil {
				klog.Warningf("error loading nf_conntrack module: %v", err)
			}
		}
	}
	// TODO: Add to /etc/modules-load.d/ ?
	return nil
}

// getRegion queries the cloud provider for the region.
func getRegion(ctx context.Context, bootConfig *nodeup.BootConfig) (string, error) {
	switch bootConfig.CloudProvider {
	case api.CloudProviderAWS:
		region, err := awsbootstrap.RegionFromMetadata(ctx)
		if err != nil {
			return "", err
		}

		return region, nil
	}

	return "", nil
}

// getNodeConfigFromServers queries kops-controllers for our node's configuration.
func getNodeConfigFromServers(ctx context.Context, bootConfig *nodeup.BootConfig, region string) (*nodeup.BootstrapResponse, error) {
	var authenticator bootstrap.Authenticator

	switch bootConfig.CloudProvider {
	case api.CloudProviderAWS:
		a, err := awsbootstrap.NewAWSAuthenticator(ctx, region)
		if err != nil {
			return nil, err
		}
		authenticator = a
	case api.CloudProviderGCE:
		a, err := gcetpmsigner.NewTPMAuthenticator()
		if err != nil {
			return nil, err
		}
		authenticator = a
	case api.CloudProviderHetzner:
		a, err := hetznermetadata.NewHetznerAuthenticator()
		if err != nil {
			return nil, err
		}
		authenticator = a
	case api.CloudProviderOpenstack:
		a, err := openstackmetadata.NewOpenstackAuthenticator()
		if err != nil {
			return nil, err
		}
		authenticator = a
	case api.CloudProviderDO:
		a, err := dometadata.NewAuthenticator()
		if err != nil {
			return nil, err
		}
		authenticator = a
	case api.CloudProviderScaleway:
		a, err := scalewaymetadata.NewScalewayAuthenticator()
		if err != nil {
			return nil, err
		}
		authenticator = a
	case api.CloudProviderAzure:
		a, err := azuremetadata.NewAzureAuthenticator()
		if err != nil {
			return nil, err
		}
		authenticator = a

	case "metal":
		a, err := pkibootstrap.NewAuthenticatorFromFile("/etc/kubernetes/kops/pki/machine/private.pem")
		if err != nil {
			return nil, err
		}
		authenticator = a
	case api.CloudProviderLinode:
		a, err := linodemetadata.NewLinodeAuthenticator()
		if err != nil {
			return nil, err
		}
		authenticator = a

	default:
		return nil, fmt.Errorf("unsupported cloud provider for node configuration %s", bootConfig.CloudProvider)
	}

	var challengeListener *bootstrap.ChallengeListener
	if kopsmodel.UseChallengeCallback(bootConfig.CloudProvider) {
		challengeServer, err := bootstrap.NewChallengeServer(bootConfig.ClusterName, []byte(bootConfig.ConfigServer.CACertificates))
		if err != nil {
			return nil, err
		}
		listen := ":" + strconv.Itoa(wellknownports.NodeupChallenge)

		l, err := challengeServer.NewListener(ctx, listen)
		if err != nil {
			return nil, fmt.Errorf("error starting challenge listener: %w", err)
		}
		challengeListener = l
		defer challengeListener.Stop()
	}

	// Note: The url is overridden in every iteration of the loop below.
	client := kopscontrollerclient.NewWithTLSServerName(authenticator, []byte(bootConfig.ConfigServer.CACertificates), url.URL{}, bootConfig.ConfigServer.TLSServerName)
	defer client.Close()

	var merr error
	for _, server := range bootConfig.ConfigServer.Servers {
		u, err := url.Parse(server)
		if err != nil {
			merr = multierr.Append(merr, fmt.Errorf("unable to parse configuration server url %q: %w", server, err))
			continue
		}
		client.BaseURL = *u

		request := nodeup.BootstrapRequest{
			APIVersion:        nodeup.BootstrapAPIVersion,
			IncludeNodeConfig: true,
		}

		if challengeListener != nil {
			request.Challenge = challengeListener.CreateChallenge()
		}

		var resp nodeup.BootstrapResponse
		err = client.Query(ctx, &request, &resp)
		if err != nil {
			merr = multierr.Append(merr, err)
			continue
		}
		return &resp, nil
	}
	return nil, merr
}

func getAWSConfigurationMode(ctx context.Context, c *model.NodeupModelContext) (string, error) {
	// Check if WarmPool is enabled first, to avoid additional API calls
	if len(c.NodeupConfig.WarmPoolImages) == 0 {
		return "", nil
	}

	// Only worker nodes and apiservers can actually autoscale.
	// We are not adding describe permissions to the other roles
	role := c.BootConfig.InstanceGroupRole
	if !role.HasNode() && !role.HasAPIServer() {
		return "", nil
	}

	targetLifecycleState, err := vfs.Context.ReadFile("metadata://aws/meta-data/autoscaling/target-lifecycle-state")
	if err != nil {
		var awsErr *awshttp.ResponseError
		if errors.As(err, &awsErr) && awsErr.HTTPStatusCode() == http.StatusNotFound {
			return "", nil
		}
		return "", fmt.Errorf("error reading target-lifecycle-state from instance metadata: %v", err)
	}

	if strings.HasPrefix(string(targetLifecycleState), "Warmed:") {
		klog.Info("instance is entering warm pool")
		return model.ConfigurationModeWarming, nil
	} else {
		klog.Info("instance is entering the ASG")
		return "", nil
	}
}
