Skip to content

Commit

Permalink
feat: add bottlerocket ami types (#2352)
Browse files Browse the repository at this point in the history
* add validation for aws ami type

* fix lint

* dependency for bottlerocket

* update api

* fix lint

* resolve validate credentials

* add test cases

* move up the logger

* more tests

* fix lint

* fix suggestions

* fix tests

* fix suggestion

* use slices

* fix print stmt

* delete duplicate

* fix lint

* fix print stmt

* fix spelling

* fix tests

* chore: make the mock function signature match the interface

Signed-off-by: nathan-nicholson <[email protected]>

* chore: fix the ValidateCredentials test to use ErrorContains

Signed-off-by: nathan-nicholson <[email protected]>

* chore: update ssm mock to use dynamic def of GetParameters

Signed-off-by: nathan-nicholson <[email protected]>

* chore: more test refactoring

Signed-off-by: nathan-nicholson <[email protected]>

* chore: remove unused attributes

Signed-off-by: nathan-nicholson <[email protected]>

* chore: remove commented out code

Signed-off-by: nathan-nicholson <[email protected]>

* fix: update paginator mock to be more flexible

Signed-off-by: nathan-nicholson <[email protected]>

* Reduce test complexity.

* Apply suggestions from code review

Co-authored-by: Patrick D'appollonio <[email protected]>

* set next macro chart for kubefirst - 2.7.8-rc6

* set next macro chart for kubefirst - 2.7.8-rc7

* docs: add azure to the readme (#2343)

* chore: remove unused GitHub Workflow for devrel (#2346)

* fix: delete kubefirst file when doing kubefirst reset - KRA-73 (#2344)

---------

Signed-off-by: nathan-nicholson <[email protected]>

* set next macro chart for kubefirst - 2.7.8-rc8

* set next macro chart for kubefirst - 2.7.8-rc9

* set next macro chart for kubefirst - 2.7.8-rc10

* set next macro chart for kubefirst - 2.7.9-rc1

* set next macro chart for kubefirst - 2.7.9-rc2

* set next macro chart for kubefirst - 2.7.9-rc3

* set next macro chart for kubefirst - 2.7.9-rc4

* set next macro chart for kubefirst - 2.7.9

* fix: pass CF_ORIGIN_CA_ISSUER_API_TOKEN to API (#2347)

* set next macro chart for kubefirst - 2.7.10-rc1

* remove vultr and google out of beta (#2351)

* set next macro chart for kubefirst - 2.7.10-rc2

* set next macro chart for kubefirst - 2.7.10-rc3

* fix(azure): change GCP ref in CLI docs to Azure (#2353)

* set next macro chart for kubefirst - 2.7.10-rc4

* fix: go indent

* fix: fumpt

---------

Signed-off-by: nathan-nicholson <[email protected]>
Co-authored-by: nathan-nicholson <[email protected]>
Co-authored-by: Patrick D'appollonio <[email protected]>
Co-authored-by: konstruct-bot <[email protected]>
Co-authored-by: Simon Emms <[email protected]>
Co-authored-by: Frédéric Harper <[email protected]>
Co-authored-by: Muse Mulatu <[email protected]>
  • Loading branch information
7 people authored Jan 6, 2025
1 parent 0dec1fd commit dcb3fe8
Show file tree
Hide file tree
Showing 11 changed files with 1,343 additions and 1,358 deletions.
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
147 changes: 128 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 := cmd.Context()

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,118 @@ 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, cp aws.CredentialsProvider) (*aws.Credentials, error) {
// Retrieve credentials
creds, err := cp.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)
}

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 {
if instanceType == nodeType {
return nil
}
}

return fmt.Errorf("node type %q not supported for %q\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("failure when fetching parameters: %w", err)
}

if output == nil || output.Parameter == nil || output.Parameter.Value == nil {
return "", fmt.Errorf("invalid parameter value found for %q", parameterName)
}

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

0 comments on commit dcb3fe8

Please sign in to comment.