mirror of https://github.com/kubernetes/kops.git
GCE: Allow updating the instance group template
We follow the same pattern as on AWS, where we create a new template, because the templates are immutable.
This commit is contained in:
parent
85b308b3e8
commit
5e5e37e2e2
|
|
@ -390,6 +390,12 @@ func ShortenImageURL(defaultProject string, imageURL string) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
type terraformInstance struct {
|
||||
terraformInstanceCommon
|
||||
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (_ *Instance) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Instance) error {
|
||||
project := t.Project
|
||||
|
||||
|
|
@ -404,13 +410,13 @@ func (_ *Instance) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *
|
|||
return err
|
||||
}
|
||||
|
||||
tf := &terraformInstanceTemplate{
|
||||
Name: i.Name,
|
||||
CanIPForward: i.CanIpForward,
|
||||
MachineType: lastComponent(i.MachineType),
|
||||
Zone: i.Zone,
|
||||
Tags: i.Tags.Items,
|
||||
tf := &terraformInstance{
|
||||
Name: i.Name,
|
||||
}
|
||||
tf.CanIPForward = i.CanIpForward
|
||||
tf.MachineType = lastComponent(i.MachineType)
|
||||
tf.Zone = i.Zone
|
||||
tf.Tags = i.Tags.Items
|
||||
|
||||
// TF requires zone
|
||||
if tf.Zone == "" && e.Zone != nil {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ func (e *InstanceGroupManager) Find(c *fi.Context) (*InstanceGroupManager, error
|
|||
actual.Zone = fi.String(lastComponent(r.Zone))
|
||||
actual.BaseInstanceName = &r.BaseInstanceName
|
||||
actual.TargetSize = &r.TargetSize
|
||||
actual.InstanceTemplate = &InstanceTemplate{Name: fi.String(lastComponent(r.InstanceTemplate))}
|
||||
actual.InstanceTemplate = &InstanceTemplate{ID: fi.String(lastComponent(r.InstanceTemplate))}
|
||||
|
||||
for _, targetPool := range r.TargetPools {
|
||||
actual.TargetPools = append(actual.TargetPools, &TargetPool{
|
||||
|
|
@ -82,12 +82,17 @@ func (_ *InstanceGroupManager) CheckChanges(a, e, changes *InstanceGroupManager)
|
|||
func (_ *InstanceGroupManager) RenderGCE(t *gce.GCEAPITarget, a, e, changes *InstanceGroupManager) error {
|
||||
project := t.Cloud.Project
|
||||
|
||||
instanceTemplateURL, err := e.InstanceTemplate.URL(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i := &compute.InstanceGroupManager{
|
||||
Name: *e.Name,
|
||||
Zone: *e.Zone,
|
||||
BaseInstanceName: *e.BaseInstanceName,
|
||||
TargetSize: *e.TargetSize,
|
||||
InstanceTemplate: e.InstanceTemplate.URL(project),
|
||||
InstanceTemplate: instanceTemplateURL,
|
||||
}
|
||||
|
||||
for _, targetPool := range e.TargetPools {
|
||||
|
|
@ -121,6 +126,22 @@ func (_ *InstanceGroupManager) RenderGCE(t *gce.GCEAPITarget, a, e, changes *Ins
|
|||
changes.TargetPools = nil
|
||||
}
|
||||
|
||||
if changes.InstanceTemplate != nil {
|
||||
request := &compute.InstanceGroupManagersSetInstanceTemplateRequest{
|
||||
InstanceTemplate: instanceTemplateURL,
|
||||
}
|
||||
op, err := t.Cloud.Compute.InstanceGroupManagers.SetInstanceTemplate(t.Cloud.Project, *e.Zone, i.Name, request).Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating InstanceTemplate for InstanceGroupManager: %v", err)
|
||||
}
|
||||
|
||||
if err := t.Cloud.WaitForOp(op); err != nil {
|
||||
return fmt.Errorf("error updating InstanceTemplate for InstanceGroupManager: %v", err)
|
||||
}
|
||||
|
||||
changes.InstanceTemplate = nil
|
||||
}
|
||||
|
||||
empty := &InstanceGroupManager{}
|
||||
if !reflect.DeepEqual(empty, changes) {
|
||||
return fmt.Errorf("Cannot apply changes to InstanceGroupManager: %v", changes)
|
||||
|
|
|
|||
|
|
@ -18,18 +18,24 @@ package gcetasks
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
compute "google.golang.org/api/compute/v0.beta"
|
||||
"k8s.io/kops/pkg/diff"
|
||||
"k8s.io/kops/upup/pkg/fi"
|
||||
"k8s.io/kops/upup/pkg/fi/cloudup/gce"
|
||||
"k8s.io/kops/upup/pkg/fi/cloudup/terraform"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InstanceTemplate represents a GCE InstanceTemplate
|
||||
//go:generate fitask -type=InstanceTemplate
|
||||
type InstanceTemplate struct {
|
||||
// Name will be used for as the name prefix
|
||||
Name *string
|
||||
Network *Network
|
||||
Tags []string
|
||||
|
|
@ -47,18 +53,21 @@ type InstanceTemplate struct {
|
|||
|
||||
Metadata map[string]*fi.ResourceHolder
|
||||
MachineType *string
|
||||
|
||||
// ID is the actual name
|
||||
ID *string
|
||||
}
|
||||
|
||||
var _ fi.CompareWithID = &InstanceTemplate{}
|
||||
|
||||
func (e *InstanceTemplate) CompareWithID() *string {
|
||||
return e.Name
|
||||
return e.ID
|
||||
}
|
||||
|
||||
func (e *InstanceTemplate) Find(c *fi.Context) (*InstanceTemplate, error) {
|
||||
cloud := c.Cloud.(*gce.GCECloud)
|
||||
|
||||
r, err := cloud.Compute.InstanceTemplates.Get(cloud.Project, *e.Name).Do()
|
||||
response, err := cloud.Compute.InstanceTemplates.List(cloud.Project).Do()
|
||||
if err != nil {
|
||||
if gce.IsNotFound(err) {
|
||||
return nil, nil
|
||||
|
|
@ -66,69 +75,94 @@ func (e *InstanceTemplate) Find(c *fi.Context) (*InstanceTemplate, error) {
|
|||
return nil, fmt.Errorf("error listing InstanceTemplates: %v", err)
|
||||
}
|
||||
|
||||
actual := &InstanceTemplate{}
|
||||
actual.Name = &r.Name
|
||||
|
||||
p := r.Properties
|
||||
|
||||
for _, tag := range p.Tags.Items {
|
||||
actual.Tags = append(actual.Tags, tag)
|
||||
}
|
||||
actual.MachineType = fi.String(lastComponent(p.MachineType))
|
||||
actual.CanIPForward = &p.CanIpForward
|
||||
|
||||
bootDiskImage, err := ShortenImageURL(cloud.Project, p.Disks[0].InitializeParams.SourceImage)
|
||||
expected, err := e.mapToGCE(cloud.Project)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing source image URL: %v", err)
|
||||
}
|
||||
actual.BootDiskImage = fi.String(bootDiskImage)
|
||||
actual.BootDiskType = &p.Disks[0].InitializeParams.DiskType
|
||||
actual.BootDiskSizeGB = &p.Disks[0].InitializeParams.DiskSizeGb
|
||||
|
||||
if p.Scheduling != nil {
|
||||
actual.Preemptible = &p.Scheduling.Preemptible
|
||||
}
|
||||
if len(p.NetworkInterfaces) != 0 {
|
||||
ni := p.NetworkInterfaces[0]
|
||||
actual.Network = &Network{Name: fi.String(lastComponent(ni.Network))}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, serviceAccount := range p.ServiceAccounts {
|
||||
for _, scope := range serviceAccount.Scopes {
|
||||
actual.Scopes = append(actual.Scopes, scopeToShortForm(scope))
|
||||
for _, r := range response.Items {
|
||||
if !strings.HasPrefix(r.Name, fi.StringValue(e.Name)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
//for i, disk := range p.Disks {
|
||||
// if i == 0 {
|
||||
// source := disk.Source
|
||||
//
|
||||
// // TODO: Parse source URL instead of assuming same project/zone?
|
||||
// name := lastComponent(source)
|
||||
// d, err := cloud.Compute.Disks.Get(cloud.Project, *e.Zone, name).Do()
|
||||
// if err != nil {
|
||||
// if gce.IsNotFound(err) {
|
||||
// return nil, fmt.Errorf("disk not found %q: %v", source, err)
|
||||
// }
|
||||
// return nil, fmt.Errorf("error querying for disk %q: %v", source, err)
|
||||
// } else {
|
||||
// imageURL, err := gce.ParseGoogleCloudURL(d.SourceImage)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("unable to parse image URL: %q", d.SourceImage)
|
||||
// }
|
||||
// actual.Image = fi.String(imageURL.Project + "/" + imageURL.Name)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
if p.Metadata != nil {
|
||||
actual.Metadata = make(map[string]*fi.ResourceHolder)
|
||||
for _, meta := range p.Metadata.Items {
|
||||
actual.Metadata[meta.Key] = fi.WrapResource(fi.NewStringResource(meta.Value))
|
||||
if !matches(expected, r) {
|
||||
continue
|
||||
}
|
||||
|
||||
actual := &InstanceTemplate{}
|
||||
|
||||
p := r.Properties
|
||||
|
||||
for _, tag := range p.Tags.Items {
|
||||
actual.Tags = append(actual.Tags, tag)
|
||||
}
|
||||
actual.MachineType = fi.String(lastComponent(p.MachineType))
|
||||
actual.CanIPForward = &p.CanIpForward
|
||||
|
||||
bootDiskImage, err := ShortenImageURL(cloud.Project, p.Disks[0].InitializeParams.SourceImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing source image URL: %v", err)
|
||||
}
|
||||
actual.BootDiskImage = fi.String(bootDiskImage)
|
||||
actual.BootDiskType = &p.Disks[0].InitializeParams.DiskType
|
||||
actual.BootDiskSizeGB = &p.Disks[0].InitializeParams.DiskSizeGb
|
||||
|
||||
if p.Scheduling != nil {
|
||||
actual.Preemptible = &p.Scheduling.Preemptible
|
||||
}
|
||||
if len(p.NetworkInterfaces) != 0 {
|
||||
ni := p.NetworkInterfaces[0]
|
||||
actual.Network = &Network{Name: fi.String(lastComponent(ni.Network))}
|
||||
}
|
||||
|
||||
for _, serviceAccount := range p.ServiceAccounts {
|
||||
for _, scope := range serviceAccount.Scopes {
|
||||
actual.Scopes = append(actual.Scopes, scopeToShortForm(scope))
|
||||
}
|
||||
}
|
||||
|
||||
// When we deal with additional disks (local disks), we'll need to map them like this...
|
||||
//for i, disk := range p.Disks {
|
||||
// if i == 0 {
|
||||
// source := disk.Source
|
||||
//
|
||||
// // TODO: Parse source URL instead of assuming same project/zone?
|
||||
// name := lastComponent(source)
|
||||
// d, err := cloud.Compute.Disks.Get(cloud.Project, *e.Zone, name).Do()
|
||||
// if err != nil {
|
||||
// if gce.IsNotFound(err) {
|
||||
// return nil, fmt.Errorf("disk not found %q: %v", source, err)
|
||||
// }
|
||||
// return nil, fmt.Errorf("error querying for disk %q: %v", source, err)
|
||||
// } else {
|
||||
// imageURL, err := gce.ParseGoogleCloudURL(d.SourceImage)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("unable to parse image URL: %q", d.SourceImage)
|
||||
// }
|
||||
// actual.Image = fi.String(imageURL.Project + "/" + imageURL.Name)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
if p.Metadata != nil {
|
||||
actual.Metadata = make(map[string]*fi.ResourceHolder)
|
||||
for _, meta := range p.Metadata.Items {
|
||||
actual.Metadata[meta.Key] = fi.WrapResource(fi.NewStringResource(meta.Value))
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent spurious changes
|
||||
actual.Name = e.Name
|
||||
|
||||
actual.ID = &r.Name
|
||||
if e.ID == nil {
|
||||
e.ID = actual.ID
|
||||
}
|
||||
|
||||
return actual, nil
|
||||
}
|
||||
|
||||
return actual, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (e *InstanceTemplate) Run(c *fi.Context) error {
|
||||
|
|
@ -168,6 +202,7 @@ func (e *InstanceTemplate) mapToGCE(project string) (*compute.InstanceTemplate,
|
|||
|
||||
var disks []*compute.AttachedDisk
|
||||
disks = append(disks, &compute.AttachedDisk{
|
||||
Kind: "compute#attachedDisk",
|
||||
InitializeParams: &compute.AttachedDiskInitializeParams{
|
||||
SourceImage: BuildImageURL(project, *e.BootDiskImage),
|
||||
DiskSizeGb: *e.BootDiskSizeGB,
|
||||
|
|
@ -191,6 +226,7 @@ func (e *InstanceTemplate) mapToGCE(project string) (*compute.InstanceTemplate,
|
|||
var networkInterfaces []*compute.NetworkInterface
|
||||
ni := &compute.NetworkInterface{
|
||||
AccessConfigs: []*compute.AccessConfig{{
|
||||
Kind: "compute#accessConfig",
|
||||
//NatIP: *e.IPAddress.Address,
|
||||
Type: "ONE_TO_ONE_NAT",
|
||||
}},
|
||||
|
|
@ -228,7 +264,7 @@ func (e *InstanceTemplate) mapToGCE(project string) (*compute.InstanceTemplate,
|
|||
}
|
||||
|
||||
i := &compute.InstanceTemplate{
|
||||
Name: *e.Name,
|
||||
Kind: "compute#instanceTemplate",
|
||||
Properties: &compute.InstanceProperties{
|
||||
CanIpForward: *e.CanIPForward,
|
||||
|
||||
|
|
@ -237,6 +273,7 @@ func (e *InstanceTemplate) mapToGCE(project string) (*compute.InstanceTemplate,
|
|||
MachineType: *e.MachineType,
|
||||
|
||||
Metadata: &compute.Metadata{
|
||||
Kind: "compute#metadata",
|
||||
Items: metadataItems,
|
||||
},
|
||||
|
||||
|
|
@ -253,8 +290,53 @@ func (e *InstanceTemplate) mapToGCE(project string) (*compute.InstanceTemplate,
|
|||
return i, nil
|
||||
}
|
||||
|
||||
func (e *InstanceTemplate) URL(project string) string {
|
||||
return fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/instanceTemplates/%s", project, fi.StringValue(e.Name))
|
||||
type ByKey []*compute.MetadataItems
|
||||
|
||||
func (a ByKey) Len() int { return len(a) }
|
||||
func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByKey) Less(i, j int) bool { return a[i].Key < a[j].Key }
|
||||
|
||||
func matches(l, r *compute.InstanceTemplate) bool {
|
||||
normalizeInstanceProperties := func(v *compute.InstanceProperties) *compute.InstanceProperties {
|
||||
c := *v
|
||||
if c.Metadata != nil {
|
||||
cm := *c.Metadata
|
||||
c.Metadata = &cm
|
||||
c.Metadata.Fingerprint = ""
|
||||
sort.Sort(ByKey(c.Metadata.Items))
|
||||
}
|
||||
return &c
|
||||
}
|
||||
normalize := func(v *compute.InstanceTemplate) *compute.InstanceTemplate {
|
||||
c := *v
|
||||
c.SelfLink = ""
|
||||
c.CreationTimestamp = ""
|
||||
c.Id = 0
|
||||
c.Name = ""
|
||||
c.Properties = normalizeInstanceProperties(c.Properties)
|
||||
return &c
|
||||
}
|
||||
normalizedL := normalize(l)
|
||||
normalizedR := normalize(r)
|
||||
|
||||
if !reflect.DeepEqual(normalizedL, normalizedR) {
|
||||
if glog.V(10) {
|
||||
ls := fi.DebugAsJsonStringIndent(normalizedL)
|
||||
rs := fi.DebugAsJsonStringIndent(normalizedR)
|
||||
glog.V(10).Infof("Not equal")
|
||||
glog.V(10).Infof(diff.FormatDiff(ls, rs))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *InstanceTemplate) URL(project string) (string, error) {
|
||||
if e.ID == nil {
|
||||
return "", fmt.Errorf("InstanceTemplate not yet built; ID is not yet known")
|
||||
}
|
||||
return fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/instanceTemplates/%s", project, *e.ID), nil
|
||||
}
|
||||
|
||||
func (_ *InstanceTemplate) RenderGCE(t *gce.GCEAPITarget, a, e, changes *InstanceTemplate) error {
|
||||
|
|
@ -268,6 +350,10 @@ func (_ *InstanceTemplate) RenderGCE(t *gce.GCEAPITarget, a, e, changes *Instanc
|
|||
if a == nil {
|
||||
glog.V(4).Infof("Creating InstanceTemplate %v", i)
|
||||
|
||||
name := fi.StringValue(e.Name) + "-" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
e.ID = &name
|
||||
i.Name = name
|
||||
|
||||
op, err := t.Cloud.Compute.InstanceTemplates.Insert(t.Cloud.Project, i).Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating InstanceTemplate: %v", err)
|
||||
|
|
@ -277,45 +363,18 @@ func (_ *InstanceTemplate) RenderGCE(t *gce.GCEAPITarget, a, e, changes *Instanc
|
|||
return fmt.Errorf("error creating InstanceTemplate: %v", err)
|
||||
}
|
||||
} else {
|
||||
// TODO: Make error again
|
||||
if changes.Metadata != nil {
|
||||
for k, ev := range e.Metadata {
|
||||
av := a.Metadata[k]
|
||||
if av == nil {
|
||||
glog.Infof("Metadata %q not found", k)
|
||||
continue
|
||||
}
|
||||
|
||||
evString, err := ev.AsString()
|
||||
if err != nil {
|
||||
glog.Infof("Expected.Metadata.%s could not be rendered: %v", k, err)
|
||||
continue
|
||||
}
|
||||
avString, err := av.AsString()
|
||||
if err != nil {
|
||||
glog.Infof("Actual.Metadata.%s could not be rendered: %v", k, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if evString == avString {
|
||||
continue
|
||||
}
|
||||
|
||||
glog.Infof("Difference in Metadata.%s:", k)
|
||||
glog.Infof("%s", diff.FormatDiff(avString, evString))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make error again
|
||||
//return fmt.Errorf("Cannot apply changes to InstanceTemplate: %v", changes)
|
||||
glog.Warningf("Cannot apply changes to InstanceTemplate: %v", changes)
|
||||
return fmt.Errorf("Cannot apply changes to InstanceTemplate: %v", changes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type terraformInstanceTemplate struct {
|
||||
Name string `json:"name"`
|
||||
terraformInstanceCommon
|
||||
NamePrefix string `json:"name_prefix"`
|
||||
}
|
||||
|
||||
type terraformInstanceCommon struct {
|
||||
CanIPForward bool `json:"can_ip_forward"`
|
||||
MachineType string `json:"machine_type,omitempty"`
|
||||
ServiceAccount *terraformServiceAccount `json:"service_account,omitempty"`
|
||||
|
|
@ -377,7 +436,7 @@ type terraformAccessConfig struct {
|
|||
NatIP *terraform.Literal `json:"nat_ip,omitempty"`
|
||||
}
|
||||
|
||||
func (t *terraformInstanceTemplate) AddNetworks(network *Network, subnet *Subnet, networkInterfacs []*compute.NetworkInterface) {
|
||||
func (t *terraformInstanceCommon) AddNetworks(network *Network, subnet *Subnet, networkInterfacs []*compute.NetworkInterface) {
|
||||
for _, g := range networkInterfacs {
|
||||
tf := &terraformNetworkInterface{}
|
||||
if network != nil {
|
||||
|
|
@ -402,7 +461,7 @@ func (t *terraformInstanceTemplate) AddNetworks(network *Network, subnet *Subnet
|
|||
}
|
||||
}
|
||||
|
||||
func (t *terraformInstanceTemplate) AddMetadata(metadata *compute.Metadata) {
|
||||
func (t *terraformInstanceCommon) AddMetadata(metadata *compute.Metadata) {
|
||||
if metadata != nil {
|
||||
if t.Metadata == nil {
|
||||
t.Metadata = make(map[string]string)
|
||||
|
|
@ -415,7 +474,7 @@ func (t *terraformInstanceTemplate) AddMetadata(metadata *compute.Metadata) {
|
|||
}
|
||||
}
|
||||
|
||||
func (t *terraformInstanceTemplate) AddServiceAccounts(serviceAccounts []*compute.ServiceAccount) {
|
||||
func (t *terraformInstanceCommon) AddServiceAccounts(serviceAccounts []*compute.ServiceAccount) {
|
||||
for _, g := range serviceAccounts {
|
||||
for _, scope := range g.Scopes {
|
||||
if t.ServiceAccount == nil {
|
||||
|
|
@ -435,13 +494,14 @@ func (_ *InstanceTemplate) RenderTerraform(t *terraform.TerraformTarget, a, e, c
|
|||
}
|
||||
|
||||
tf := &terraformInstanceTemplate{
|
||||
Name: i.Name,
|
||||
CanIPForward: i.Properties.CanIpForward,
|
||||
//Description: i.Properties.Description,
|
||||
MachineType: i.Properties.MachineType,
|
||||
Tags: i.Properties.Tags.Items,
|
||||
NamePrefix: fi.StringValue(e.Name),
|
||||
}
|
||||
|
||||
tf.CanIPForward = i.Properties.CanIpForward
|
||||
tf.MachineType = lastComponent(i.Properties.MachineType)
|
||||
//tf.Zone = i.Properties.Zone
|
||||
tf.Tags = i.Properties.Tags.Items
|
||||
|
||||
tf.AddServiceAccounts(i.Properties.ServiceAccounts)
|
||||
|
||||
for _, d := range i.Properties.Disks {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ func BuildCloud(cluster *api.Cluster) (fi.Cloud, error) {
|
|||
|
||||
project = cluster.Spec.Project
|
||||
if project == "" {
|
||||
return nil, fmt.Errorf("project is required for GCE")
|
||||
return nil, fmt.Errorf("project is required for GCE - try gcloud config get-value project")
|
||||
}
|
||||
|
||||
labels := map[string]string{gce.GceLabelNameKubernetesCluster: gce.SafeClusterName(cluster.ObjectMeta.Name)}
|
||||
|
|
|
|||
|
|
@ -157,6 +157,11 @@ func (t *DryRunTarget) PrintReport(taskMap map[string]Task, out io.Writer) error
|
|||
field := changes.Field(i)
|
||||
|
||||
fieldName := changes.Type().Field(i).Name
|
||||
if changes.Type().Field(i).PkgPath != "" {
|
||||
// Not exported
|
||||
continue
|
||||
}
|
||||
|
||||
fieldValue := ValueAsString(field)
|
||||
|
||||
shouldPrint := true
|
||||
|
|
@ -217,6 +222,11 @@ func (t *DryRunTarget) PrintReport(taskMap map[string]Task, out io.Writer) error
|
|||
}
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue