diff --git a/.gitignore b/.gitignore index c4839f6..4b803a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ *.swp .idea - +.vscode dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a8b09e8..678866b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -28,27 +28,27 @@ archives: {{- if .Arm }}v{{ .Arm }}{{ end }} # use zip for windows archives format_overrides: - - goos: windows - format: zip + - goos: windows + format: zip checksum: - name_template: 'checksums.txt' + name_template: "checksums.txt" snapshot: name_template: "{{ incpatch .Version }}-next" changelog: sort: asc filters: exclude: - - '^docs:' - - '^test:' + - "^docs:" + - "^test:" dockers: -## standard AMIs e.g. eks m5.2xlarae types + ## standard AMIs e.g. eks m5.2xlarae types - image_templates: - "ghcr.io/armory-io/eks-auto-updater:{{ .Version }}-linux-amd64" use: buildx goos: linux build_flag_templates: - "--platform=linux/amd64" -## This is graviton2 in AWS like mg5g.2xlarge types + ## This is graviton2 in AWS like mg5g.2xlarge types - image_templates: - "ghcr.io/armory-io/eks-auto-updater:{{ .Version }}-linux-arm64v8" use: buildx @@ -62,7 +62,6 @@ docker_manifests: image_templates: - "ghcr.io/armory-io/eks-auto-updater:{{ .Version }}-linux-amd64" - "ghcr.io/armory-io/eks-auto-updater:{{ .Version }}-linux-arm64v8" - # The lines beneath this are called `modelines`. See `:help modeline` # Feel free to remove those if you don't want/use them. # yaml-language-server: $schema=https://goreleaser.com/static/schema.json diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..e32491a --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,51 @@ +/* +Copyright © 2023 NAME HERE + +*/ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + + + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "eks-auto-updater", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.eks-auto-updater.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + + diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..6098822 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "github.com/armory-io/eks-auto-updater/internal/updater" + "github.com/armory-io/eks-auto-updater/pkg/aws" + + "github.com/spf13/cobra" +) + +// runCmd represents the run command +var runCmd = &cobra.Command{ + Use: "run", + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + region := cmd.Flag("region").Value.String() + roleArn := cmd.Flag("role-arn").Value.String() + clusterName := cmd.Flag("cluster-name").Value.String() + nodegroupName := cmd.Flag("nodegroup-name").Value.String() + waitForNodeUpdates, _ := cmd.Flags().GetInt("nodegroup-wait-time") + + awsClient, err := aws.GetEksClient(ctx, region, roleArn) + if err != nil { + return err + } + + updater := updater.NewEKSUpdater(awsClient) + err = updater.UpdateClusterNodeGroup(ctx, &clusterName, &nodegroupName, waitForNodeUpdates) + if err != nil { + return err + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(runCmd) + + // Region + runCmd.Flags().String("region", "us-west-2", "AWS Region to use") + runCmd.MarkFlagRequired("region") + + // Cluster Name + runCmd.Flags().String("cluster-name", "", "Name of the EKS cluster to update") + runCmd.MarkFlagRequired("cluster-name") + + // Nodegroup Name + runCmd.Flags().String("nodegroup-name", "", "Name of the EKS nodegroup to update") + runCmd.MarkFlagRequired("nodegroup-name") + + // Role ARN + runCmd.Flags().String("role-arn", "", "Role ARN to assume") + + // Nodegroup Wait Time + runCmd.Flags().Int("nodegroup-wait-time", 120, "Time in minutes to wait for node group update to complete. Defaults to 120 minutes") +} diff --git a/eks-updater.go b/eks-updater.go deleted file mode 100644 index 1607a2e..0000000 --- a/eks-updater.go +++ /dev/null @@ -1,191 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "log" - "math/rand" - "strconv" - "strings" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials" - _ "github.com/aws/aws-sdk-go-v2/credentials/stscreds" - "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/aws/aws-sdk-go-v2/service/sts" - "github.com/hashicorp/go-version" -) - -/* -LONG TERM goals: -Update CLUSTER VERSION as well. E.g. this can update the whole shebang saving a lot of time/headache. We can validate things beforehand LIKE - - Make sure there are PDBs on resources like spinnaker first - - Verify nodes are on an n-1 release version first. AND update nodes post upgrade kinda things -*/ -func main() { - - clusterName := flag.String("cluster-name", "", "Cluster name REQUIRED") - //TODO: LOOK UP managed node groups instead of parameters... enhancement for later. AND update multiple node groups sequentially would be a later thing - nodegroupName := flag.String("nodegroup-name", "", "Node group name to update REQUIRED") - roleArn := flag.String("role-arn", "", "Role to assume if set") - region := flag.String("region", "us-west-2", "Region to operate in - defaults to us-west-2") - waitTimeForNodeUpdates := *flag.Int("nodegroup-wait-time", 120, "Time in minutes to wait for node group update to complete. Defaults to 120 minutes") - addonsToUpdate := strings.Split(*flag.String("addons-to-update", "kube-proxy,coredns,vpc-cni,aws-ebs-csi-driver", "Comma separated list of adds on to updates. Defaults to kube-proxy, coredns, vpc-cni, aws-ebs-csi-driver addons"), ",") - flag.Parse() - if len(*clusterName) == 0 { - log.Fatal("Invalid cluster name! Must be set!") - } - - // Load the Shared AWS Configuration (~/.aws/config) - ctx := context.TODO() - client, err := getEksClient(ctx, *region, *roleArn) - if err != nil { - log.Fatal("Unable to get EKS client:", err) - } - - log.Println("INFO: Starting updates...") - clusterInformation, _ := client.DescribeCluster(ctx, &eks.DescribeClusterInput{Name: clusterName}) - if len(*nodegroupName) == 0 { - // Lookup and update the node groups... - nodeGroups, nodeGroupListErr := client.ListNodegroups(ctx, &eks.ListNodegroupsInput{ClusterName: clusterName}) - if nodeGroupListErr != nil { - log.Fatal("ERROR: Unable to list node groups... ", nodeGroupListErr) - } - for _, nodeGroup := range nodeGroups.Nodegroups { - log.Println("INFO: Starting updates of node group " + nodeGroup) - updateError := updateClusterNodeGroup(client, ctx, clusterName, &nodeGroup, waitTimeForNodeUpdates) - if updateError != nil { - log.Fatal("ERROR: Unable to update cluster node group... ", updateError) - } - } - } else { - log.Println("INFO: Starting updates of node group " + *nodegroupName) - err = updateClusterNodeGroup(client, ctx, clusterName, nodegroupName, waitTimeForNodeUpdates) - if err != nil { - log.Fatal("ERROR: Unable to update cluster node group... ", err) - } - } - for _, addon := range addonsToUpdate { - err = updateAddon(client, ctx, clusterName, &addon, clusterInformation.Cluster.Version) - if err != nil { - log.Fatal("Unable to update addon "+addon, err) - } - - } - log.Println("INFO: Updates complete!") - -} - -func updateAddon(client *eks.Client, ctx context.Context, clusterName *string, addonName *string, k8sVersion *string) (err error) { - - addonInfo, err := client.DescribeAddon(ctx, &eks.DescribeAddonInput{AddonName: addonName, ClusterName: clusterName}) - if err != nil { - log.Println("WARN: Error describing addon " + *addonName + " in the cluster... is it actually in the cluster? Skipping...") - return nil - } - - versions, err := client.DescribeAddonVersions(ctx, &eks.DescribeAddonVersionsInput{AddonName: addonName, KubernetesVersion: k8sVersion}) - if err != nil { - log.Println("ERROR: Failure getting addon versions ", err) - return err - } - // Find the default version. TECHNICALLY this is a paginated call, so need to long term add support for that. - var defaultVersion = "" - for _, addon := range versions.Addons { - for _, addonVersion := range addon.AddonVersions { - for _, capability := range addonVersion.Compatibilities { - if capability.DefaultVersion { - defaultVersion = *addonVersion.AddonVersion - } - } - } - } - if len(defaultVersion) == 0 { - log.Println("WARN: Failed to find valid addon for " + *addonName + " ... skipping updates of the addon!") - // Should we return err instead of exiting? - return nil - } - currentVersion, err := version.NewVersion(*addonInfo.Addon.AddonVersion) - if err != nil { - return fmt.Errorf("ERROR: Unable to parse version correctly for addon "+*addonName+" version of "+*addonInfo.Addon.AddonVersion+", %w", err) - } - newVersion, err := version.NewVersion(defaultVersion) - if err != nil { - return fmt.Errorf("ERROR: Unable to parse version correctly for addon "+*addonName+" version of "+defaultVersion+", %w", err) - } - if newVersion.Compare(currentVersion) < 0 { - log.Println("WARNING: Skipping addon " + *addonName + " as the version to upgrade to is older/equal to the version installed!") - return nil - } - log.Println("INFO: Updating addon " + *addonName + " from: " + *addonInfo.Addon.AddonVersion + " to:" + defaultVersion) - //NOMINALLY we should check if there's a service account/config and apply that here not just default to node settinsg :) - response, err := client.UpdateAddon(ctx, &eks.UpdateAddonInput{ - AddonName: addonName, - ClusterName: clusterName, - AddonVersion: &defaultVersion, - ResolveConflicts: "OVERWRITE", - }) - if err != nil { - return err - } - log.Println("INFO: Addon update triggered ... ID of " + *response.Update.Id + "... waiting for completion") - err = eks.NewAddonActiveWaiter(client).Wait(ctx, &eks.DescribeAddonInput{ - AddonName: addonName, - ClusterName: clusterName, - }, time.Duration(20)*time.Minute) - return err -} - -func updateClusterNodeGroup(client *eks.Client, ctx context.Context, clusterName *string, nodegroupName *string, waitForNodeUpdates int) (err error) { - version, err := client.UpdateNodegroupVersion(ctx, &eks.UpdateNodegroupVersionInput{ClusterName: clusterName, NodegroupName: nodegroupName}) - if err != nil { - return fmt.Errorf("ERROR: Update call failed %w", err) - } - log.Println("INFO: Upgrade job started... " + *version.Update.Id) - waiter := eks.NewNodegroupActiveWaiter(client) - err = waiter.Wait(ctx, &eks.DescribeNodegroupInput{ClusterName: clusterName, NodegroupName: nodegroupName}, time.Duration(waitForNodeUpdates)*time.Minute) - if err != nil { - return fmt.Errorf("ERROR: Update failed to complete in the allotted time: %w", err) - } - return nil -} - -func getEksClient(ctx context.Context, region string, roleArn string) (client *eks.Client, err error) { - - var cfg aws.Config - cfg, err = config.LoadDefaultConfig(ctx, config.WithRegion(region)) - - if err != nil { - return client, err - } - if len(roleArn) == 0 { - return eks.NewFromConfig(cfg), err - } - log.Println("INFO: Assuming role ARN " + roleArn) - // Create config & sts client with source account - - sourceAccount := sts.NewFromConfig(cfg) - // Default and only support 1 hour duration. We MAY hit an issue here particularly if node groups take a LONG time to update. - duration := int32(3600) - // Assume target role and store credentials - rand.Seed(time.Now().UnixNano()) - response, err := sourceAccount.AssumeRole(ctx, &sts.AssumeRoleInput{ - RoleArn: aws.String(roleArn), - RoleSessionName: aws.String("eks-auto-updater-" + strconv.Itoa(10000+rand.Intn(25000))), - DurationSeconds: &duration, - }) - if err != nil { - return client, err - } - var assumedRoleCreds = response.Credentials - - // Create config with target service client, using assumed role - cfg, err = config.LoadDefaultConfig(ctx, config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(*assumedRoleCreds.AccessKeyId, *assumedRoleCreds.SecretAccessKey, *assumedRoleCreds.SessionToken)), config.WithRegion(region)) - if err != nil { - return client, err - } - return eks.NewFromConfig(cfg), err -} diff --git a/go.mod b/go.mod index 21adb3a..9efed38 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module eks-updater +module github.com/armory-io/eks-auto-updater go 1.20 @@ -9,6 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/eks v1.27.7 github.com/aws/aws-sdk-go-v2/service/sts v1.18.6 github.com/hashicorp/go-version v1.6.0 + github.com/spf13/cobra v1.7.0 ) require ( @@ -20,5 +21,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.12.5 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.5 // indirect github.com/aws/smithy-go v1.13.5 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect ) diff --git a/go.sum b/go.sum index d76726d..3921a91 100644 --- a/go.sum +++ b/go.sum @@ -24,19 +24,28 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.18.6 h1:rIFn5J3yDoeuKCE9sESXqM5POTAh github.com/aws/aws-sdk-go-v2/service/sts v1.18.6/go.mod h1:48WJ9l3dwP0GSHWGc5sFGGlCkuA82Mc2xnw+T6Q8aDw= github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/updater/eks.go b/internal/updater/eks.go new file mode 100644 index 0000000..1c619b5 --- /dev/null +++ b/internal/updater/eks.go @@ -0,0 +1,128 @@ +package updater + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/hashicorp/go-version" +) + +type EKSUpdater struct { + client *eks.Client +} + +func NewEKSUpdater(client *eks.Client) *EKSUpdater { + return &EKSUpdater{ + client: client, + } +} + +// UpdateClusterNodeGroup updates the nodegroup of a cluster to the latest version +func (u EKSUpdater) UpdateClusterNodeGroup(ctx context.Context, clusterName *string, nodegroupName *string, waitForNodeUpdates int) (err error) { + // Check if there is an update in progress + currentNodeGroup, err := u.client.DescribeNodegroup(ctx, &eks.DescribeNodegroupInput{ + ClusterName: clusterName, + NodegroupName: nodegroupName, + }, + ) + if err != nil { + return fmt.Errorf("ERROR: Unable to describe nodegroup: %w", err) + } + + // Determine next action based on current nodegroup status + switch currentNodeGroup.Nodegroup.Status { + case types.NodegroupStatusUpdating: // No need to trigger an update if it's already updating, just wait for it to finish + log.Println("INFO: Nodegroup is already updating. Waiting for completion...") + case types.NodegroupStatusActive: // If it's active, trigger an update + log.Println("INFO: Nodegroup is active. Triggering update...") + + version, err := u.client.UpdateNodegroupVersion(ctx, &eks.UpdateNodegroupVersionInput{ + ClusterName: clusterName, + NodegroupName: nodegroupName, + }, + ) + log.Println("INFO: Upgrade job started... ", *version.Update.Id) + if err != nil { + return fmt.Errorf("ERROR: Update call failed %w", err) + } + default: + log.Printf("INFO: Nodegroup is in '%s' state which cannot be updated. Skipping...\n", currentNodeGroup.Nodegroup.Status) + } + + waiter := eks.NewNodegroupActiveWaiter(u.client) + err = waiter.Wait(ctx, &eks.DescribeNodegroupInput{ + ClusterName: clusterName, + NodegroupName: nodegroupName, + }, + time.Duration(waitForNodeUpdates)*time.Minute, + ) + if err != nil { + return fmt.Errorf("ERROR: Update failed to complete in the allotted time: %w", err) + } + + return nil +} + +func (u EKSUpdater) UpdateAddon(ctx context.Context, clusterName *string, addonName *string, k8sVersion *string) (err error) { + + addonInfo, err := u.client.DescribeAddon(ctx, &eks.DescribeAddonInput{AddonName: addonName, ClusterName: clusterName}) + if err != nil { + log.Println("WARN: Error describing addon " + *addonName + " in the cluster... is it actually in the cluster? Skipping...") + return nil + } + + versions, err := u.client.DescribeAddonVersions(ctx, &eks.DescribeAddonVersionsInput{AddonName: addonName, KubernetesVersion: k8sVersion}) + if err != nil { + log.Println("ERROR: Failure getting addon versions ", err) + return err + } + // Find the default version. TECHNICALLY this is a paginated call, so need to long term add support for that. + var defaultVersion = "" + for _, addon := range versions.Addons { + for _, addonVersion := range addon.AddonVersions { + for _, capability := range addonVersion.Compatibilities { + if capability.DefaultVersion { + defaultVersion = *addonVersion.AddonVersion + } + } + } + } + if len(defaultVersion) == 0 { + log.Println("WARN: Failed to find valid addon for " + *addonName + " ... skipping updates of the addon!") + // Should we return err instead of exiting? + return nil + } + currentVersion, err := version.NewVersion(*addonInfo.Addon.AddonVersion) + if err != nil { + return fmt.Errorf("ERROR: Unable to parse version correctly for addon "+*addonName+" version of "+*addonInfo.Addon.AddonVersion+", %w", err) + } + newVersion, err := version.NewVersion(defaultVersion) + if err != nil { + return fmt.Errorf("ERROR: Unable to parse version correctly for addon "+*addonName+" version of "+defaultVersion+", %w", err) + } + if newVersion.Compare(currentVersion) < 0 { + log.Println("WARNING: Skipping addon " + *addonName + " as the version to upgrade to is older/equal to the version installed!") + return nil + } + log.Println("INFO: Updating addon " + *addonName + " from: " + *addonInfo.Addon.AddonVersion + " to:" + defaultVersion) + //NOMINALLY we should check if there's a service account/config and apply that here not just default to node settinsg :) + response, err := u.client.UpdateAddon(ctx, &eks.UpdateAddonInput{ + AddonName: addonName, + ClusterName: clusterName, + AddonVersion: &defaultVersion, + ResolveConflicts: "OVERWRITE", + }) + if err != nil { + return err + } + log.Println("INFO: Addon update triggered ... ID of " + *response.Update.Id + "... waiting for completion") + err = eks.NewAddonActiveWaiter(u.client).Wait(ctx, &eks.DescribeAddonInput{ + AddonName: addonName, + ClusterName: clusterName, + }, time.Duration(20)*time.Minute) + return err +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9346dd0 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/armory-io/eks-auto-updater/cmd" + +func main() { + cmd.Execute() +} diff --git a/pkg/aws/aws.go b/pkg/aws/aws.go new file mode 100644 index 0000000..0b7f8c4 --- /dev/null +++ b/pkg/aws/aws.go @@ -0,0 +1,50 @@ +package aws + +import ( + "context" + "log" + "math/rand" + "strconv" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/aws/aws-sdk-go-v2/service/sts" +) + +func GetEksClient(ctx context.Context, region string, roleArn string) (*eks.Client, error) { + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) + if err != nil { + return nil, err + } + + if len(roleArn) == 0 { + return eks.NewFromConfig(cfg), nil + } + + log.Println("INFO: Assuming role ARN " + roleArn) + + // Create config & sts client with source account + sourceAccount := sts.NewFromConfig(cfg) + // Default and only support 1 hour duration. We MAY hit an issue here particularly if node groups take a LONG time to update. + duration := int32(3600) + // Assume target role and store credentials + response, err := sourceAccount.AssumeRole(ctx, &sts.AssumeRoleInput{ + RoleArn: aws.String(roleArn), + RoleSessionName: aws.String("eks-auto-updater-" + strconv.Itoa(10000+rand.Intn(25000))), + DurationSeconds: &duration, + }) + if err != nil { + return nil, err + } + + // Create config with target service client, using assumed role + assumedRoleCreds := response.Credentials + cfg, err = config.LoadDefaultConfig(ctx, config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(*assumedRoleCreds.AccessKeyId, *assumedRoleCreds.SecretAccessKey, *assumedRoleCreds.SessionToken)), config.WithRegion(region)) + if err != nil { + return nil, err + } + + return eks.NewFromConfig(cfg), nil +}