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

feat: update VMSS node pools #3830

Merged
merged 11 commits into from
Sep 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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