Aberus.AWS.Tools.EKS.KubeConfig.psm1


function Update-EKSKubeConfig {
    <#
        .SYNOPSIS
        Updates the kubeconfig file for an EKS cluster.

        .DESCRIPTION
        This function updates the kubeconfig file for an Amazon EKS cluster, allowing users to interact with the cluster using kubectl.

        .PARAMETER Name
        The name of the EKS cluster to update the kubeconfig for.

        .PARAMETER KubeConfigPath
        The path to the kubeconfig file. Relative paths are resolved against the current location and wildcard characters are not allowed. The path must not be an existing directory and its parent directory must exist. If not specified, defaults to `$HOME\.kube\config`, which is created if missing.

        .PARAMETER RoleArn
        The ARN of an IAM role to assume when authenticating against the cluster. The role is written into the generated kubeconfig, so kubectl assumes it on every call; the cluster itself is still looked up with the credentials given to this cmdlet.

        .PARAMETER Alias
        An alias for the context in the kubeconfig file. If not specified, defaults to the cluster ARN.

        .PARAMETER UserAlias
        An alias for the user in the kubeconfig file. If not specified, defaults to the cluster ARN.

        .PARAMETER Select
        Use the -Select parameter to control the cmdlet output. The default value is '*' which returns the whole result object. Specifying the name of one of its properties, 'Context' or 'Path', will result in that property being returned. Specifying -Select '^ParameterName' will result in the cmdlet returning the selected cmdlet parameter value.

        .PARAMETER Force
        This parameter overrides confirmation prompts to force the cmdlet to continue its operation. This parameter should always be used with caution.

        .PARAMETER ClientConfig
        An Amazon.EKS.AmazonEKSConfig instance used to configure the underlying service client, for example to set a custom timeout or proxy.

        .PARAMETER EndpointUrl
        The endpoint to make the call against.
        <b>Note:</b> This parameter is primarily for internal AWS use and is not required/should not be specified for normal usage. The cmdlets normally determine which endpoint to call based on the region specified to the -Region parameter or set as default in the shell (via Set-DefaultAWSRegion). Only specify this parameter if you must direct the call to a specific custom endpoint.

        .PARAMETER Region
        The system name of an AWS region or an AWSRegion instance. This governs the endpoint that will be used when calling service operations. Note that the AWS resources referenced in a call are usually region-specific.

        .PARAMETER AccessKey
        The AWS access key for the user account. This can be a temporary access key if the corresponding session token is supplied to the -SessionToken parameter.

        .PARAMETER SecretKey
        The AWS secret key for the user account. This can be a temporary secret key if the corresponding session token is supplied to the -SessionToken parameter.

        .PARAMETER SessionToken
        The session token if the access and secret keys are temporary session-based credentials.

        .PARAMETER ProfileName
        The user-defined name of an AWS credentials or SAML-based role profile containing credential information. The profile is expected to be found in the secure credential file shared with the AWS SDK for .NET and AWS Toolkit for Visual Studio. You can also specify the name of a profile stored in the .ini-format credential file used with the AWS CLI and other AWS SDKs.

        .PARAMETER ProfileLocation
        Used to specify the name and location of the ini-format credential file (shared with the AWS CLI and other AWS SDKs)
        If this optional parameter is omitted this cmdlet will search the encrypted credential file used by the AWS SDK for .NET and AWS Toolkit for Visual Studio for the 'default' and 'AWS PS Default' profiles. If the profiles are not found then the cmdlet will search in the ini-format credential file at the default location: (user's home directory)\.aws\credentials.
        If this parameter is specified then this cmdlet will only search the ini-format credential file at the location given.
        As the current folder can vary in a shell or during script execution it is advised that you use specify a fully qualified path instead of a relative path.

        .PARAMETER Credential
        An AWSCredentials object instance containing access and secret key information, and optionally a token for session-based credentials.

        .PARAMETER NetworkCredential
        Used with SAML-based authentication when ProfileName references a SAML role profile. Contains the network credentials to be supplied during authentication with the configured identity provider's endpoint. This parameter is not required if the user's default network identity can or should be used during authentication.

        .EXAMPLE
        Update-EKSKubeConfig -Name my-eks-cluster -KubeConfigPath "C:\path\to\config" -RoleArn "arn:aws:iam::123456789012:role/EKS-Role" -Alias my-cluster-alias -UserAlias my-user-alias -Region us-west-2

        Updated context my-cluster-alias in C:\path\to\config

        Context Path
        ------- ----
        my-cluster-alias C:\path\to\config

        .EXAMPLE
        Update-EKSKubeConfig -Region eu-west-1 -Name my-eks-cluster -ProfileName user1

        Updated context arn:aws:eks:us-west-2:012345678910:cluster/example in /Users/ericn/.kube/config

        Context Path
        ------- ----
        arn:aws:eks:us-west-2:012345678910:cluster/example /Users/ericn/.kube/config

        .LINK
        https://github.com/aberus/aws-eks-kubeconfig-powershell/blob/main/docs/Update-EKSKubeConfig.md

        .LINK
        Get-EKSToken
    #>


    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, Mandatory = $true)]
        [string]$Name,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$KubeConfigPath,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$RoleArn,

        [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $false)]
        [string]$Alias = $null,

        [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $false)]
        [string]$UserAlias = $null,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$Select = '*',

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [switch]$Force,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Amazon.EKS.AmazonEKSConfig]$ClientConfig,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$EndpointUrl,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ArgumentCompleter(
            {
                param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

                $regionHash = @{ }
                $regions = [Amazon.RegionEndpoint]::EnumerableAllRegions
                foreach ($r in $regions) {
                    $regionHash.Add($r.SystemName, $r.DisplayName)
                }

                $regionHash.Keys |
                Sort-Object |
                Where-Object { $_ -like "$wordToComplete*" } |
                ForEach-Object {
                    New-Object System.Management.Automation.CompletionResult $_, $_, 'ParameterValue', $regionHash[$_]
                }
            }
        )]
        [Alias("RegionToCall")]
        [object]$Region,

        [Alias("AK")]
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$AccessKey,

        [Alias("SK", "SecretAccessKey")]
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$SecretKey,

        [Alias("ST")]
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$SessionToken,

        [ArgumentCompleter(
            {
                param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

                # Allow for new user with no profiles set up yet
                $profiles = Get-AWSCredentials -ListProfileDetail | Select-Object -ExpandProperty ProfileName
                if ($profiles) {
                    $profiles |
                    Sort-Object |
                    Where-Object { $_ -like "$wordToComplete*" } |
                    ForEach-Object {
                        New-Object System.Management.Automation.CompletionResult $_, $_, 'ParameterValue', $_
                    }
                }
            }
        )]
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias("StoredCredentials", "AWSProfileName")]
        [string]$ProfileName,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias("AWSProfilesLocation", "ProfilesLocation")]
        [string]$ProfileLocation,

        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Amazon.Runtime.AWSCredentials]$Credential,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Management.Automation.PSCredential]$NetworkCredential
    )

    process {

    ValidateSelectParameter $Select @('Context', 'Path') $MyInvocation.MyCommand.Parameters

    if ($Force) {
        $ConfirmPreference = 'None'
    }

    # Validate and resolve the kubeconfig path before any service call is made. This lands in
    # a local rather than back in $KubeConfigPath: a parameter keeps its last bound value
    # when the next pipeline object does not carry the property.
    if ($KubeConfigPath) {
        $configPath = ResolveKubeConfigPath $KubeConfigPath 'KubeConfigPath'
    }
    else {
        $configPath = Join-Path -Path (Join-Path -Path $HOME -ChildPath '.kube') -ChildPath 'config'
    }

    if (-not $PSCmdlet.ShouldProcess($configPath, 'Update-EKSKubeConfig')) {
        return
    }

    # Build parameter hashtable for Get-EksCluster
    $eksClusterParams = @{
        Name = $Name
    }
    if ($AccessKey) { $eksClusterParams.Add("AccessKey", $AccessKey) }
    if ($SecretKey) { $eksClusterParams.Add("SecretKey", $SecretKey) }
    if ($SessionToken) { $eksClusterParams.Add("SessionToken", $SessionToken) }
    if ($ClientConfig) { $eksClusterParams.Add("ClientConfig", $ClientConfig) }
    if ($Credential) { $eksClusterParams.Add("Credential", $Credential) }
    if ($EndpointUrl) { $eksClusterParams.Add("EndpointUrl", $EndpointUrl) }
    if ($NetworkCredential) { $eksClusterParams.Add("NetworkCredential", $NetworkCredential) }
    if ($ProfileLocation) { $eksClusterParams.Add("ProfileLocation", $ProfileLocation) }
    if ($ProfileName) { $eksClusterParams.Add("ProfileName", $ProfileName) }
    if ($Region) { $eksClusterParams.Add("Region", $Region) }

    # Get EKS Cluster information
    try {
        $eksCluster = Get-EksCluster @eksClusterParams
    }
    catch {
        Write-Error -Exception $_.Exception -Category $_.CategoryInfo.Category -TargetObject $_.TargetObject
        return
    }

    if ($null -eq $eksCluster) {
        Write-Error "EKS Cluster with name '$Name' not found."
        return
    }

    if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) {
        New-Item -ItemType File -Path $configPath -Force | Out-Null
    }

    # Load existing kubeconfig; create new if not exists
    if (ValidateKubeConfigFile $configPath) {
        $kubeConfig = ConvertFrom-Yaml (Get-Content $configPath -Raw) -Ordered

        foreach ($section in 'clusters', 'contexts', 'users') {
            if (-not $kubeConfig.$section) {
                Write-Verbose "The '$section' field is missing in the KubeConfig file."
            }

            # A YAML sequence of one comes back as a bare entry rather than a list, so
            # normalise every section to the List the Update* helpers index into.
            $kubeConfig.$section = AsKubeConfigList $kubeConfig.$section
        }

        if (-not $kubeConfig.'current-context') {
            Write-Verbose "The 'current-context' field is missing in the KubeConfig file."
        }
    }
    else {
        $kubeConfig = [ordered]@{
            apiVersion        = 'v1'
            clusters          = [System.Collections.Generic.List[object]]::new()
            contexts          = [System.Collections.Generic.List[object]]::new() 
            'current-context' = ''
            kind              = 'Config'
            preferences       = @{}
            users             = [System.Collections.Generic.List[object]]::new() 
        }
    }

    if ($Alias) {
        $contextName = $Alias
    }
    else {
        $contextName = $EksCluster.Arn
    }

    if ($UserAlias) {
        $userName = $UserAlias
    }
    else {
        $userName = $EksCluster.Arn
    }

    # Update or add cluster
    UpdateCluster -EksCluster $EksCluster -KubeConfig ([ref]$kubeConfig)

    # Update or add user
    UpdateUser -EksCluster $EksCluster -UserName $userName -KubeConfig ([ref]$kubeConfig) `
        -ProfileName $ProfileName -ProfileLocation $ProfileLocation -RoleArn $RoleArn

    # Update or add context
    UpdateContext -ClusterName $EksCluster.Arn -ContexName $contextName -UserName $userName -KubeConfig ([ref]$kubeConfig)

    # Update the current context (optional)
    $kubeConfig.'current-context' = $contextName

    # Save updated kubeconfig
    $kubeConfig | ConvertTo-Yaml | Set-Content $configPath

    Write-Host "Updated context $contextName in $configPath"

    $result = [PSCustomObject]@{
        Context  = $contextName
        Path     = $configPath
    }

    if ($Select.StartsWith('^')) {
        Get-Variable -Name $Select.Substring(1) -ValueOnly
    }
    else {
        SelectResponseValue $result $Select
    }

    }
}

function ResolveKubeConfigPath([string]$path, [string]$parameterName) {
    if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($path)) {
        throw [System.Management.Automation.PSArgumentException]::new(
            "Wildcard characters are not allowed in $parameterName.", $parameterName)
    }

    $provider = $null
    $drive = $null
    $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path, [ref]$provider, [ref]$drive)

    if ($provider.Name -ne 'FileSystem') {
        throw [System.Management.Automation.PSArgumentException]::new(
            "$parameterName '$path' does not resolve to a file system path.", $parameterName)
    }

    if (Test-Path -LiteralPath $resolvedPath -PathType Container) {
        throw [System.Management.Automation.PSArgumentException]::new(
            "$parameterName '$resolvedPath' is a directory. Provide the path to a file.", $parameterName)
    }

    $parentPath = Split-Path -Path $resolvedPath -Parent
    if ($parentPath -and -not (Test-Path -LiteralPath $parentPath -PathType Container)) {
        throw [System.Management.Automation.PSArgumentException]::new(
            "The directory '$parentPath' specified in $parameterName does not exist.", $parameterName)
    }

    return $resolvedPath
}

function ValidateKubeConfigFile([string]$kubeConfigPath) {
    # Check if the kubeconfig file exists
    if (-not (Test-Path $kubeConfigPath)) {
        Write-Verbose "KubeConfig file does not exist at path: $kubeConfigPath"
        return $false
    }

    # Check if the kubeconfig file is not empty
    if ((Get-Item $kubeConfigPath).Length -eq 0) {
        Write-Verbose "KubeConfig file is empty: $kubeConfigPath"
        return $false
    }

    try {
        # Attempt to parse the kubeconfig file as YAML
        Get-Content $kubeConfigPath -Raw | ConvertFrom-Yaml | Out-Null
    } catch {
        Write-Verbose "Failed to parse KubeConfig file as YAML: $_"
        return $false
    }
    # If all checks pass
    return $true
}

function ValidateSelectParameter([string]$Select, [string[]]$SelectableProperty, [System.Collections.IDictionary]$CommandParameter) {
    # Mirrors the -Select validation of the generated AWS.Tools cmdlets: '*' for the whole
    # result, one of the result's properties, or '^Name' for a parameter the caller passed.
    if ($Select -eq '*') {
        return
    }

    if ($Select.StartsWith('^')) {
        $parameterName = $Select.Substring(1)
        $commonParameter = [System.Management.Automation.PSCmdlet]::CommonParameters +
            [System.Management.Automation.PSCmdlet]::OptionalCommonParameters

        if ($CommandParameter.Keys -contains $parameterName -and $commonParameter -notcontains $parameterName) {
            return
        }
    }
    elseif ($SelectableProperty -contains $Select) {
        return
    }

    throw [System.ArgumentException]::new('Invalid value for -Select parameter.', 'Select')
}

function SelectResponseValue([object]$Response, [string]$Select) {
    if ($Select -eq '*') {
        return $Response
    }

    # -Select takes a property path, so 'Status.Token' walks into the nested member.
    $value = $Response
    foreach ($segment in $Select.Split('.')) {
        if ($null -eq $value) { break }
        $value = $value.$segment
    }

    return $value
}

function QuoteExecArgument([string]$Value) {
    # The exec entry packs every argument into a single -command string, so values that
    # contain spaces - a -ProfileLocation under 'C:\Users\Jane Doe' for instance - have
    # to survive being re-parsed by pwsh.
    return "'" + $Value.Replace("'", "''") + "'"
}

function ResolveAWSCredential([string]$AccessKey, [string]$SecretKey, [string]$SessionToken, [string]$ProfileName, [string]$ProfileLocation, [string]$RoleArn, [Amazon.Runtime.AWSCredentials]$Credential, [System.Management.Automation.PSCredential]$NetworkCredential) {
    # Same precedence the AWS.Tools cmdlets use: an explicit AWSCredentials object, then
    # static keys, then a named profile, then whatever the shell or the SDK chain resolves.
    if ($Credential) {
        $credentials = $Credential
    }
    elseif ($AccessKey -or $SecretKey) {
        if (-not $AccessKey -or -not $SecretKey) {
            throw [System.Management.Automation.PSArgumentException]::new(
                'Both -AccessKey and -SecretKey are needed to use static credentials.', 'AccessKey')
        }

        if ($SessionToken) {
            $credentials = [Amazon.Runtime.SessionAWSCredentials]::new($AccessKey, $SecretKey, $SessionToken)
        }
        else {
            $credentials = [Amazon.Runtime.BasicAWSCredentials]::new($AccessKey, $SecretKey)
        }
    }
    else {
        $lookup = @{ }
        if ($ProfileName) {
            $lookup['ProfileName'] = $ProfileName
            if ($ProfileLocation) { $lookup['ProfileLocation'] = $ProfileLocation }
        }

        # Get-AWSCredential only knows about profiles and the shell defaults, so fall back
        # to the SDK's own chain (environment variables, container and instance metadata).
        $credentials = Get-AWSCredential @lookup
        if (-not $credentials -and -not $ProfileName) {
            $credentials = [Amazon.Runtime.FallbackCredentialsFactory]::GetCredentials()
        }
    }

    if ($NetworkCredential -and $credentials -is [Amazon.Runtime.FederatedAWSCredentials]) {
        # A SAML role profile prompts for the identity provider password unless the caller
        # supplies one, which is what -NetworkCredential is for.
        $networkIdentity = $NetworkCredential.GetNetworkCredential()
        $callback = { param($callbackArgs) $networkIdentity }.GetNewClosure()
        $credentials.Options.CredentialRequestCallback =
            [Func[Amazon.Runtime.CredentialRequestCallbackArgs, System.Net.NetworkCredential]]$callback
    }

    if ($RoleArn -and $credentials) {
        # Same session name the AWS CLI uses for 'aws eks get-token --role-arn', so the
        # calls are recognisable in CloudTrail. Nothing is called until the credentials are
        # actually used, which keeps -RoleArn free until the token is signed.
        $credentials = [Amazon.Runtime.AssumeRoleAWSCredentials]::new($credentials, $RoleArn, 'EKSGetTokenAuth')
    }

    return $credentials
}

function AsKubeConfigList([object]$Value) {
    # ::new() rather than New-Object: on PowerShell 7.6.3 a List[object] built by New-Object
    # makes @() throw 'Argument types do not match', which any caller wrapping a section in
    # @() would hit.
    $list = [System.Collections.Generic.List[object]]::new()
    foreach ($item in @($Value)) {
        if ($null -ne $item) { $list.Add($item) }
    }

    return , $list
}

function UpdateCluster([PSCustomObject]$EksCluster, [ref]$KubeConfig) {
    # Extract cluster details
    $clusterName = $EksCluster.Arn
    $endpoint = $EksCluster.Endpoint
    $certificateAuthorityData = $EksCluster.CertificateAuthority.Data

    # Check if the cluster already exists in kubeconfig
    $clusterIndex = $KubeConfig.Value.clusters.FindIndex({ $args.name -eq $clusterName })

    $clusterEntry = [ordered]@{
        cluster = [ordered]@{
            'certificate-authority-data' = $certificateAuthorityData
            server                       = $endpoint
        }
        name = $clusterName
    }

    if ($clusterIndex -ge 0) {
        # Update existing cluster entry
        $KubeConfig.Value.clusters[$clusterIndex] = $clusterEntry
    }
    else {
        # Add new cluster entry. += would rebuild the List as an Object[], which has no
        # FindIndex, so the next call against the same kubeconfig would throw.
        $KubeConfig.Value.clusters.Add($clusterEntry)
    }
}

function UpdateUser([PSCustomObject]$EksCluster, [ref]$KubeConfig, [string]$ProfileName, [string]$ProfileLocation, [string]$RoleArn, [string]$UserName) {
    $outpostConfig = $EksCluster.OutpostConfig
    $region = $EksCluster.Arn.Split(":")[3]

    if ($outpostConfig) {
        #$clusterIdentificationParameter = "--cluster-id"
        $clusterIdentificationValue = $EksCluster.Id
    }
    else {
        #$clusterIdentificationParameter = "--cluster-name"
        $clusterIdentificationValue = $EksCluster.Name
    }

    # Only the parameters that carry a value are written out: an empty one would leave
    # kubectl running 'Get-EKSToken -ProfileName -Region eu-west-1', which fails to bind.
    # Static credentials are deliberately never written into a kubeconfig file.
    $tokenArguments = [System.Collections.Generic.List[string]]::new()
    $tokenArguments.Add("-ClusterNameOrId $(QuoteExecArgument $clusterIdentificationValue)")
    if ($RoleArn) { $tokenArguments.Add("-RoleArn $(QuoteExecArgument $RoleArn)") }
    if ($ProfileName) { $tokenArguments.Add("-ProfileName $(QuoteExecArgument $ProfileName)") }
    if ($ProfileLocation) { $tokenArguments.Add("-ProfileLocation $(QuoteExecArgument $ProfileLocation)") }
    $tokenArguments.Add("-Region $(QuoteExecArgument $region)")

    # Check if user entry exists and update if necessary
    $userIndex = $KubeConfig.Value.users.FindIndex({ $args.name -eq $UserName })

    # Add new user entry
    $userEntry = [ordered]@{
        name = $userName
        user = [ordered]@{
            exec = [ordered]@{
                apiVersion         = 'client.authentication.k8s.io/v1'
                args               = @(
                    '-command',
                    "&{ &'Get-EKSToken' $($tokenArguments -join ' ')}"
                )
                command            = 'pwsh'
                interactiveMode    = 'IfAvailable'
                provideClusterInfo = $False
            }
        }
    }

    if ($ProfileName) {
        $userEntry.user.exec.env = @(
            [ordered]@{
                name  = 'AWS_PROFILE'
                value = $ProfileName
            }
        )
    }

    if ($userIndex -ge 0) {
        $KubeConfig.Value.users[$userIndex] = $userEntry
    }
    else {
        # Add new user entry
        $KubeConfig.Value.users.Add($userEntry)
    }
}

function UpdateContext([string]$ClusterName, [string]$ContexName, [ref]$KubeConfig, [string]$UserName) {
    # Check if context entry exists and update if necessary
    $contextIndex = $KubeConfig.Value.contexts.FindIndex({ $args.name -eq $ContexName })

    $contextEntry = [ordered]@{
        context = [ordered]@{
            cluster = $ClusterName
            user    = $UserName
        }
        name    = $ContexName
    }

    if ($contextIndex -ge 0) {
        # Update context configuration if needed
        $KubeConfig.Value.contexts[$contextIndex] = $contextEntry
    }
    else {
        # Add new context entry
        $KubeConfig.Value.contexts.Add($contextEntry)
    }
}

function Get-EKSToken {
    <#
        .SYNOPSIS
        Gets a bearer token for authenticating against an EKS cluster.

        .DESCRIPTION
        This function returns the ExecCredential document kubectl expects from an exec credential plugin. The token is a presigned STS GetCallerIdentity URL bound to the cluster, and is the call Update-EKSKubeConfig writes into the user entry of the kubeconfig file.

        .PARAMETER ClusterNameOrId
        The name of the EKS cluster, or its id when the cluster runs on an Outpost.

        .PARAMETER RoleArn
        The ARN of an IAM role to assume before the token is signed. The credentials resolved from the other parameters are used to assume it, and the token then authenticates as the role.

        .PARAMETER Select
        Use the -Select parameter to control the cmdlet output. The default value is '*' which returns the whole ExecCredential document as the JSON kubectl expects. Specifying the name of one of its properties, such as 'Status.Token' or 'Status.ExpirationTimestamp', will result in that property being returned. Specifying -Select '^ParameterName' will result in the cmdlet returning the selected cmdlet parameter value.

        .PARAMETER ClientConfig
        An Amazon.SecurityToken.AmazonSecurityTokenServiceConfig instance used to configure the underlying service client, for example to set a custom timeout or proxy. The token is signed for the resolved region regardless of the region carried by the configuration.

        .PARAMETER EndpointUrl
        The endpoint to make the call against.
        <b>Note:</b> This parameter is primarily for internal AWS use and is not required/should not be specified for normal usage. The cmdlets normally determine which endpoint to call based on the region specified to the -Region parameter or set as default in the shell (via Set-DefaultAWSRegion). Only specify this parameter if you must direct the call to a specific custom endpoint.

        .PARAMETER Region
        The system name of an AWS region or an AWSRegion instance. This governs the endpoint that will be used when calling service operations. Note that the AWS resources referenced in a call are usually region-specific.

        .PARAMETER AccessKey
        The AWS access key for the user account. This can be a temporary access key if the corresponding session token is supplied to the -SessionToken parameter.

        .PARAMETER SecretKey
        The AWS secret key for the user account. This can be a temporary secret key if the corresponding session token is supplied to the -SessionToken parameter.

        .PARAMETER SessionToken
        The session token if the access and secret keys are temporary session-based credentials.

        .PARAMETER ProfileName
        The user-defined name of an AWS credentials or SAML-based role profile containing credential information. The profile is expected to be found in the secure credential file shared with the AWS SDK for .NET and AWS Toolkit for Visual Studio. You can also specify the name of a profile stored in the .ini-format credential file used with the AWS CLI and other AWS SDKs.

        .PARAMETER ProfileLocation
        Used to specify the name and location of the ini-format credential file (shared with the AWS CLI and other AWS SDKs)
        If this optional parameter is omitted this cmdlet will search the encrypted credential file used by the AWS SDK for .NET and AWS Toolkit for Visual Studio for the 'default' and 'AWS PS Default' profiles. If the profiles are not found then the cmdlet will search in the ini-format credential file at the default location: (user's home directory)\.aws\credentials.
        If this parameter is specified then this cmdlet will only search the ini-format credential file at the location given.
        As the current folder can vary in a shell or during script execution it is advised that you use specify a fully qualified path instead of a relative path.

        .PARAMETER Credential
        An AWSCredentials object instance containing access and secret key information, and optionally a token for session-based credentials.

        .PARAMETER NetworkCredential
        Used with SAML-based authentication when ProfileName references a SAML role profile. Contains the network credentials to be supplied during authentication with the configured identity provider's endpoint. This parameter is not required if the user's default network identity can or should be used during authentication.

        .EXAMPLE
        Get-EKSToken -ClusterNameOrId my-eks-cluster -Region us-west-2

        {
          "kind": "ExecCredential",
          "apiVersion": "client.authentication.k8s.io/v1",
          "spec": {},
          "status": {
            "expirationTimestamp": "2024-08-22T12:09:49Z",
            "token": "k8s-aws-v1.aHR0cHM6Ly9zdHMudXMtd2VzdC0yLmFtYXpvbmF3cy5jb20v..."
          }
        }

        .EXAMPLE
        Get-EKSToken -ClusterNameOrId my-eks-cluster -Region us-west-2 -ProfileName user1 -Select Status.Token

        k8s-aws-v1.aHR0cHM6Ly9zdHMudXMtd2VzdC0yLmFtYXpvbmF3cy5jb20v...

        .LINK
        https://github.com/aberus/aws-eks-kubeconfig-powershell/blob/main/docs/Get-EKSToken.md

        .LINK
        Update-EKSKubeConfig
    #>


    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias("ClusterName", "ClusterId")]
        [string]$ClusterNameOrId,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$RoleArn,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$Select = '*',

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Amazon.SecurityToken.AmazonSecurityTokenServiceConfig]$ClientConfig,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$EndpointUrl,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ArgumentCompleter(
            {
                param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

                $regionHash = @{ }
                $regions = [Amazon.RegionEndpoint]::EnumerableAllRegions
                foreach ($r in $regions) {
                    $regionHash.Add($r.SystemName, $r.DisplayName)
                }

                $regionHash.Keys |
                Sort-Object |
                Where-Object { $_ -like "$wordToComplete*" } |
                ForEach-Object {
                    New-Object System.Management.Automation.CompletionResult $_, $_, 'ParameterValue', $regionHash[$_]
                }
            }
        )]
        [Alias("RegionToCall")]
        [object]$Region,

        [Alias("AK")]
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$AccessKey,

        [Alias("SK", "SecretAccessKey")]
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$SecretKey,

        [Alias("ST")]
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$SessionToken,

        [ArgumentCompleter(
            {
                param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

                # Allow for new user with no profiles set up yet
                $profiles = Get-AWSCredentials -ListProfileDetail | Select-Object -ExpandProperty ProfileName
                if ($profiles) {
                    $profiles |
                    Sort-Object |
                    Where-Object { $_ -like "$wordToComplete*" } |
                    ForEach-Object {
                        New-Object System.Management.Automation.CompletionResult $_, $_, 'ParameterValue', $_
                    }
                }
            }
        )]
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias("StoredCredentials", "AWSProfileName")]
        [string]$ProfileName,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias("AWSProfilesLocation", "ProfilesLocation")]
        [string]$ProfileLocation,

        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Amazon.Runtime.AWSCredentials]$Credential,

        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [System.Management.Automation.PSCredential]$NetworkCredential
    )

    ValidateSelectParameter $Select @('Kind', 'ApiVersion', 'Spec', 'Status', 'Status.Token', 'Status.ExpirationTimestamp') `
        $MyInvocation.MyCommand.Parameters

    $regionArgs = New-Object Amazon.PowerShell.Common.StandaloneRegionArguments
    $regionArgs.Region = $Region
    $regionArgs.ProfileLocation = $ProfileLocation

    $regionEndpoint = [Amazon.PowerShell.Common.IAWSRegionArgumentsMethods]::GetRegion($regionArgs, $true, $ExecutionContext.SessionState)
    if (-not $regionEndpoint) {
        Write-Error -Message "No region specified or obtained from persisted/shell defaults." -Category InvalidOperation
        return
    }

    try {
        $credentials = ResolveAWSCredential -AccessKey $AccessKey -SecretKey $SecretKey -SessionToken $SessionToken `
            -ProfileName $ProfileName -ProfileLocation $ProfileLocation -RoleArn $RoleArn `
            -Credential $Credential -NetworkCredential $NetworkCredential
    }
    catch {
        Write-Error -Exception $_.Exception -Category $_.CategoryInfo.Category -TargetObject $_.TargetObject
        return
    }

    if (-not $credentials) {
        Write-Error -Message "No credentials specified or obtained from persisted/shell defaults." -Category InvalidOperation
        return
    }

    # Create the STS client configuration
    if ($ClientConfig) {
        $config = $ClientConfig
    }
    else {
        $config = New-Object Amazon.SecurityToken.AmazonSecurityTokenServiceConfig
    }

    # RegionEndpoint and ServiceURL clear each other on a client configuration, so hold on
    # to the region the request has to be signed for and only touch the one that wins:
    # -EndpointUrl first, then a ServiceURL the caller already put on -ClientConfig.
    $signingRegion = $regionEndpoint.SystemName
    if ($EndpointUrl) {
        $config.ServiceURL = $EndpointUrl
    }
    elseif (-not $config.ServiceURL) {
        $config.RegionEndpoint = $regionEndpoint
    }

    # Create the GetCallerIdentity request
    $getCallerIdentityRequest = [Amazon.SecurityToken.Model.GetCallerIdentityRequest]::new()
    $marshaller = [Amazon.SecurityToken.Model.Internal.MarshallTransformations.GetCallerIdentityRequestMarshaller]::new()
    $request = $marshaller.Marshall($getCallerIdentityRequest)
    # $request = [Amazon.Runtime.Internal.DefaultRequest]::new($getCallerIdentityRequest, $config.AuthenticationServiceName)
    # $request.Parameters.Add("Action", "GetCallerIdentity")
    # $request.Parameters.Add("Version", "2011-06-15")
    $request.UseQueryString = $true
    $request.HttpMethod = "GET"
    $request.Endpoint = [Uri]::new($config.DetermineServiceOperationEndpoint($getCallerIdentityRequest).URL)

    $expirationTime = New-TimeSpan -Seconds 60
    $request.Parameters["X-Amz-Expires"] = [int]$expirationTime.TotalSeconds.ToString([System.Globalization.CultureInfo]::InvariantCulture)

    # Get credentials (assuming credentials is defined)
    $immutableCredentials = $credentials.GetCredentials()
    if ($immutableCredentials.UseToken) {
        $request.Parameters["X-Amz-Security-Token"] = $immutableCredentials.Token
    }

    $request.Headers["x-k8s-aws-id"] = $ClusterNameOrId

    # Sign the request
    $signingResult = [Amazon.Runtime.Internal.Auth.AWS4PreSignedUrlSigner]::SignRequest(
        $request,
        $config,
        [Amazon.Runtime.Internal.Util.RequestMetrics]::new(),
        $immutableCredentials.AccessKey,
        $immutableCredentials.SecretKey,
        $config.AuthenticationServiceName,
        $signingRegion
    )

    # Calculate token expiration
    $tokenExpiration = $signingResult.DateTime.AddMinutes(14)

    # Compose the URL
    $authorization = "&" + $signingResult.ForQueryParameters
    $url = [Amazon.Runtime.AmazonServiceClient]::ComposeUrl($request).AbsoluteUri + $authorization

    $bytes = [System.Text.Encoding]::UTF8.GetBytes($url)
    $encodedText = [Convert]::ToBase64String($bytes)

    $expirationTimestamp = [DateTime]::new($tokenExpiration.Year, $tokenExpiration.Month, $tokenExpiration.Day, $tokenExpiration.Hour, $tokenExpiration.Minute, $tokenExpiration.Second, $tokenExpiration.Kind)

    $execCredential = [PSCustomObject]@{
        kind       = 'ExecCredential'
        apiVersion = 'client.authentication.k8s.io/v1'
        spec       = @{}
        status     = @{
            expirationTimestamp = $expirationTimestamp # "2024-08-22T12:09:49Z"
            token               = 'k8s-aws-v1.' + $encodedText.Replace("=", "")
        }
    }

    if ($Select -eq '*') {
        ConvertTo-Json $execCredential
    }
    elseif ($Select.StartsWith('^')) {
        Get-Variable -Name $Select.Substring(1) -ValueOnly
    }
    else {
        SelectResponseValue $execCredential $Select
    }
}