kops/upup/pkg/fi/dryrun_target.go

421 lines
10 KiB
Go

/*
Copyright 2016 The Kubernetes 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 fi
import (
"bytes"
"fmt"
"io"
"reflect"
"sort"
"strings"
"sync"
"github.com/golang/glog"
"k8s.io/kops/pkg/assets"
"k8s.io/kops/pkg/diff"
"k8s.io/kops/util/pkg/reflectutils"
)
// DryRunTarget is a special Target that does not execute anything, but instead tracks all changes.
// By running against a DryRunTarget, a list of changes that would be made can be easily collected,
// without any special support from the Tasks.
type DryRunTarget struct {
mutex sync.Mutex
changes []*render
deletions []Deletion
// The destination to which the final report will be printed on Finish()
out io.Writer
// assetBuilder records all assets used
assetBuilder *assets.AssetBuilder
}
type render struct {
a Task
aIsNil bool
e Task
changes Task
}
// ByTaskKey sorts []*render by TaskKey (type/name)
type ByTaskKey []*render
func (a ByTaskKey) Len() int { return len(a) }
func (a ByTaskKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByTaskKey) Less(i, j int) bool {
return buildTaskKey(a[i].e) < buildTaskKey(a[j].e)
}
// DeletionByTaskName sorts []Deletion by TaskName
type DeletionByTaskName []Deletion
func (a DeletionByTaskName) Len() int { return len(a) }
func (a DeletionByTaskName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a DeletionByTaskName) Less(i, j int) bool {
return a[i].TaskName() < a[i].TaskName()
}
var _ Target = &DryRunTarget{}
func NewDryRunTarget(assetBuilder *assets.AssetBuilder, out io.Writer) *DryRunTarget {
t := &DryRunTarget{}
t.out = out
t.assetBuilder = assetBuilder
return t
}
func (t *DryRunTarget) ProcessDeletions() bool {
// We display deletions
return true
}
func (t *DryRunTarget) Render(a, e, changes Task) error {
valA := reflect.ValueOf(a)
aIsNil := valA.IsNil()
t.mutex.Lock()
defer t.mutex.Unlock()
t.changes = append(t.changes, &render{
a: a,
aIsNil: aIsNil,
e: e,
changes: changes,
})
return nil
}
func (t *DryRunTarget) Delete(deletion Deletion) error {
t.mutex.Lock()
defer t.mutex.Unlock()
t.deletions = append(t.deletions, deletion)
return nil
}
func idForTask(taskMap map[string]Task, t Task) string {
for k, v := range taskMap {
if v == t {
// Skip task type, if present (taskType/taskName)
firstSlash := strings.Index(k, "/")
if firstSlash != -1 {
k = k[firstSlash+1:]
}
return k
}
}
glog.Fatalf("unknown task: %v", t)
return "?"
}
func (t *DryRunTarget) PrintReport(taskMap map[string]Task, out io.Writer) error {
b := &bytes.Buffer{}
if len(t.changes) != 0 {
var creates []*render
var updates []*render
for _, r := range t.changes {
if r.aIsNil {
creates = append(creates, r)
} else {
updates = append(updates, r)
}
}
// Give everything a consistent ordering
sort.Sort(ByTaskKey(creates))
sort.Sort(ByTaskKey(updates))
if len(creates) != 0 {
fmt.Fprintf(b, "Will create resources:\n")
for _, r := range creates {
taskName := getTaskName(r.changes)
fmt.Fprintf(b, " %s/%s\n", taskName, idForTask(taskMap, r.e))
changes := reflect.ValueOf(r.changes)
if changes.Kind() == reflect.Ptr && !changes.IsNil() {
changes = changes.Elem()
}
if changes.Kind() == reflect.Struct {
for i := 0; i < changes.NumField(); i++ {
field := changes.Field(i)
fieldName := changes.Type().Field(i).Name
if changes.Type().Field(i).PkgPath != "" {
// Not exported
continue
}
fieldValue := reflectutils.ValueAsString(field)
shouldPrint := true
if fieldName == "Name" {
// The field name is already printed above, no need to repeat it.
shouldPrint = false
}
if fieldName == "Lifecycle" {
// Lifecycle is a "system" field; no need to show it
shouldPrint = false
}
if fieldValue == "<nil>" || fieldValue == "<resource>" {
// Uninformative
shouldPrint = false
}
if fieldValue == "id:<nil>" {
// Uninformative, but we can often print the name instead
name := ""
if field.CanInterface() {
hasName, ok := field.Interface().(HasName)
if ok {
name = StringValue(hasName.GetName())
}
}
if name != "" {
fieldValue = "name:" + name
} else {
shouldPrint = false
}
}
if shouldPrint {
fmt.Fprintf(b, " \t%-20s\t%s\n", fieldName, fieldValue)
}
}
}
fmt.Fprintf(b, "\n")
}
}
if len(updates) != 0 {
fmt.Fprintf(b, "Will modify resources:\n")
// We can't use our reflection helpers here - we want corresponding values from a,e,c
for _, r := range updates {
changeList, err := buildChangeList(r.a, r.e, r.changes)
if err != nil {
return err
}
taskName := getTaskName(r.changes)
fmt.Fprintf(b, " %s/%s\n", taskName, idForTask(taskMap, r.e))
if len(changeList) == 0 {
fmt.Fprintf(b, " internal consistency error!\n")
fmt.Fprintf(b, " actual: %+v\n", r.a)
fmt.Fprintf(b, " expect: %+v\n", r.e)
continue
}
for _, change := range changeList {
lines := strings.Split(change.Description, "\n")
if len(lines) == 1 {
fmt.Fprintf(b, " \t%-20s\t%s\n", change.FieldName, change.Description)
} else {
fmt.Fprintf(b, " \t%-20s\n", change.FieldName)
for _, line := range lines {
fmt.Fprintf(b, " \t%-20s\t%s\n", "", line)
}
}
}
fmt.Fprintf(b, "\n")
}
}
}
if len(t.deletions) != 0 {
// Give everything a consistent ordering
sort.Sort(DeletionByTaskName(t.deletions))
fmt.Fprintf(b, "Will delete items:\n")
for _, d := range t.deletions {
fmt.Fprintf(b, " %-20s %s\n", d.TaskName(), d.Item())
}
}
if len(t.assetBuilder.ContainerAssets) != 0 {
glog.V(4).Infof("ContainerAssets:")
for _, a := range t.assetBuilder.ContainerAssets {
glog.V(4).Infof(" %s %s", a.DockerImage, a.CanonicalLocation)
}
}
if len(t.assetBuilder.FileAssets) != 0 {
glog.V(4).Infof("FileAssets:")
for _, a := range t.assetBuilder.FileAssets {
if a.FileURL != nil && a.CanonicalFileURL != nil {
glog.V(4).Infof(" %s %s", a.FileURL.String(), a.CanonicalFileURL.String())
} else if a.FileURL != nil {
glog.V(4).Infof(" %s", a.FileURL.String())
}
}
}
_, err := out.Write(b.Bytes())
return err
}
type change struct {
FieldName string
Description string
}
func buildChangeList(a, e, changes Task) ([]change, error) {
var changeList []change
valC := reflect.ValueOf(changes)
valA := reflect.ValueOf(a)
valE := reflect.ValueOf(e)
if valC.Kind() == reflect.Ptr && !valC.IsNil() {
valC = valC.Elem()
}
if valA.Kind() == reflect.Ptr && !valA.IsNil() {
valA = valA.Elem()
}
if valE.Kind() == reflect.Ptr && !valE.IsNil() {
valE = valE.Elem()
}
if valC.Kind() == reflect.Struct {
for i := 0; i < valC.NumField(); i++ {
if valC.Type().Field(i).PkgPath != "" {
// Not exported
continue
}
fieldValC := valC.Field(i)
changed := true
switch fieldValC.Kind() {
case reflect.Ptr, reflect.Interface, reflect.Slice, reflect.Map:
changed = !fieldValC.IsNil()
case reflect.String:
changed = fieldValC.Interface().(string) != ""
}
if !changed {
continue
}
if fieldValC.Kind() == reflect.String && fieldValC.Interface().(string) == "" {
// No change
continue
}
fieldValE := valE.Field(i)
description := ""
ignored := false
if fieldValE.CanInterface() {
fieldValA := valA.Field(i)
switch fieldValE.Interface().(type) {
//case SimpleUnit:
// ignored = true
case Resource, ResourceHolder:
resA, okA := tryResourceAsString(fieldValA)
resE, okE := tryResourceAsString(fieldValE)
if okA && okE {
description = diff.FormatDiff(resA, resE)
}
}
if !ignored && description == "" {
description = fmt.Sprintf(" %v -> %v", reflectutils.ValueAsString(fieldValA), reflectutils.ValueAsString(fieldValE))
}
}
if ignored {
continue
}
changeList = append(changeList, change{FieldName: valC.Type().Field(i).Name, Description: description})
}
} else {
return nil, fmt.Errorf("unhandled change type: %v", valC.Type())
}
return changeList, nil
}
func tryResourceAsString(v reflect.Value) (string, bool) {
if !v.IsValid() {
return "", false
}
if !v.CanInterface() {
return "", false
}
// Guard against nil interface go-tcha
if v.Kind() == reflect.Interface && v.IsNil() {
return "", false
}
if v.Kind() == reflect.Ptr && v.IsNil() {
return "", false
}
intf := v.Interface()
if res, ok := intf.(Resource); ok {
s, err := ResourceAsString(res)
if err != nil {
glog.Warningf("error converting to resource: %v", err)
return "", false
}
return s, true
}
if res, ok := intf.(*ResourceHolder); ok {
s, err := res.AsString()
if err != nil {
glog.Warningf("error converting to resource: %v", err)
return "", false
}
return s, true
}
return "", false
}
func getTaskName(t Task) string {
s := fmt.Sprintf("%T", t)
lastDot := strings.LastIndexByte(s, '.')
if lastDot != -1 {
s = s[lastDot+1:]
}
return s
}
// Finish is called at the end of a run, and prints a list of changes to the configured Writer
func (t *DryRunTarget) Finish(taskMap map[string]Task) error {
return t.PrintReport(taskMap, t.out)
}
// Changes returns all changed tasks
func (t *DryRunTarget) Changes() []Task {
var tasks []Task
for _, r := range t.changes {
if r.aIsNil {
tasks = append(tasks, r.changes)
}
}
return tasks
}
// HasChanges returns true iff any changes would have been made
func (t *DryRunTarget) HasChanges() bool {
return len(t.changes)+len(t.deletions) != 0
}