Skip to content
This repository has been archived by the owner on Oct 24, 2023. It is now read-only.

Commit

Permalink
feat: update VMSS node pools (#3830)
Browse files Browse the repository at this point in the history
  • Loading branch information
jackfrancis authored Sep 17, 2020
1 parent a5609f4 commit 4f4b8e7
Show file tree
Hide file tree
Showing 8 changed files with 433 additions and 11 deletions.
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func NewRootCmd() *cobra.Command {
rootCmd.AddCommand(newOrchestratorsCmd())
rootCmd.AddCommand(newUpgradeCmd())
rootCmd.AddCommand(newScaleCmd())
rootCmd.AddCommand(newUpdateCmd())
rootCmd.AddCommand(newRotateCertsCmd())
rootCmd.AddCommand(newAddPoolCmd())
rootCmd.AddCommand(newGetLocationsCmd())
Expand Down
2 changes: 1 addition & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestNewRootCmd(t *testing.T) {
t.Fatalf("root command should have use %s equal %s, short %s equal %s and long %s equal to %s", command.Use, rootName, command.Short, rootShortDescription, command.Long, rootLongDescription)
}
// The commands need to be listed in alphabetical order
expectedCommands := []*cobra.Command{newAddPoolCmd(), getCompletionCmd(command), newDeployCmd(), newGenerateCmd(), newGetLocationsCmd(), newGetLogsCmd(), newGetSkusCmd(), newGetVersionsCmd(), newOrchestratorsCmd(), newRotateCertsCmd(), newScaleCmd(), newUpgradeCmd(), newVersionCmd()}
expectedCommands := []*cobra.Command{newAddPoolCmd(), getCompletionCmd(command), newDeployCmd(), newGenerateCmd(), newGetLocationsCmd(), newGetLogsCmd(), newGetSkusCmd(), newGetVersionsCmd(), newOrchestratorsCmd(), newRotateCertsCmd(), newScaleCmd(), newUpdateCmd(), newUpgradeCmd(), newVersionCmd()}
rc := command.Commands()

for i, c := range expectedCommands {
Expand Down
37 changes: 29 additions & 8 deletions cmd/scale.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ type scaleCmd struct {
agentPoolToScale string
masterFQDN string

// lib input
updateVMSSModel bool
validateCmd bool
loadAPIModel bool
persistAPIModel bool

// derived
containerService *api.ContainerService
apiVersion string
Expand All @@ -67,7 +73,12 @@ const (

// NewScaleCmd run a command to upgrade a Kubernetes cluster
func newScaleCmd() *cobra.Command {
sc := scaleCmd{}
sc := scaleCmd{
validateCmd: true,
updateVMSSModel: false,
loadAPIModel: true,
persistAPIModel: true,
}

scaleCmd := &cobra.Command{
Use: scaleName,
Expand Down Expand Up @@ -141,6 +152,7 @@ func (sc *scaleCmd) load() error {

ctx, cancel := context.WithTimeout(context.Background(), armhelpers.DefaultARMOperationTimeout)
defer cancel()
sc.updateVMSSModel = true

if sc.apiModelPath == "" {
sc.apiModelPath = filepath.Join(sc.deploymentDirectory, apiModelFilename)
Expand Down Expand Up @@ -236,11 +248,15 @@ func (sc *scaleCmd) load() error {
}

func (sc *scaleCmd) run(cmd *cobra.Command, args []string) error {
if err := sc.validate(cmd); err != nil {
return errors.Wrap(err, "failed to validate scale command")
if sc.validateCmd {
if err := sc.validate(cmd); err != nil {
return errors.Wrap(err, "failed to validate scale command")
}
}
if err := sc.load(); err != nil {
return errors.Wrap(err, "failed to load existing container service")
if sc.loadAPIModel {
if err := sc.load(); err != nil {
return errors.Wrap(err, "failed to load existing container service")
}
}

ctx, cancel := context.WithTimeout(context.Background(), armhelpers.DefaultARMOperationTimeout)
Expand Down Expand Up @@ -382,7 +398,9 @@ func (sc *scaleCmd) run(cmd *cobra.Command, args []string) error {
}
}

return sc.saveAPIModel()
if sc.persistAPIModel {
return sc.saveAPIModel()
}
}
} else {
for vmssListPage, err := sc.client.ListVirtualMachineScaleSets(ctx, sc.resourceGroupName); vmssListPage.NotDone(); err = vmssListPage.NextWithContext(ctx) {
Expand All @@ -409,7 +427,7 @@ func (sc *scaleCmd) run(cmd *cobra.Command, args []string) error {

if vmss.Sku != nil {
currentNodeCount = int(*vmss.Sku.Capacity)
if int(*vmss.Sku.Capacity) == sc.newDesiredAgentCount {
if int(*vmss.Sku.Capacity) == sc.newDesiredAgentCount && !sc.updateVMSSModel {
sc.printScaleTargetEqualsExisting(currentNodeCount)
return nil
} else if int(*vmss.Sku.Capacity) > sc.newDesiredAgentCount {
Expand Down Expand Up @@ -533,7 +551,10 @@ func (sc *scaleCmd) run(cmd *cobra.Command, args []string) error {
}
}

return sc.saveAPIModel()
if sc.persistAPIModel {
return sc.saveAPIModel()
}
return nil
}

func (sc *scaleCmd) saveAPIModel() error {
Expand Down
260 changes: 260 additions & 0 deletions cmd/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

package cmd

import (
"context"
"fmt"
"os"
"strconv"

"github.com/Azure/aks-engine/pkg/api"
"github.com/Azure/aks-engine/pkg/armhelpers"
"github.com/Azure/aks-engine/pkg/engine"
"github.com/Azure/aks-engine/pkg/helpers"
"github.com/Azure/aks-engine/pkg/i18n"
"github.com/leonelquinteros/gotext"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
)

type updateCmd struct {
authArgs

// user input
apiModelPath string
resourceGroupName string
location string
agentPoolToUpdate string

// derived
containerService *api.ContainerService
apiVersion string
agentPool *api.AgentPoolProfile
client armhelpers.AKSEngineClient
locale *gotext.Locale
nameSuffix string
agentPoolIndex int
logger *log.Entry
kubeconfig string
}

const (
updateName = "update"
updateShortDescription = "Update an existing AKS Engine-created VMSS node pool"
updateLongDescription = "Update an existing AKS Engine-created VMSS node pool in a Kubernetes cluster by updating its VMSS model"
)

// newUpdateCmd returns an instance reference of updateCmd
func newUpdateCmd() *cobra.Command {
uc := updateCmd{}

updateCmd := &cobra.Command{
Use: updateName,
Short: updateShortDescription,
Long: updateLongDescription,
RunE: uc.run,
}

f := updateCmd.Flags()
f.StringVarP(&uc.location, "location", "l", "", "location the cluster is deployed in")
f.StringVarP(&uc.resourceGroupName, "resource-group", "g", "", "the resource group where the cluster is deployed")
f.StringVarP(&uc.apiModelPath, "api-model", "m", "", "path to the generated apimodel.json file")
f.StringVar(&uc.agentPoolToUpdate, "node-pool", "", "node pool to scale")
addAuthFlags(&uc.authArgs, f)

return updateCmd
}

func (uc *updateCmd) validate(cmd *cobra.Command) error {
log.Debugln("validating update command line arguments...")
var err error

uc.locale, err = i18n.LoadTranslations()
if err != nil {
return errors.Wrap(err, "error loading translation files")
}

if uc.resourceGroupName == "" {
_ = cmd.Usage()
return errors.New("--resource-group must be specified")
}

if uc.location == "" {
_ = cmd.Usage()
return errors.New("--location must be specified")
}

uc.location = helpers.NormalizeAzureRegion(uc.location)

if uc.apiModelPath == "" {
_ = cmd.Usage()
return errors.New("--api-model must be specified")
}

if uc.agentPoolToUpdate == "" {
_ = cmd.Usage()
return errors.New("--node-pool must be specified")
}

return nil
}

func (uc *updateCmd) load() error {
logger := log.New()
logger.Formatter = new(prefixed.TextFormatter)
uc.logger = log.NewEntry(log.New())
var err error

ctx, cancel := context.WithTimeout(context.Background(), armhelpers.DefaultARMOperationTimeout)
defer cancel()

if _, err = os.Stat(uc.apiModelPath); os.IsNotExist(err) {
return errors.Errorf("specified api model does not exist (%s)", uc.apiModelPath)
}

apiloader := &api.Apiloader{
Translator: &i18n.Translator{
Locale: uc.locale,
},
}
uc.containerService, uc.apiVersion, err = apiloader.LoadContainerServiceFromFile(uc.apiModelPath, true, true, nil)
if err != nil {
return errors.Wrap(err, "error parsing the api model")
}

if uc.containerService.Properties.IsCustomCloudProfile() {
if err = writeCustomCloudProfile(uc.containerService); err != nil {
return errors.Wrap(err, "error writing custom cloud profile")
}

if err = uc.containerService.Properties.SetCustomCloudSpec(api.AzureCustomCloudSpecParams{IsUpgrade: false, IsScale: true}); err != nil {
return errors.Wrap(err, "error parsing the api model")
}
}

if err = uc.authArgs.validateAuthArgs(); err != nil {
return err
}

if uc.client, err = uc.authArgs.getClient(); err != nil {
return errors.Wrap(err, "failed to get client")
}

_, err = uc.client.EnsureResourceGroup(ctx, uc.resourceGroupName, uc.location, nil)
if err != nil {
return err
}

if uc.containerService.Location == "" {
uc.containerService.Location = uc.location
} else if uc.containerService.Location != uc.location {
return errors.New("--location does not match api model location")
}

agentPoolIndex := -1
for i, pool := range uc.containerService.Properties.AgentPoolProfiles {
if pool.Name == uc.agentPoolToUpdate {
agentPoolIndex = i
uc.agentPool = pool
uc.agentPoolIndex = i
}
}
if agentPoolIndex == -1 {
return errors.Errorf("node pool %s was not found in the deployed api model", uc.agentPoolToUpdate)
}
if uc.agentPool.AvailabilityProfile != api.VirtualMachineScaleSets {
return errors.Errorf("aks-engine node pool update requires a VMSS node pool, %s is backed by a VM Availability Set", uc.agentPoolToUpdate)
}

//allows to identify VMs in the resource group that belong to this cluster.
uc.nameSuffix = uc.containerService.Properties.GetClusterID()
log.Debugf("Cluster ID used in all agent pools: %s", uc.nameSuffix)

uc.kubeconfig, err = engine.GenerateKubeConfig(uc.containerService.Properties, uc.location)
if err != nil {
return errors.New("Unable to derive kubeconfig from api model")
}
return nil
}

func (uc *updateCmd) run(cmd *cobra.Command, args []string) error {
if err := uc.validate(cmd); err != nil {
return errors.Wrap(err, "failed to validate scale command")
}
if err := uc.load(); err != nil {
return errors.Wrap(err, "failed to load existing container service")
}

ctx, cancel := context.WithTimeout(context.Background(), armhelpers.DefaultARMOperationTimeout)
defer cancel()

sc := scaleCmd{
location: uc.location,
apiModelPath: uc.apiModelPath,
resourceGroupName: uc.resourceGroupName,
agentPoolToScale: uc.agentPoolToUpdate,
validateCmd: false,
updateVMSSModel: true,
loadAPIModel: false,
persistAPIModel: false,
}
sc.RawAzureEnvironment = uc.RawAzureEnvironment
sc.rawSubscriptionID = uc.rawSubscriptionID
sc.SubscriptionID = uc.SubscriptionID
sc.AuthMethod = uc.AuthMethod
sc.rawClientID = uc.rawClientID
sc.ClientID = uc.ClientID
sc.ClientSecret = uc.ClientSecret
sc.CertificatePath = uc.CertificatePath
sc.PrivateKeyPath = uc.PrivateKeyPath
sc.IdentitySystem = uc.IdentitySystem
sc.language = uc.language
sc.logger = uc.logger
sc.containerService = uc.containerService
sc.apiVersion = uc.apiVersion
sc.client = uc.client
sc.nameSuffix = uc.nameSuffix
sc.kubeconfig = uc.kubeconfig
sc.agentPool = uc.agentPool
sc.agentPoolIndex = uc.agentPoolIndex

for vmssListPage, err := sc.client.ListVirtualMachineScaleSets(ctx, sc.resourceGroupName); vmssListPage.NotDone(); err = vmssListPage.NextWithContext(ctx) {
if err != nil {
return errors.Wrap(err, "failed to get VMSS list in the resource group")
}
for _, vmss := range vmssListPage.Values() {
vmssName := *vmss.Name
if sc.agentPool.OSType == api.Windows {
possibleIndex, nameMungingErr := strconv.Atoi(vmssName[len(vmssName)-2:])
if nameMungingErr != nil {
continue
}
if !(sc.containerService.Properties.GetAgentVMPrefix(sc.agentPool, possibleIndex) == vmssName) {
continue
}
} else {
if !sc.vmInAgentPool(vmssName, vmss.Tags) {
continue
}
}

if vmss.Sku != nil {
sc.newDesiredAgentCount = int(*vmss.Sku.Capacity)
uc.agentPool.Count = sc.newDesiredAgentCount
} else {
return errors.Wrap(err, fmt.Sprintf("failed to find VMSS matching node pool %s in resource group %s", sc.agentPoolToScale, sc.resourceGroupName))
}
}
}

err := sc.run(cmd, args)
if err != nil {
return errors.Wrap(err, "aks-engine update failed")
}

return nil
}
Loading

0 comments on commit 4f4b8e7

Please sign in to comment.