Skip to content

Commit

Permalink
Simplify cluster stack updates
Browse files Browse the repository at this point in the history
- import VPC and subnets, as at the point of stack update imported
  VPC descriptor will be the same as the one already in use, whether
  it was originally imported or created as part of the cluster stack
- generalise updates to only append any new resources
  • Loading branch information
errordeveloper committed Jan 17, 2019
1 parent 9287fc9 commit 8d05318
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 118 deletions.
2 changes: 2 additions & 0 deletions pkg/cfn/manager/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"github.com/weaveworks/eksctl/pkg/cfn/builder"
)

const resourcesRootPath = "Resources"

var (
stackCapabilitiesIAM = aws.StringSlice([]string{cloudformation.CapabilityCapabilityIam})
)
Expand Down
121 changes: 28 additions & 93 deletions pkg/cfn/manager/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (

api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha3"
"github.com/weaveworks/eksctl/pkg/cfn/builder"
"github.com/weaveworks/eksctl/pkg/utils/ipnet"
)

func (c *StackCollection) makeClusterStackName() string {
Expand Down Expand Up @@ -62,99 +61,29 @@ func (c *StackCollection) WaitDeleteCluster() error {
return c.BlockingWaitDeleteStack(c.makeClusterStackName())
}

// UpdateClusterForCompability will update cluster
// with new resources based on features that have
// a critical effect on forward-compatibility with
// respect to overal functionality and integrity
func (c *StackCollection) UpdateClusterForCompability() error {
const resourceRootPath = "Resources"

// AppendNewClusterStackResource will update cluster
// stack with new resources in append-only way
func (c *StackCollection) AppendNewClusterStackResource() error {
name := c.makeClusterStackName()

currentStack, err := c.DescribeClusterStack()
if err != nil {
return err
}

// NOTE: currently we can only append new
// resources to the stack, as there are a
// few limitations;
// we don't have a way of recompiling the
// definition of the stack from it's current
// template and we don't have all feature
// indicators we would need (e.g. when
// existing VPC is used);
// to do that in a sensible manner we would
// have to thoroughly insepect the current
// template and see if e.g. VPC or SGs are
// imported or managed by us;
// addtionally, the EKS control plane itself
// cannot yet be updated via CloudFormation

missingSharedNodeSecurityGroup := true

for _, x := range currentStack.Outputs {
switch *x.OutputKey {
case builder.CfnOutputClusterSharedNodeSecurityGroup:
missingSharedNodeSecurityGroup = false
}
}
// NOTE: currently we can only append new resources to the stack,
// as there are a few limitations:
// - it must work with VPC that are imported as well as VPC that
// is mamaged as part of the stack;
// - CloudFormation cannot yet upgrade EKS control plane itself;

// Get current stack
currentTemplate, err := c.GetStackTemplate(name)
if err != nil {
return errors.Wrapf(err, "error getting stack template %s", name)
}

updateFeatureList := []string{}
addResources := []string{}

if missingSharedNodeSecurityGroup {
updateFeatureList = append(updateFeatureList, "shared node security group")
addResources = append(addResources,
"ClusterSharedNodeSecurityGroup",
"IngressInterNodeGroupSG",
)
}

if len(addResources) == 0 {
logger.Success("all resources in cluster stack %q are up-to-date", name)
return nil
}

currentResources := gjson.Get(currentTemplate, resourceRootPath)
currentResources := gjson.Get(currentTemplate, resourcesRootPath)
if !currentResources.IsObject() {
return fmt.Errorf("unexpected template format of the current stack ")
}

{
// We need to use same subnet CIDRs in order to recompile the template
// with all of default resources
vpc := c.spec.VPC
vpc.Subnets = map[api.SubnetTopology]map[string]api.Network{
api.SubnetTopologyPublic: map[string]api.Network{},
api.SubnetTopologyPrivate: map[string]api.Network{},
}
currentResources.ForEach(func(resourceKey, resource gjson.Result) bool {
if resource.Get("Type").Value() == "AWS::EC2::Subnet" {
az := resource.Get("Properties.AvailabilityZone").String()
cidr, _ := ipnet.ParseCIDR(resource.Get("Properties.CidrBlock").String())
k := resourceKey.String()
if strings.HasPrefix(k, "SubnetPrivate") {
vpc.Subnets[api.SubnetTopologyPrivate][az] = api.Network{
CIDR: cidr,
}
}
if strings.HasPrefix(k, "SubnetPublic") {
vpc.Subnets[api.SubnetTopologyPublic][az] = api.Network{
CIDR: cidr,
}
}
}
return true
})
}

logger.Info("creating cluster stack %q", name)
newStack := builder.NewClusterResourceSet(c.provider, c.spec)
if err := newStack.AddAllResources(); err != nil {
Expand All @@ -167,30 +96,36 @@ func (c *StackCollection) UpdateClusterForCompability() error {
}
logger.Debug("newTemplate = %s", newTemplate)

newResources := gjson.Get(string(newTemplate), resourceRootPath)

newResources := gjson.Get(string(newTemplate), resourcesRootPath)
if !newResources.IsObject() {
return fmt.Errorf("unexpected template format of the new version of the stack ")
}

logger.Debug("currentTemplate = %s", currentTemplate)

for _, resourceKey := range addResources {
var err error
resource := newResources.Get(resourceKey)
if !resource.Exists() {
return fmt.Errorf("resource with key %q doesn't exist in the new version of the stack", resourceKey)
}
currentTemplate, err = sjson.Set(currentTemplate, resourceRootPath+"."+resourceKey, resource.Value())
if err != nil {
return errors.Wrapf(err, "unable to add resource with key %q to cluster stack", resourceKey)
var iterErr error
newResources.ForEach(func(resourceKey, resource gjson.Result) bool {
key := resourceKey.String()
if currentResources.Get(key).Exists() {
return true
}
addResources = append(addResources, key)
path := resourcesRootPath + "." + key
currentTemplate, iterErr = sjson.Set(currentTemplate, path, resource.Value())
return iterErr == nil
})
if iterErr != nil {
return errors.Wrap(iterErr, "updating stack template")
}

if len(addResources) == 0 {
logger.Success("all resources in cluster stack %q are up-to-date", name)
return nil
}

logger.Debug("currentTemplate = %s", currentTemplate)

describeUpdate := fmt.Sprintf("updating stack to add new features: %s;",
strings.Join(updateFeatureList, ", "))
describeUpdate := fmt.Sprintf("updating stack to add new resources: %v", addResources)
return c.UpdateStack(name, "update-cluster", describeUpdate, []byte(currentTemplate), nil)
}

Expand Down
10 changes: 5 additions & 5 deletions pkg/cfn/manager/nodegroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ import (
)

const (
desiredCapacityPath = "Resources.NodeGroup.Properties.DesiredCapacity"
maxSizePath = "Resources.NodeGroup.Properties.MaxSize"
minSizePath = "Resources.NodeGroup.Properties.MinSize"
instanceTypePath = "Resources.NodeLaunchConfig.Properties.InstanceType"
imageIDPath = "Resources.NodeLaunchConfig.Properties.ImageId"
desiredCapacityPath = resourcesRootPath + ".NodeGroup.Properties.DesiredCapacity"
maxSizePath = resourcesRootPath + ".NodeGroup.Properties.MaxSize"
minSizePath = resourcesRootPath + ".NodeGroup.Properties.MinSize"
instanceTypePath = resourcesRootPath + ".NodeLaunchConfig.Properties.InstanceType"
imageIDPath = resourcesRootPath + ".NodeLaunchConfig.Properties.ImageId"
)

// NodeGroupSummary represents a summary of a nodegroup stack
Expand Down
25 changes: 5 additions & 20 deletions pkg/ctl/utils/update_cluster_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

"github.com/kris-nova/logger"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

Expand Down Expand Up @@ -61,32 +62,16 @@ func doUpdateClusterStacksCmd(p *api.ProviderConfig, cfg *api.ClusterConfig, nam
return fmt.Errorf("--name must be set")
}

stackManager := ctl.NewStackManager(cfg)

{
stack, err := stackManager.DescribeClusterStack()
if err != nil {
return err
}
logger.Info("cluster = %#v", stack)
if err := ctl.GetClusterVPC(cfg); err != nil {
return errors.Wrapf(err, "getting VPC configuration for cluster %q", cfg.Metadata.Name)
}

// if err := ctl.GetClusterVPC(cfg); err != nil {
// return errors.Wrapf(err, "getting VPC configuration for cluster %q", cfg.Metadata.Name)
// }
stackManager := ctl.NewStackManager(cfg)

if err := stackManager.UpdateClusterForCompability(); err != nil {
if err := stackManager.AppendNewClusterStackResource(); err != nil {
return err
}

{
stack, err := stackManager.DescribeClusterStack()
if err != nil {
return err
}
logger.Info("cluster = %#v", stack)
}

if err := ctl.ValidateExistingNodeGroupsForCompatibility(cfg, stackManager); err != nil {
logger.Critical("failed checking nodegroups", err.Error())
}
Expand Down

0 comments on commit 8d05318

Please sign in to comment.