forked from istio/operator
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add package for upgrade hooks (istio#385)
* Add package for upgrade hooks * Remove prometheus log * Lint * Update version range * Fix dummy integration
- Loading branch information
1 parent
8c48d37
commit b658413
Showing
2 changed files
with
311 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
// Copyright 2019 Istio Authors | ||
// | ||
// 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 hooks | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
"runtime" | ||
"strings" | ||
|
||
v1 "k8s.io/api/core/v1" | ||
|
||
"github.com/hashicorp/go-version" | ||
|
||
"istio.io/operator/pkg/apis/istio/v1alpha2" | ||
"istio.io/operator/pkg/util" | ||
"istio.io/pkg/log" | ||
) | ||
|
||
// ExecClient is an interface for remote execution | ||
// TODO: replace with correct type. | ||
type ExecClient interface { | ||
PodsForSelector(namespace, labelSelector string) (*v1.PodList, error) | ||
} | ||
|
||
// hook is a callout function that may be called during an upgrade to check state or modify the cluster. | ||
// hooks should only be used for version-specific actions. | ||
type hook func(kubeClient ExecClient, sourceValues, targetValues *v1alpha2.IstioControlPlaneSpec) util.Errors | ||
type hooks []hook | ||
|
||
// hookVersionMapping is a mapping between a hashicorp/go-version formatted constraints for the source and target | ||
// versions and the list of hooks that should be run if the constraints match. | ||
type hookVersionMapping struct { | ||
sourceVersionConstraint string | ||
targetVersionConstraint string | ||
hooks hooks | ||
} | ||
|
||
// hookCommonParams is a set of common params passed to all hooks. | ||
type hookCommonParams struct { | ||
sourceVer string | ||
targetVer string | ||
sourceValues *v1alpha2.IstioControlPlaneSpec | ||
targetValues *v1alpha2.IstioControlPlaneSpec | ||
} | ||
|
||
var ( | ||
// preUpgradeHooks is a list of hook version constraint pairs mapping to a slide of corresponding hooks to run | ||
// before upgrade. | ||
preUpgradeHooks = []hookVersionMapping{ | ||
{ | ||
sourceVersionConstraint: ">=1.3", | ||
targetVersionConstraint: ">=1.3", | ||
hooks: []hook{checkInitCrdJobs}, | ||
}, | ||
} | ||
// postUpgradeHooks is a list of hook version constraint pairs mapping to a slide of corresponding hooks to run | ||
// before upgrade. | ||
postUpgradeHooks []hookVersionMapping | ||
) | ||
|
||
func runPreUpgradeHooks(kubeClient ExecClient, hc *hookCommonParams, dryRun bool) util.Errors { | ||
return runUpgradeHooks(preUpgradeHooks, kubeClient, hc, dryRun) | ||
} | ||
|
||
func runPostUpgradeHooks(kubeClient ExecClient, hc *hookCommonParams, dryRun bool) util.Errors { | ||
return runUpgradeHooks(postUpgradeHooks, kubeClient, hc, dryRun) | ||
} | ||
|
||
// runUpgradeHooks checks a list of hook version map entries and runs the hooks in each entry whose constraints match | ||
// the source/target versions in hc. | ||
func runUpgradeHooks(hml []hookVersionMapping, kubeClient ExecClient, hc *hookCommonParams, dryRun bool) util.Errors { | ||
var errs util.Errors | ||
_, err := version.NewVersion(hc.sourceVer) | ||
if err != nil { | ||
return util.NewErrs(err) | ||
} | ||
_, err = version.NewVersion(hc.targetVer) | ||
if err != nil { | ||
return util.NewErrs(err) | ||
} | ||
|
||
for _, h := range hml { | ||
matches, err := checkHookListEntry(h, hc) | ||
if err != nil { | ||
errs = util.AppendErr(errs, err) | ||
continue | ||
} | ||
if !matches { | ||
continue | ||
} | ||
log.Infof("Running the following hooks which match source->target versions %s->%s: %s", hc.sourceVer, hc.targetVer, h.hooks) | ||
if dryRun { | ||
log.Info("(Skipping running hooks due to dry-run being set.)") | ||
continue | ||
} | ||
for _, hf := range h.hooks { | ||
log.Infof("Running hook %s", hf) | ||
errs = util.AppendErrs(errs, hf(kubeClient, hc.sourceValues, hc.targetValues)) | ||
} | ||
} | ||
return errs | ||
} | ||
|
||
// checkHookListEntry checks a hookVersionMapping against the source/target versions in hc and returns true if it | ||
// matches. | ||
func checkHookListEntry(h hookVersionMapping, hc *hookCommonParams) (bool, error) { | ||
ch, err := checkConstraint(hc.sourceVer, h.sourceVersionConstraint) | ||
if err != nil { | ||
return false, err | ||
} | ||
if !ch { | ||
log.Infof("Source version %s does not satisfy source constraint %s, skip hooks", hc.sourceVer, h.sourceVersionConstraint) | ||
return false, nil | ||
} | ||
|
||
ch, err = checkConstraint(hc.targetVer, h.targetVersionConstraint) | ||
if err != nil { | ||
return false, err | ||
} | ||
if !ch { | ||
log.Infof("Target version %s does not satisfy target constraint %s, skip hooks", hc.targetVer, h.targetVersionConstraint) | ||
return false, nil | ||
} | ||
return true, nil | ||
} | ||
|
||
// checkConstraint reports whether SemVer formatted string verStr matches hashicorp/go-version formatted constraints | ||
// in constraintStr. | ||
func checkConstraint(verStr, constraintStr string) (bool, error) { | ||
ver, err := version.NewVersion(verStr) | ||
if err != nil { | ||
return false, err | ||
} | ||
constraint, err := version.NewConstraint(constraintStr) | ||
if err != nil { | ||
return false, err | ||
} | ||
return constraint.Check(ver), nil | ||
} | ||
|
||
func checkInitCrdJobs(kubeClient ExecClient, currentValues, _ *v1alpha2.IstioControlPlaneSpec) util.Errors { | ||
pl, err := kubeClient.PodsForSelector(currentValues.DefaultNamespace, "") | ||
if err != nil { | ||
return util.NewErrs(fmt.Errorf("failed to list pods: %v", err)) | ||
} | ||
|
||
for _, p := range pl.Items { | ||
if strings.Contains(p.Name, "istio-init-crd") { | ||
return util.NewErrs(fmt.Errorf("istio-init-crd pods exist: %v. Istio was installed with non-operator methods, "+ | ||
"please migrate to operator installation first", p.Name)) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (h hooks) String() string { | ||
var out []string | ||
for _, hh := range h { | ||
out = append(out, hh.String()) | ||
} | ||
return strings.Join(out, ", ") | ||
} | ||
|
||
func (h hook) String() string { | ||
return getFunctionName(h) | ||
} | ||
|
||
func getFunctionName(i interface{}) string { | ||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// Copyright 2019 Istio Authors | ||
// | ||
// 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 hooks | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/pkg/errors" | ||
|
||
"istio.io/operator/pkg/apis/istio/v1alpha2" | ||
"istio.io/operator/pkg/util" | ||
) | ||
|
||
var ( | ||
err1 = fmt.Errorf("err1") | ||
err2 = fmt.Errorf("err2") | ||
err3 = fmt.Errorf("err3") | ||
) | ||
|
||
func init() { | ||
// TODO: remove when integrated, this is just to silence lint. | ||
hc := hookCommonParams{} | ||
_ = runPreUpgradeHooks(nil, &hc, true) | ||
_ = runPostUpgradeHooks(nil, &hc, true) | ||
} | ||
|
||
func h1(_ ExecClient, _, _ *v1alpha2.IstioControlPlaneSpec) util.Errors { | ||
return util.NewErrs(err1) | ||
} | ||
func h2(_ ExecClient, _, _ *v1alpha2.IstioControlPlaneSpec) util.Errors { | ||
return util.NewErrs(err2) | ||
} | ||
func h3(_ ExecClient, _, _ *v1alpha2.IstioControlPlaneSpec) util.Errors { | ||
return util.NewErrs(err3) | ||
} | ||
|
||
func TestRunUpgradeHooks(t *testing.T) { | ||
testUpgradeHooks := []hookVersionMapping{ | ||
{ | ||
sourceVersionConstraint: ">0", | ||
targetVersionConstraint: ">0", | ||
hooks: []hook{h1}, | ||
}, | ||
{ | ||
sourceVersionConstraint: ">=1.3, <1.4", | ||
targetVersionConstraint: ">=1.5", | ||
hooks: []hook{h2}, | ||
}, | ||
{ | ||
sourceVersionConstraint: ">=1.5", | ||
targetVersionConstraint: ">=1.5", | ||
hooks: []hook{h3}, | ||
}, | ||
} | ||
|
||
malformedStr := "Malformed version: bad ver" | ||
tests := []struct { | ||
desc string | ||
sourceVer string | ||
targetVer string | ||
dryRun bool | ||
wantErrs util.Errors | ||
}{ | ||
{ | ||
desc: "bad ver", | ||
sourceVer: "bad ver", | ||
targetVer: "1.3", | ||
wantErrs: util.Errors{errors.New(malformedStr)}, | ||
}, | ||
{ | ||
desc: "h1", | ||
sourceVer: "1.2", | ||
targetVer: "1.3", | ||
wantErrs: util.Errors{err1}, | ||
}, | ||
{ | ||
desc: "h2 boundary outside", | ||
sourceVer: "1.4", | ||
targetVer: "1.5", | ||
wantErrs: util.Errors{err1}, | ||
}, | ||
{ | ||
desc: "h2 boundary inside", | ||
sourceVer: "1.3", | ||
targetVer: "1.5", | ||
wantErrs: util.Errors{err1, err2}, | ||
}, | ||
{ | ||
desc: "h2 range", | ||
sourceVer: "1.3.5", | ||
targetVer: "1.6.1", | ||
wantErrs: util.Errors{err1, err2}, | ||
}, | ||
{ | ||
desc: "h3 range", | ||
sourceVer: "1.5.2", | ||
targetVer: "1.5.1", | ||
wantErrs: util.Errors{err1, err3}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.desc, func(t *testing.T) { | ||
hc := hookCommonParams{ | ||
sourceVer: tt.sourceVer, | ||
targetVer: tt.targetVer, | ||
} | ||
gotErrs := runUpgradeHooks(testUpgradeHooks, nil, &hc, tt.dryRun) | ||
if !util.EqualErrors(gotErrs, tt.wantErrs) { | ||
t.Errorf("%s: got: %s, wantErrs: %s", tt.desc, gotErrs.String(), tt.wantErrs.String()) | ||
} | ||
}) | ||
} | ||
} |