pkg/clusters/nodepools.go (161 lines of code) (raw):
/*
Copyright © 2021 Google
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clusters
import (
"context"
"fmt"
"regexp"
"strings"
"legacymigration/pkg"
"legacymigration/pkg/operations"
log "github.com/sirupsen/logrus"
"go.uber.org/multierr"
"google.golang.org/api/container/v1"
)
var (
// Extract the location and name from an instance group manager parent path or URL.
instanceGroupManagerRegex = regexp.MustCompile(`projects/[^/]+/(?:zones|regions)/([^/]+)/instanceGroupManagers/([^/]+)$`)
)
type nodePoolMigrator struct {
*clusterMigrator
nodePool *container.NodePool
// Field(s) populated during Complete.
upgradeRequired bool
resolvedDesiredNodeVersion string
}
func NewNodePool(
clusterMigrator *clusterMigrator,
nodePool *container.NodePool) *nodePoolMigrator {
return &nodePoolMigrator{
clusterMigrator: clusterMigrator,
nodePool: nodePool,
}
}
// Complete finalizes the initialization of the nodePoolMigrator.
func (m *nodePoolMigrator) Complete(ctx context.Context) error {
var err error
m.upgradeRequired, err = m.isUpgradeRequired(ctx)
if err != nil {
return fmt.Errorf("unable to verify state for NodePool %s: %w", m.ResourcePath(), err)
}
def, valid := getVersions(m.serverConfig, m.releaseChannel, Node)
if m.opts.DesiredNodeVersion == DefaultVersion {
// Node pool upgrade using default alias selects the control plane version.
// See: https://cloud.google.com/kubernetes-engine/docs/reference/rest/v1/projects.zones.clusters.nodePools/update#request-body
def = m.resolvedDesiredControlPlaneVersion
}
m.resolvedDesiredNodeVersion, err = resolveVersion(m.opts.DesiredNodeVersion, def, valid)
if err != nil {
return m.wrap(err, "Complete")
}
return nil
}
// Validate ensures a NodePool upgrade is an allowed upgrade path.
func (m *nodePoolMigrator) Validate(_ context.Context) error {
if !m.upgradeRequired {
log.Infof("State of NodePool %s is valid; does not require an upgrade.", m.ResourcePath())
return nil
}
var (
desired = m.opts.DesiredNodeVersion
resolved = m.resolvedDesiredNodeVersion
current = m.nodePool.Version
// Wrap errors with node pool and method context.
wrap = func(err error) error {
return m.wrap(err, "Validation")
}
)
_, valid := getVersions(m.serverConfig, m.releaseChannel, Node)
if err := isUpgrade(resolved, current, valid, false); err != nil {
return wrap(err)
}
if err := IsWithinVersionSkew(resolved, m.resolvedDesiredControlPlaneVersion, MaxVersionSkew); err != nil {
return wrap(err)
}
log.Infof("Upgrade for NodePool %s is valid; desired: %q (%s), current: %s",
m.ResourcePath(), desired, resolved, current)
return nil
}
// Migrate performs a NodePool upgrade is deemed necessary.
func (m *nodePoolMigrator) Migrate(ctx context.Context) error {
return operations.WaitForOperationInProgress(ctx, m.migrate, m.wait)
}
func (m *nodePoolMigrator) migrate(ctx context.Context) error {
if !m.upgradeRequired {
log.Infof("Upgrade not required for NodePool %s; skipping upgrade.", m.ResourcePath())
return nil
}
log.Infof("Upgrading NodePool %s to version %q", m.ResourcePath(), m.resolvedDesiredNodeVersion)
return m.upgrade(ctx)
}
func (m *nodePoolMigrator) upgrade(ctx context.Context) error {
npp := m.ResourcePath()
req := &container.UpdateNodePoolRequest{
Name: npp,
NodeVersion: m.resolvedDesiredNodeVersion,
}
op, err := m.clients.Container.UpdateNodePool(ctx, req)
if err != nil {
return fmt.Errorf("error upgrading NodePool %s: %w", npp, err)
}
opPath := pkg.PathRegex.FindString(op.SelfLink)
log.Infof("Upgrade in progress for NodePool %s; operation: %s", npp, opPath)
w := &ContainerOperation{
ProjectID: m.projectID,
Path: opPath,
Client: m.clients.Container,
}
if err := m.handler.Wait(ctx, w); err != nil {
return fmt.Errorf("error waiting on Operation %s: %w", opPath, err)
}
log.Infof("NodePool %s upgraded. ", npp)
required, err := m.isUpgradeRequired(ctx)
if err != nil {
return fmt.Errorf("unable to verify post-upgrade state for NodePool %s: %w", m.ResourcePath(), err)
}
if required {
// This should not happen, as the cluster must first be successfully migrated.
return fmt.Errorf("state was not patched for NodePool %s", m.ResourcePath())
}
return nil
}
// ClusterPath formats identifying information about the cluster.
func (m *nodePoolMigrator) ResourcePath() string {
return pkg.NodePoolPath(m.projectID, m.cluster.Location, m.cluster.Name, m.nodePool.Name)
}
// isUpgradeRequired returns whether a the NodePool's state requires an upgrade.
func (m *nodePoolMigrator) isUpgradeRequired(ctx context.Context) (bool, error) {
var (
errors error
required bool
)
for _, url := range m.nodePool.InstanceGroupUrls {
res := instanceGroupManagerRegex.FindStringSubmatch(url)
if res == nil {
errors = multierr.Append(errors, fmt.Errorf("unable to parse location and name information from InstanceGroup URL (%s) for NodePool %s", url, m.ResourcePath()))
continue
}
igm, err := m.clients.Compute.GetInstanceGroupManager(ctx, m.projectID, res[1], res[2])
if err != nil {
errors = multierr.Append(errors, fmt.Errorf("error retrieving InstanceGroupManagers (%s) for NodePool %s: %w", url, m.ResourcePath(), err))
continue
}
it, err := m.clients.Compute.GetInstanceTemplate(ctx, m.projectID, getName(igm.InstanceTemplate))
if err != nil {
errors = multierr.Append(errors, fmt.Errorf("error retrieving GetInstanceTemplateResp %s for NodePool %s: %w", igm.InstanceTemplate, m.ResourcePath(), err))
continue
}
missing := true
for _, ni := range it.Properties.NetworkInterfaces {
if ni.Subnetwork != "" {
missing = false
break
}
}
if missing {
required = true
break
}
}
if errors != nil && !required {
return required, fmt.Errorf("error(s) encountered obtaining an InstanceTemplate for NodePool %s: %w", m.ResourcePath(), errors)
}
if errors != nil {
log.Infof("Error(s) retrieving InstanceTemplate(s) for NodePool %s: %v", m.ResourcePath(), errors)
}
return required, nil
}
// getName extracts the name portion of a resource's parent string
// e.g. getName("projects/x/locations/y/resources/z") -> "z"
func getName(path string) string {
s := strings.Split(path, "/")
return s[len(s)-1]
}
// wrap contextualizes errors during nodePoolMigrator methods
func (m *nodePoolMigrator) wrap(err error, stage string) error {
//goland:noinspection ALL
return fmt.Errorf("NodePool %s error during %s: %w", m.ResourcePath(), stage, err)
}