Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add bottlerocket ami types #2352

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions cmd/aws/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,20 @@ var (
nodeCountFlag string
installCatalogApps string
installKubefirstProFlag bool
amiType string

// Supported argument arrays
supportedDNSProviders = []string{"aws", "cloudflare"}
supportedGitProviders = []string{"github", "gitlab"}
supportedGitProtocolOverride = []string{"https", "ssh"}
supportedAMITypes = map[string]string{
"AL2_x86_64": "/aws/service/eks/optimized-ami/1.29/amazon-linux-2/recommended/image_id",
"AL2_ARM_64": "/aws/service/eks/optimized-ami/1.29/amazon-linux-2-arm64/recommended/image_id",
"BOTTLEROCKET_ARM_64": "/aws/service/bottlerocket/aws-k8s-1.29/arm64/latest/image_id",
"BOTTLEROCKET_x86_64": "/aws/service/bottlerocket/aws-k8s-1.29/x86_64/latest/image_id",
"BOTTLEROCKET_ARM_64_NVIDIA": "/aws/service/bottlerocket/aws-k8s-1.29-nvidia/arm64/latest/image_id",
"BOTTLEROCKET_x86_64_NVIDIA": "/aws/service/bottlerocket/aws-k8s-1.29-nvidia/x86_64/latest/image_id",
}
)

func NewCommand() *cobra.Command {
Expand Down Expand Up @@ -99,10 +108,19 @@ func Create() *cobra.Command {
createCmd.Flags().BoolVar(&useTelemetryFlag, "use-telemetry", true, "whether to emit telemetry")
createCmd.Flags().BoolVar(&ecrFlag, "ecr", false, "whether or not to use ecr vs the git provider")
createCmd.Flags().BoolVar(&installKubefirstProFlag, "install-kubefirst-pro", true, "whether or not to install kubefirst pro")
createCmd.Flags().StringVar(&amiType, "ami-type", "AL2_x86_64", fmt.Sprintf("the ami type for node group - one of: %q", getSupportedAMITypes()))

return createCmd
}

func getSupportedAMITypes() []string {
amiTypes := make([]string, 0, len(supportedAMITypes))
for k := range supportedAMITypes {
amiTypes = append(amiTypes, k)
}
return amiTypes
}

func Destroy() *cobra.Command {
destroyCmd := &cobra.Command{
Use: "destroy",
Expand Down
145 changes: 126 additions & 19 deletions cmd/aws/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ See the LICENSE file for more details.
package aws

import (
"context"
"fmt"
"os"
"slices"
"strings"

"github.com/aws/aws-sdk-go/aws"
awsinternal "github.com/konstructio/kubefirst-api/pkg/aws"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/ssm"
internalssh "github.com/konstructio/kubefirst-api/pkg/ssh"
pkg "github.com/konstructio/kubefirst-api/pkg/utils"
"github.com/konstructio/kubefirst/internal/catalog"
Expand Down Expand Up @@ -41,7 +46,14 @@ func createAws(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("invalid catalog apps: %w", err)
}

err = ValidateProvidedFlags(cliFlags.GitProvider)
ctx := context.Background()

cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(cliFlags.CloudRegion))
if err != nil {
return fmt.Errorf("unable to load AWS SDK config: %w", err)
}

err = ValidateProvidedFlags(ctx, cfg, cliFlags.GitProvider, cliFlags.AMIType, cliFlags.NodeType)
if err != nil {
progress.Error(err.Error())
return fmt.Errorf("failed to validate provided flags: %w", err)
Expand All @@ -57,15 +69,7 @@ func createAws(cmd *cobra.Command, _ []string) error {
return nil
}

// Validate aws region
config, err := awsinternal.NewAwsV2(cloudRegionFlag)
if err != nil {
progress.Error(err.Error())
return fmt.Errorf("failed to validate AWS region: %w", err)
}

awsClient := &awsinternal.Configuration{Config: config}
creds, err := awsClient.Config.Credentials.Retrieve(aws.BackgroundContext())
creds, err := getSessionCredentials(ctx, cfg.Credentials)
if err != nil {
progress.Error(err.Error())
return fmt.Errorf("failed to retrieve AWS credentials: %w", err)
Expand All @@ -78,12 +82,6 @@ func createAws(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("failed to write config: %w", err)
}

_, err = awsClient.CheckAvailabilityZones(cliFlags.CloudRegion)
if err != nil {
progress.Error(err.Error())
return fmt.Errorf("failed to check availability zones: %w", err)
}

gitAuth, err := gitShim.ValidateGitCredentials(cliFlags.GitProvider, cliFlags.GithubOrg, cliFlags.GitlabGroup)
if err != nil {
progress.Error(err.Error())
Expand Down Expand Up @@ -136,7 +134,7 @@ func createAws(cmd *cobra.Command, _ []string) error {
return nil
}

func ValidateProvidedFlags(gitProvider string) error {
func ValidateProvidedFlags(ctx context.Context, cfg aws.Config, gitProvider, amiType, nodeType string) error {
progress.AddStep("Validate provided flags")

// Validate required environment variables for dns provider
Expand All @@ -161,7 +159,116 @@ func ValidateProvidedFlags(gitProvider string) error {
log.Info().Msgf("%q %s", "gitlab.com", key.Type())
}

ssmClient := ssm.NewFromConfig(cfg)
ec2Client := ec2.NewFromConfig(cfg)
paginator := ec2.NewDescribeInstanceTypesPaginator(ec2Client, &ec2.DescribeInstanceTypesInput{})

if err := validateAMIType(ctx, amiType, nodeType, ssmClient, ec2Client, paginator); err != nil {
progress.Error(err.Error())
return fmt.Errorf("failed to validate ami type for node group: %w", err)
}

progress.CompleteStep("Validate provided flags")

return nil
}

func getSessionCredentials(ctx context.Context, cfg aws.CredentialsProvider) (*aws.Credentials, error) {
// Retrieve credentials
creds, err := cfg.Retrieve(ctx)
if err != nil {
return nil, fmt.Errorf("failed to retrieve AWS credentials: %w", err)
}

return &creds, nil
}

func validateAMIType(ctx context.Context, amiType, nodeType string, ssmClient ssmClienter, ec2Client ec2Clienter, paginator paginator) error {
ssmParameterName, ok := supportedAMITypes[amiType]
if !ok {
return fmt.Errorf("not a valid ami type: %q", amiType)
}

log.Info().Msgf("ami type is %s", amiType)

amiID, err := getLatestAMIFromSSM(ctx, ssmClient, ssmParameterName)
if err != nil {
return fmt.Errorf("failed to get AMI ID from SSM: %w", err)
}

architecture, err := getAMIArchitecture(ctx, ec2Client, amiID)
if err != nil {
return fmt.Errorf("failed to get AMI architecture: %w", err)
}

instanceTypes, err := getSupportedInstanceTypes(ctx, paginator, architecture)
if err != nil {
return fmt.Errorf("failed to get supported instance types: %w", err)
}

for _, instanceType := range instanceTypes {
jokestax marked this conversation as resolved.
Show resolved Hide resolved
if instanceType == nodeType {
return nil
}
}

return fmt.Errorf("node type %s not supported for %s\nSupported instance types: %s", nodeType, amiType, instanceTypes)
}

type ssmClienter interface {
GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error)
}

func getLatestAMIFromSSM(ctx context.Context, ssmClient ssmClienter, parameterName string) (string, error) {
input := &ssm.GetParameterInput{
Name: aws.String(parameterName),
}
output, err := ssmClient.GetParameter(ctx, input)
if err != nil {
return "", fmt.Errorf("failed to initialize ssm client: %w", err)
}

return *output.Parameter.Value, nil
}

type ec2Clienter interface {
DescribeImages(ctx context.Context, params *ec2.DescribeImagesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeImagesOutput, error)
}

func getAMIArchitecture(ctx context.Context, ec2Client ec2Clienter, amiID string) (string, error) {
input := &ec2.DescribeImagesInput{
ImageIds: []string{amiID},
}
output, err := ec2Client.DescribeImages(ctx, input)
if err != nil {
return "", fmt.Errorf("failed to describe images: %w", err)
}

if len(output.Images) == 0 {
return "", fmt.Errorf("no images found for AMI ID: %s", amiID)
}

return string(output.Images[0].Architecture), nil
}

type paginator interface {
HasMorePages() bool
NextPage(ctx context.Context, optFns ...func(*ec2.Options)) (*ec2.DescribeInstanceTypesOutput, error)
}

func getSupportedInstanceTypes(ctx context.Context, p paginator, architecture string) ([]string, error) {
var instanceTypes []string
for p.HasMorePages() {
page, err := p.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load next pages for instance types: %w", err)
}

for _, instanceType := range page.InstanceTypes {
if slices.Contains(instanceType.ProcessorInfo.SupportedArchitectures, ec2Types.ArchitectureType(architecture)) {
instanceTypes = append(instanceTypes, string(instanceType.InstanceType))
}
}
}
return instanceTypes, nil
}
Loading
Loading