Skip to content

Commit

Permalink
Add migrate command for legacy HPAs
Browse files Browse the repository at this point in the history
There are current broken HPAs floating around that either use the legacy
oapi DeploymentConfig defintion (`v1.DeploymentConfig`) or the incorrect
API group, due to the webconsole (all scalables, but with the group as
`extensions/v1beta1`).  This introduces a migrate command that corrects
the ScaleTargetRef of those HPAs to have correct API groups that are
usable by generic scale clients.
  • Loading branch information
DirectXMan12 committed Feb 7, 2018
1 parent f2b6cd1 commit 54bb08a
Show file tree
Hide file tree
Showing 3 changed files with 290 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pkg/oc/admin/admin.go
Expand Up @@ -21,6 +21,7 @@ import (
migrateauthorization "github.com/openshift/origin/pkg/oc/admin/migrate/authorization"
migrateetcd "github.com/openshift/origin/pkg/oc/admin/migrate/etcd"
migrateimages "github.com/openshift/origin/pkg/oc/admin/migrate/images"
migratehpa "github.com/openshift/origin/pkg/oc/admin/migrate/legacyhpa"
migratestorage "github.com/openshift/origin/pkg/oc/admin/migrate/storage"
"github.com/openshift/origin/pkg/oc/admin/network"
"github.com/openshift/origin/pkg/oc/admin/node"
Expand Down Expand Up @@ -97,6 +98,7 @@ func NewCommandAdmin(name, fullName string, in io.Reader, out io.Writer, errout
migratestorage.NewCmdMigrateAPIStorage("storage", fullName+" "+migrate.MigrateRecommendedName+" storage", f, in, out, errout),
migrateauthorization.NewCmdMigrateAuthorization("authorization", fullName+" "+migrate.MigrateRecommendedName+" authorization", f, in, out, errout),
migrateetcd.NewCmdMigrateTTLs("etcd-ttl", fullName+" "+migrate.MigrateRecommendedName+" etcd-ttl", f, in, out, errout),
migratehpa.NewCmdMigrateLegacyHPA("legacy-hpa", fullName+" "+migrate.MigrateRecommendedName+" legacy-hpa", f, in, out, errout),
),
top.NewCommandTop(top.TopRecommendedName, fullName+" "+top.TopRecommendedName, f, out, errout),
image.NewCmdVerifyImageSignature(name, fullName+" "+image.VerifyRecommendedName, f, out, errout),
Expand Down
209 changes: 209 additions & 0 deletions pkg/oc/admin/migrate/legacyhpa/hpa.go
@@ -0,0 +1,209 @@
package legacyhpa

import (
"fmt"
"io"
"strings"

"github.com/spf13/cobra"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
autoscaling "k8s.io/kubernetes/pkg/apis/autoscaling"
autoscalingclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/autoscaling/internalversion"
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource"

"github.com/openshift/origin/pkg/oc/admin/migrate"
"github.com/openshift/origin/pkg/oc/cli/util/clientcmd"
)

var (
defaultMigrations = map[versionKind]versionKind{
// legacy oapi group
{"v1", "DeploymentConfig"}: {"apps.openshift.io/v1", "DeploymentConfig"},

// webconsole shenaniganry
{"extensions/v1beta1", "DeploymentConfig"}: {"apps.openshift.io/v1", "DeploymentConfig"},
{"extensions/v1beta1", "Deployment"}: {"apps/v1", "Deployment"},
{"extensions/v1beta1", "ReplicaSet"}: {"apps/v1", "ReplicaSet"},
{"extensions/v1beta1", "ReplicationController"}: {"v1", "ReplicationController"},
}

internalMigrateLegacyHPALong = templates.LongDesc(fmt.Sprintf(`
Migrate Horizontal Pod Autoscalers to refer to new API groups
This command locates and updates every Horizontal Pod Autoscaler which refers to a particular
group-version-kind to refer to some other, equivalent group-version-kind.
The following transformations will occur:
%s`, prettyPrintMigrations(defaultMigrations)))

internalMigrateLegacyHPAExample = templates.Examples(`
# Perform a dry-run of updating all objects
%[1]s
# To actually perform the update, the confirm flag must be appended
%[1]s --confirm
# Migrate a specific group-version-kind to the latest preferred version
%[1]s --initial=extensions/v1beta1.ReplicaSet --confirm
# Migrate a specific group-version-kind to a specific group-version-kind
%[1]s --initial=v1.DeploymentConfig --final=apps.openshift.io/v1.DeploymentConfig --confirm`)
)

func prettyPrintMigrations(versionKinds map[versionKind]versionKind) string {
lines := make([]string, 0, len(versionKinds))
for initial, final := range versionKinds {
line := fmt.Sprintf(" - %s.%s --> %s.%s", initial.APIVersion, initial.Kind, final.APIVersion, final.Kind)
lines = append(lines, line)
}

return strings.Join(lines, "\n")
}

type versionKind struct {
APIVersion string
Kind string
}

func (vk versionKind) GroupVersionKind() (schema.GroupVersionKind, error) {
groupVer, err := schema.ParseGroupVersion(vk.APIVersion)
if err != nil {
return schema.GroupVersionKind{}, err
}
return groupVer.WithKind(vk.Kind), nil
}

type MigrateLegacyHPAOptions struct {
// maps initial gvks to final gvks in the same format
// as HPAs use (CrossVersionObjectReferences) for ease of access.
finalVersionKinds map[versionKind]versionKind

hpaClient autoscalingclient.AutoscalingInterface

migrate.ResourceOptions
}

func (o *MigrateLegacyHPAOptions) Bind(c *cobra.Command) {
o.ResourceOptions.Bind(c)
}

// NewCmdMigrateLegacyAPI implements a MigrateLegacyHPA command
func NewCmdMigrateLegacyHPA(name, fullName string, f *clientcmd.Factory, in io.Reader, out, errout io.Writer) *cobra.Command {
options := &MigrateLegacyHPAOptions{
ResourceOptions: migrate.ResourceOptions{
In: in,
Out: out,
ErrOut: errout,

AllNamespaces: true,
Include: []string{"horizontalpodautoscalers.autoscaling"},
},
}
cmd := &cobra.Command{
Use: name,
Short: "Update HPAs to point to the latest group-version-kinds",
Long: internalMigrateLegacyHPALong,
Example: fmt.Sprintf(internalMigrateLegacyHPAExample, fullName),
Run: func(cmd *cobra.Command, args []string) {
kcmdutil.CheckErr(options.Complete(name, f, cmd, args))
kcmdutil.CheckErr(options.Validate())
kcmdutil.CheckErr(options.Run())
},
}
options.Bind(cmd)

return cmd
}

func (o *MigrateLegacyHPAOptions) Complete(name string, f *clientcmd.Factory, c *cobra.Command, args []string) error {
if len(args) != 0 {
return fmt.Errorf("%s takes no positional arguments", name)
}

o.ResourceOptions.SaveFn = o.save
if err := o.ResourceOptions.Complete(f, c); err != nil {
return err
}

o.finalVersionKinds = make(map[versionKind]versionKind)

// copy all manual transformations in
for initial, final := range defaultMigrations {
o.finalVersionKinds[initial] = final
}

kubeClientSet, err := f.ClientSet()
if err != nil {
return err
}
o.hpaClient = kubeClientSet.Autoscaling()

return nil
}

func (o MigrateLegacyHPAOptions) Validate() error {
return o.ResourceOptions.Validate()
}

func (o MigrateLegacyHPAOptions) Run() error {
return o.ResourceOptions.Visitor().Visit(func(info *resource.Info) (migrate.Reporter, error) {
return o.checkAndTransform(info.Object)
})
}

func (o *MigrateLegacyHPAOptions) checkAndTransform(hpaRaw runtime.Object) (migrate.Reporter, error) {
var hpa *autoscaling.HorizontalPodAutoscaler
var wasHPA bool
if hpa, wasHPA = hpaRaw.(*autoscaling.HorizontalPodAutoscaler); !wasHPA {
return nil, fmt.Errorf("unrecognized object %#v", hpaRaw)
}

currentVersionKind := versionKind{
APIVersion: hpa.Spec.ScaleTargetRef.APIVersion,
Kind: hpa.Spec.ScaleTargetRef.Kind,
}

newVersionKind, err := o.latestVersionKind(currentVersionKind)
if err != nil {
return nil, err
}

if currentVersionKind != newVersionKind {
hpa.Spec.ScaleTargetRef.APIVersion = newVersionKind.APIVersion
hpa.Spec.ScaleTargetRef.Kind = newVersionKind.Kind
return migrate.ReporterBool(true), nil
}

return migrate.ReporterBool(false), nil
}

func (o *MigrateLegacyHPAOptions) latestVersionKind(current versionKind) (versionKind, error) {
if newVersionKind, isKnown := o.finalVersionKinds[current]; isKnown {
return newVersionKind, nil
}

return current, nil
}

// save invokes the API to alter an object. The reporter passed to this method is the same returned by
// the migration visitor method. It should return an error if the input type cannot be saved
// It returns migrate.ErrRecalculate if migration should be re-run on the provided object.
func (o *MigrateLegacyHPAOptions) save(info *resource.Info, reporter migrate.Reporter) error {
var hpa *autoscaling.HorizontalPodAutoscaler
var wasHPA bool
if hpa, wasHPA = info.Object.(*autoscaling.HorizontalPodAutoscaler); !wasHPA {
return fmt.Errorf("unrecognized object %#v", info.Object)
}

updatedHPA, err := o.hpaClient.HorizontalPodAutoscalers(hpa.Namespace).Update(hpa)
if err != nil {
return migrate.DefaultRetriable(info, err)
}
info.Refresh(updatedHPA, true)
return nil
}
79 changes: 79 additions & 0 deletions pkg/oc/admin/migrate/legacyhpa/hpa_test.go
@@ -0,0 +1,79 @@
package legacyhpa

import (
"testing"

"github.com/stretchr/testify/assert"

autoscaling "k8s.io/kubernetes/pkg/apis/autoscaling"
)

func TestDefaultMigrations(t *testing.T) {
testCases := []struct {
name string
input versionKind
output versionKind
}{
{
name: "legacy-dc",
input: versionKind{"v1", "DeploymentConfig"},
output: versionKind{"apps.openshift.io/v1", "DeploymentConfig"},
},
{
name: "console-dc",
input: versionKind{"extensions/v1beta1", "DeploymentConfig"},
output: versionKind{"apps.openshift.io/v1", "DeploymentConfig"},
},
{
name: "console-rc",
input: versionKind{"extensions/v1beta1", "ReplicationController"},
output: versionKind{"v1", "ReplicationController"},
},
{
name: "console-deploy",
input: versionKind{"extensions/v1beta1", "Deployment"},
output: versionKind{"apps/v1", "Deployment"},
},
{
name: "console-rs",
input: versionKind{"extensions/v1beta1", "ReplicaSet"},
output: versionKind{"apps/v1", "ReplicaSet"},
},
{
name: "ok-dc",
input: versionKind{"apps.openshift.io/v1", "DeploymentConfig"},
output: versionKind{"apps.openshift.io/v1", "DeploymentConfig"},
},
{
name: "other",
input: versionKind{"cheese/v1alpha1", "Cheddar"},
output: versionKind{"cheese/v1alpha1", "Cheddar"},
},
}

opts := MigrateLegacyHPAOptions{
finalVersionKinds: defaultMigrations,
}

for _, tc := range testCases {
oldHPA := &autoscaling.HorizontalPodAutoscaler{
Spec: autoscaling.HorizontalPodAutoscalerSpec{
ScaleTargetRef: autoscaling.CrossVersionObjectReference{
APIVersion: tc.input.APIVersion,
Kind: tc.input.Kind,
Name: tc.name,
},
},
}

reporter, err := opts.checkAndTransform(oldHPA)
if !assert.NoError(t, err, "%s: expected not to produce an error", tc.name) {
continue
}

assert.Equal(t, tc.input != tc.output, reporter.Changed(), "%s: expected to properly indicate whether or not it the HPA was changed", tc.name)

newVersionKind := versionKind{oldHPA.Spec.ScaleTargetRef.APIVersion, oldHPA.Spec.ScaleTargetRef.Kind}
assert.Equal(t, tc.output, newVersionKind, "%s: expected the HPA to be updated to the correct group-version-kind", tc.name)
}
}

0 comments on commit 54bb08a

Please sign in to comment.