Skip to content

Commit

Permalink
Look for the latest eksbuild version to use for each region (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
yorinasub17 authored Aug 20, 2021
1 parent c5ddc93 commit eb4df83
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 21 deletions.
109 changes: 88 additions & 21 deletions eks/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,44 @@ import (
k8stypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"

"github.com/gruntwork-io/kubergrunt/commonerrors"
"github.com/gruntwork-io/kubergrunt/eksawshelper"
"github.com/gruntwork-io/kubergrunt/jsonpatch"
"github.com/gruntwork-io/kubergrunt/kubectl"
"github.com/gruntwork-io/kubergrunt/logging"
)

const (
containerAccountID = "602401143452"
kubeProxyRepoPath = "eks/kube-proxy"
coreDNSRepoPath = "eks/coredns"

// Largest eksbuild tag we will try looking for.
maxEKSBuild = 10
)

var (
// NOTE: Ensure that there is an entry for each supported version in the following tables.
supportedVersions = []string{"1.21", "1.20", "1.19", "1.18", "1.17", "1.16"}

// Reference: https://docs.aws.amazon.com/eks/latest/userguide/managing-coredns.html
coreDNSVersionLookupTable = map[string]string{
"1.21": "1.8.4-eksbuild.1",
"1.20": "1.8.3-eksbuild.1",
"1.19": "1.8.0-eksbuild.1",
"1.18": "1.7.0-eksbuild.1",
"1.17": "1.6.6-eksbuild.1",
"1.16": "1.6.6-eksbuild.1",
"1.21": "1.8.4-eksbuild",
"1.20": "1.8.3-eksbuild",
"1.19": "1.8.0-eksbuild",
"1.18": "1.7.0-eksbuild",
"1.17": "1.6.6-eksbuild",
"1.16": "1.6.6-eksbuild",
}

// Reference: https://docs.aws.amazon.com/eks/latest/userguide/managing-kube-proxy.html#updating-kube-proxy-add-on
kubeProxyVersionLookupTable = map[string]string{
"1.21": "1.21.2-eksbuild.2",
"1.20": "1.20.4-eksbuild.2",
"1.19": "1.19.6-eksbuild.2",
"1.18": "1.18.8-eksbuild.2",
"1.17": "1.17.9-eksbuild.2",
"1.16": "1.16.13-eksbuild.2",
"1.21": "1.21.2-eksbuild",
"1.20": "1.20.4-eksbuild",
"1.19": "1.19.6-eksbuild",
"1.18": "1.18.8-eksbuild",
"1.17": "1.17.9-eksbuild",
"1.16": "1.16.13-eksbuild",
}

// Reference: https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html
Expand Down Expand Up @@ -82,6 +92,12 @@ type SkipComponentsConfig struct {
VPCCNI bool
}

type componentVersions struct {
kubeProxy string
coreDNS string
vpcCNI string
}

// SyncClusterComponents will perform the steps described in
// https://docs.aws.amazon.com/eks/latest/userguide/update-cluster.html
// There are three core applications on an EKS cluster:
Expand Down Expand Up @@ -113,8 +129,27 @@ func SyncClusterComponents(
return errors.WithStackTrace(UnsupportedEKSVersion{k8sVersion})
}

kubeProxyVersion := kubeProxyVersionLookupTable[k8sVersion]
coreDNSVersion := coreDNSVersionLookupTable[k8sVersion]
awsRegion, err := eksawshelper.GetRegionFromArn(eksClusterArn)
if err != nil {
return err
}

dockerToken, err := eksawshelper.GetDockerLoginToken(awsRegion)
if err != nil {
return err
}

repoDomain := getRepoDomain(awsRegion)
kubeProxyVersion, err := findLatestEKSBuild(dockerToken, repoDomain, kubeProxyRepoPath, kubeProxyVersionLookupTable[k8sVersion])
if err != nil {
return err
}

coreDNSVersion, err := findLatestEKSBuild(dockerToken, repoDomain, coreDNSRepoPath, coreDNSVersionLookupTable[k8sVersion])
if err != nil {
return err
}

amznVPCCNIVersion := amazonVPCCNIVersionLookupTable[k8sVersion]

logger.Info("Syncing Kubernetes Applications to:")
Expand All @@ -134,11 +169,6 @@ func SyncClusterComponents(
return err
}

awsRegion, err := eksawshelper.GetRegionFromArn(eksClusterArn)
if err != nil {
return err
}

if skipConfig.KubeProxy {
logger.Info("Skipping kube-proxy sync.")
} else {
Expand Down Expand Up @@ -179,7 +209,7 @@ func upgradeKubeProxy(
) error {
logger := logging.GetProjectLogger()

targetImage := fmt.Sprintf("602401143452.dkr.ecr.%s.amazonaws.com/eks/kube-proxy:v%s", awsRegion, kubeProxyVersion)
targetImage := fmt.Sprintf("%s/%s:v%s", getRepoDomain(awsRegion), kubeProxyRepoPath, kubeProxyVersion)
currentImage, err := getCurrentDeployedKubeProxyImage(clientset)
if err != nil {
return err
Expand Down Expand Up @@ -292,7 +322,7 @@ func upgradeCoreDNS(
logger.Info("ClusterRole permissions for coredns is up to date. Skipping adjusting ClusterRole permissions.")
}

targetImage := fmt.Sprintf("602401143452.dkr.ecr.%s.amazonaws.com/eks/coredns:v%s", awsRegion, coreDNSVersion)
targetImage := fmt.Sprintf("%s/%s:v%s", getRepoDomain(awsRegion), coreDNSRepoPath, coreDNSVersion)
currentImage, err := getCurrentDeployedCoreDNSImage(clientset)
if err != nil {
return err
Expand Down Expand Up @@ -581,3 +611,40 @@ func removeUpstreamKeywordFromCorednsConfigMap(clientset *kubernetes.Clientset,
_, err = configMapAPI.Update(context.Background(), corednsConfigMap, metav1.UpdateOptions{})
return errors.WithStackTrace(err)
}

// findLatestEKSBuild will continuously query the ECR repo to look for the latest eksbuild version. We do this by
// incrementally checking one tag at a time until we reach a 404, or the maximum trials.
func findLatestEKSBuild(token, repoDomain, repoPath, tagBase string) (string, error) {
logger := logging.GetProjectLogger()
logger.Debugf("Looking up latest eksbuild for repo %s/%s", repoDomain, repoPath)

var existingTag string
for i := 0; i < maxEKSBuild; i++ {
version := fmt.Sprintf("%s.%d", tagBase, i+1)
query := "v" + version
logger.Debugf("Trying %s", query)
tagExists, err := eksawshelper.TagExistsInRepo(token, repoDomain, repoPath, query)
if err != nil {
return "", err
}
if tagExists {
logger.Debugf("Found %s", query)
// Update the latest tag marker
existingTag = version
} else {
logger.Debugf("Not found %s", query)
logger.Debugf("Returning %s", existingTag)
// At this point, the last existing tag we encountered is the latest, so we return it.
return existingTag, nil
}
}

// MAINTAINER'S NOTE: If we ever reach here, this is 100% a bug in kubergrunt. Investigation is needed to resolve
// this, as it could be either the wrong version is being queried, or the maxEKSBuild count is too small.
return "", commonerrors.ImpossibleErr("TOO_MANY_EKS_BUILD_TAGS")
}

// getRepoDomain is a conveniency function to construct the ECR docker repo URL domain.
func getRepoDomain(region string) string {
return fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", containerAccountID, region)
}
77 changes: 77 additions & 0 deletions eksawshelper/ecr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package eksawshelper

import (
"fmt"
"io/ioutil"
"net/http"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/gruntwork-io/go-commons/errors"
"github.com/gruntwork-io/kubergrunt/commonerrors"
"github.com/hashicorp/go-cleanhttp"
)

// TagExistsInRepo queries the ECR repository docker API to see if the given tag exists for the given ECR repository.
func TagExistsInRepo(token, repoDomain, repoPath, tag string) (bool, error) {
manifestURL := fmt.Sprintf("https://%s/v2/%s/manifests/%s", repoDomain, repoPath, tag)
req, err := http.NewRequest("GET", manifestURL, nil)
if err != nil {
return false, errors.WithStackTrace(err)
}
req.Header.Set("Authorization", "Basic "+token)

httpClient := cleanhttp.DefaultClient()
resp, err := httpClient.Do(req)
if err != nil {
return false, errors.WithStackTrace(err)
}

switch resp.StatusCode {
case 200:
return true, nil
case 404:
return false, nil
}

// All other status codes should be consider API errors.
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false, errors.WithStackTrace(err)
}
return false, errors.WithStackTrace(ECRManifestFetchError{
manifestURL: manifestURL,
statusCode: resp.StatusCode,
body: string(body),
})
}

// GetDockerLoginToken retrieves an authorization token that can be used to access ECR via the docker APIs. The
// return token can directly be used as a HTTP authorization header for basic auth.
func GetDockerLoginToken(region string) (string, error) {
client, err := NewECRClient(region)
if err != nil {
return "", errors.WithStackTrace(err)
}

resp, err := client.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
if err != nil {
return "", errors.WithStackTrace(err)
}

if len(resp.AuthorizationData) != 1 {
// AWS docs mention that there is always one token returned on a successful response.
return "", errors.WithStackTrace(commonerrors.ImpossibleErr("AWS_DID_NOT_RETURN_DOCKER_TOKEN"))
}
return aws.StringValue(resp.AuthorizationData[0].AuthorizationToken), nil
}

// NewECRClient creates an AWS SDK client to access ECR API.
func NewECRClient(region string) (*ecr.ECR, error) {
sess, err := NewAuthenticatedSession(region)
if err != nil {
return nil, err
}
return ecr.New(sess), nil
}
11 changes: 11 additions & 0 deletions eksawshelper/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,14 @@ type CredentialsError struct {
func (err CredentialsError) Error() string {
return fmt.Sprintf("Error finding AWS credentials. Did you set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables or configure an AWS profile? Underlying error: %v", err.UnderlyingErr)
}

// ECRManifestFetchError is an error that occurs when retrieving information about a given tag in an ECR repository.
type ECRManifestFetchError struct {
manifestURL string
statusCode int
body string
}

func (err ECRManifestFetchError) Error() string {
return fmt.Sprintf("Error querying ECR repo URL %s (status code %d) (response body %s)", err.manifestURL, err.statusCode, err.body)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/blang/semver/v4 v4.0.0
github.com/gruntwork-io/go-commons v0.8.2
github.com/gruntwork-io/terratest v0.32.9
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-multierror v1.1.0
github.com/mitchellh/go-homedir v1.1.0
github.com/sirupsen/logrus v1.6.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ github.com/gruntwork-io/terratest v0.32.9 h1:ciWWJxISk06LAYImn6h1Vvir8hUz13VtwT2
github.com/gruntwork-io/terratest v0.32.9/go.mod h1:FckR+7ks472IJfSKUPfPvnJfSxV1LKGWGMJ9m/LHegE=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
Expand Down

0 comments on commit eb4df83

Please sign in to comment.