diff --git a/upup/pkg/fi/cloudup/alitasks/BUILD.bazel b/upup/pkg/fi/cloudup/alitasks/BUILD.bazel index 97a40ba125932..4d46ba2fac14b 100644 --- a/upup/pkg/fi/cloudup/alitasks/BUILD.bazel +++ b/upup/pkg/fi/cloudup/alitasks/BUILD.bazel @@ -3,6 +3,8 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", srcs = [ + "disk.go", + "disk_fitask.go", "vpc.go", "vpc_fitask.go", "vswitch.go", diff --git a/upup/pkg/fi/cloudup/alitasks/disk.go b/upup/pkg/fi/cloudup/alitasks/disk.go new file mode 100644 index 0000000000000..23565f15448e5 --- /dev/null +++ b/upup/pkg/fi/cloudup/alitasks/disk.go @@ -0,0 +1,206 @@ +/* +Copyright 2018 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 alitasks + +import ( + "fmt" + + "github.com/denverdino/aliyungo/common" + "github.com/denverdino/aliyungo/ecs" + "github.com/golang/glog" + + "k8s.io/kops/upup/pkg/fi" + "k8s.io/kops/upup/pkg/fi/cloudup/aliup" + "k8s.io/kops/upup/pkg/fi/cloudup/terraform" +) + +// Disk represents a ALI Cloud Disk +//go:generate fitask -type=Disk +const ( + DiskResource = "disk" + DiskType = ecs.DiskTypeAllData +) + +type Disk struct { + Lifecycle *fi.Lifecycle + Name *string + DiskId *string + ZoneId *string + DiskCategory *string + Encrypted *bool + SizeGB *int + Tags map[string]string +} + +var _ fi.CompareWithID = &Disk{} + +func (d *Disk) CompareWithID() *string { + return d.DiskId +} + +func (d *Disk) Find(c *fi.Context) (*Disk, error) { + cloud := c.Cloud.(aliup.ALICloud) + clusterTags := cloud.GetClusterTags() + + request := &ecs.DescribeDisksArgs{ + DiskType: DiskType, + RegionId: common.Region(cloud.Region()), + ZoneId: fi.StringValue(d.ZoneId), + Tag: clusterTags, + DiskName: fi.StringValue(d.Name), + } + + responseDisks, _, err := cloud.EcsClient().DescribeDisks(request) + if err != nil { + return nil, fmt.Errorf("error finding Disks: %v", err) + } + // Don't exist disk with specified ClusterTags or Name. + if len(responseDisks) == 0 { + return nil, nil + } + if len(responseDisks) > 1 { + glog.V(4).Info("The number of specified disk with the same name and ClusterTags exceeds 1, diskName:%q", *d.Name) + } + + glog.V(2).Infof("found matching Disk with name: %q", *d.Name) + + actual := &Disk{} + actual.Name = fi.String(responseDisks[0].DiskName) + actual.DiskCategory = fi.String(string(responseDisks[0].Category)) + actual.ZoneId = fi.String(responseDisks[0].ZoneId) + actual.SizeGB = fi.Int(responseDisks[0].Size) + actual.DiskId = fi.String(responseDisks[0].DiskId) + + tags, err := cloud.GetTags(fi.StringValue(actual.DiskId), DiskResource) + + if err != nil { + glog.V(4).Info("Error getting tags on resourceId:%q", *actual.DiskId) + } + actual.Tags = tags + + // Ignore "system" fields + actual.Lifecycle = d.Lifecycle + d.DiskId = actual.DiskId + return actual, nil +} + +func (d *Disk) Run(c *fi.Context) error { + if d.Tags == nil { + d.Tags = make(map[string]string) + } + c.Cloud.(aliup.ALICloud).AddClusterTags(d.Tags) + return fi.DefaultDeltaRunMethod(d, c) +} + +func (_ *Disk) CheckChanges(a, e, changes *Disk) error { + if a == nil { + if e.ZoneId == nil { + return fi.RequiredField("ZoneId") + } + if e.Name == nil { + return fi.RequiredField("Name") + } + } else { + if changes.DiskCategory != nil { + return fi.CannotChangeField("DiskCategory") + } + } + return nil +} + +//Disk can only modify tags. +func (_ *Disk) RenderALI(t *aliup.ALIAPITarget, a, e, changes *Disk) error { + if a == nil { + glog.V(2).Infof("Creating Disk with Name:%q", fi.StringValue(e.Name)) + + request := &ecs.CreateDiskArgs{ + DiskName: fi.StringValue(e.Name), + RegionId: common.Region(t.Cloud.Region()), + ZoneId: fi.StringValue(e.ZoneId), + Encrypted: fi.BoolValue(e.Encrypted), + DiskCategory: ecs.DiskCategory(fi.StringValue(e.DiskCategory)), + Size: fi.IntValue(e.SizeGB), + } + diskId, err := t.Cloud.EcsClient().CreateDisk(request) + if err != nil { + return fmt.Errorf("error creating disk: %v", err) + } + e.DiskId = fi.String(diskId) + } + + if changes != nil && changes.Tags != nil { + glog.V(2).Infof("Modifing tags of disk with Name:%q", fi.StringValue(e.Name)) + if err := t.Cloud.CreateTags(*e.DiskId, DiskResource, e.Tags); err != nil { + return fmt.Errorf("error adding Tags to ALI YunPan: %v", err) + } + } + + if a != nil && (len(a.Tags) > 0) { + + tagsToDelete := e.getDiskTagsToDelete(a.Tags) + if len(tagsToDelete) > 0 { + glog.V(2).Infof("Deleting tags of disk with Name:%q", fi.StringValue(e.Name)) + if err := t.Cloud.RemoveTags(*e.DiskId, DiskResource, tagsToDelete); err != nil { + return fmt.Errorf("error removing Tags from ALI YunPan: %v", err) + } + } + } + + return nil +} + +// getDiskTagsToDelete loops through the currently set tags and builds a list of tags to be deleted from the specificated disk +func (d *Disk) getDiskTagsToDelete(currentTags map[string]string) map[string]string { + tagsToDelete := map[string]string{} + for k, v := range currentTags { + if _, ok := d.Tags[k]; !ok { + tagsToDelete[k] = v + } + } + + return tagsToDelete +} + +type terraformDiskTag struct { + Key *string `json:"key"` + Value *string `json:"value"` +} + +type terraformDisk struct { + DiskName *string `json:"name,omitempty"` + DiskCategory *string `json:"category,omitempty"` + SizeGB *int `json:"size,omitempty"` + Zone *string `json:"availability_zone,omitempty"` + Tags []*terraformDiskTag `json:"tags,omitempty"` +} + +func (_ *Disk) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Disk) error { + tf := &terraformDisk{ + DiskName: e.Name, + DiskCategory: e.DiskCategory, + SizeGB: e.SizeGB, + Zone: e.ZoneId, + } + + for key, value := range e.Tags { + tf.Tags = append(tf.Tags, &terraformDiskTag{ + Key: &key, + Value: &value, + }) + } + return t.RenderResource("alicloud_disk", *e.Name, tf) +} diff --git a/upup/pkg/fi/cloudup/alitasks/disk_fitask.go b/upup/pkg/fi/cloudup/alitasks/disk_fitask.go new file mode 100644 index 0000000000000..52ee25a74c06f --- /dev/null +++ b/upup/pkg/fi/cloudup/alitasks/disk_fitask.go @@ -0,0 +1,75 @@ +/* +Copyright 2018 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. +*/ + +// Code generated by ""fitask" -type=Disk"; DO NOT EDIT + +package alitasks + +import ( + "encoding/json" + + "k8s.io/kops/upup/pkg/fi" +) + +// Disk + +// JSON marshalling boilerplate +type realDisk Disk + +// UnmarshalJSON implements conversion to JSON, supporitng an alternate specification of the object as a string +func (o *Disk) UnmarshalJSON(data []byte) error { + var jsonName string + if err := json.Unmarshal(data, &jsonName); err == nil { + o.Name = &jsonName + return nil + } + + var r realDisk + if err := json.Unmarshal(data, &r); err != nil { + return err + } + *o = Disk(r) + return nil +} + +var _ fi.HasLifecycle = &Disk{} + +// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle +func (o *Disk) GetLifecycle() *fi.Lifecycle { + return o.Lifecycle +} + +// SetLifecycle sets the Lifecycle of the object, implementing fi.SetLifecycle +func (o *Disk) SetLifecycle(lifecycle fi.Lifecycle) { + o.Lifecycle = &lifecycle +} + +var _ fi.HasName = &Disk{} + +// GetName returns the Name of the object, implementing fi.HasName +func (o *Disk) GetName() *string { + return o.Name +} + +// SetName sets the Name of the object, implementing fi.SetName +func (o *Disk) SetName(name string) { + o.Name = &name +} + +// String is the stringer function for the task, producing readable output using fi.TaskAsString +func (o *Disk) String() string { + return fi.TaskAsString(o) +} diff --git a/upup/pkg/fi/cloudup/aliup/BUILD.bazel b/upup/pkg/fi/cloudup/aliup/BUILD.bazel index 3732b635536ff..7a093edb5e7af 100644 --- a/upup/pkg/fi/cloudup/aliup/BUILD.bazel +++ b/upup/pkg/fi/cloudup/aliup/BUILD.bazel @@ -17,6 +17,7 @@ go_library( "//upup/pkg/fi:go_default_library", "//vendor/github.com/denverdino/aliyungo/common:go_default_library", "//vendor/github.com/denverdino/aliyungo/ecs:go_default_library", + "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", ], ) diff --git a/upup/pkg/fi/cloudup/aliup/ali_cloud.go b/upup/pkg/fi/cloudup/aliup/ali_cloud.go index eefcd09f14261..cbb98d33f4ba0 100644 --- a/upup/pkg/fi/cloudup/aliup/ali_cloud.go +++ b/upup/pkg/fi/cloudup/aliup/ali_cloud.go @@ -20,10 +20,11 @@ import ( "errors" "fmt" "os" - "strings" - common "github.com/denverdino/aliyungo/common" - ecs "github.com/denverdino/aliyungo/ecs" + "github.com/golang/glog" + + "github.com/denverdino/aliyungo/common" + "github.com/denverdino/aliyungo/ecs" "k8s.io/api/core/v1" prj "k8s.io/kops" @@ -43,6 +44,11 @@ type ALICloud interface { EcsClient() *ecs.Client Region() string + AddClusterTags(tags map[string]string) + GetTags(resourceId string, resourceType string) (map[string]string, error) + CreateTags(resourceId string, resourceType string, tags map[string]string) error + RemoveTags(resourceId string, resourceType string, tags map[string]string) error + GetClusterTags() map[string]string } type aliCloudImplementation struct { @@ -121,12 +127,12 @@ func (c *aliCloudImplementation) FindVPCInfo(id string) (*fi.VPCInfo, error) { VpcId: id, RegionId: common.Region(c.Region()), } - vswitcheList, _, err := c.EcsClient().DescribeVSwitches(describeVSwitchesArgs) + vswitchList, _, err := c.EcsClient().DescribeVSwitches(describeVSwitchesArgs) if err != nil { return nil, fmt.Errorf("error listing VSwitchs: %v", err) } - for _, vswitch := range vswitcheList { + for _, vswitch := range vswitchList { s := &fi.SubnetInfo{ ID: vswitch.VSwitchId, Zone: vswitch.ZoneId, @@ -140,7 +146,100 @@ func (c *aliCloudImplementation) FindVPCInfo(id string) (*fi.VPCInfo, error) { } func (c *aliCloudImplementation) GetCloudGroups(cluster *kops.Cluster, instancegroups []*kops.InstanceGroup, warnUnmatched bool, nodes []v1.Node) (map[string]*cloudinstances.CloudInstanceGroup, error) { - return nil, fmt.Errorf("GetCloudGroups not implemented on aliCloud") + return nil, errors.New("GetCloudGroups not implemented on aliCloud") +} + +// GetTags will get the specified resource's tags. +func (c *aliCloudImplementation) GetTags(resourceId string, resourceType string) (map[string]string, error) { + if resourceId == "" { + return nil, errors.New("resourceId not provided to GetTags") + } + tags := map[string]string{} + + request := &ecs.DescribeTagsArgs{ + RegionId: common.Region(c.Region()), + ResourceType: ecs.TagResourceType(resourceType), //image, instance, snapshot or disk + ResourceId: resourceId, + } + responseTags, _, err := c.EcsClient().DescribeTags(request) + if err != nil { + return tags, fmt.Errorf("error getting tags on %v: %v", resourceId, err) + } + + for _, tag := range responseTags { + tags[tag.TagKey] = tag.TagValue + } + return tags, nil + +} + +// AddClusterTags will add ClusterTags to resources (in ALI, only disk, instance, snapshot or image can be tagged ) +func (c *aliCloudImplementation) AddClusterTags(tags map[string]string) { + + if c.tags != nil && len(c.tags) != 0 && tags != nil { + for k, v := range c.tags { + tags[k] = v + } + } +} + +// CreateTags will add tags to the specified resource. +func (c *aliCloudImplementation) CreateTags(resourceId string, resourceType string, tags map[string]string) error { + if len(tags) == 0 { + return nil + } else if len(tags) > 10 { + glog.V(4).Info("The number of specified resource's tags exceeds 10, resourceId:%q", resourceId) + } + if resourceId == "" { + return errors.New("resourceId not provided to CreateTags") + } + if resourceType == "" { + return errors.New("resourceType not provided to CreateTags") + } + + request := &ecs.AddTagsArgs{ + ResourceId: resourceId, + ResourceType: ecs.TagResourceType(resourceType), //image, instance, snapshot or disk + RegionId: common.Region(c.Region()), + Tag: tags, + } + err := c.EcsClient().AddTags(request) + if err != nil { + return fmt.Errorf("error creating tags on %v: %v", resourceId, err) + } + + return nil +} + +// RemoveTags will remove tags from the specified resource. +func (c *aliCloudImplementation) RemoveTags(resourceId string, resourceType string, tags map[string]string) error { + if len(tags) == 0 { + return nil + } + if resourceId == "" { + return errors.New("resourceId not provided to RemoveTags") + } + if resourceType == "" { + return errors.New("resourceType not provided to RemoveTags") + } + + request := &ecs.RemoveTagsArgs{ + ResourceId: resourceId, + ResourceType: ecs.TagResourceType(resourceType), //image, instance, snapshot or disk + RegionId: common.Region(c.Region()), + Tag: tags, + } + err := c.EcsClient().RemoveTags(request) + if err != nil { + return fmt.Errorf("error removing tags on %v: %v", resourceId, err) + } + + return nil +} + +// GetClusterTags will get the ClusterTags +func (c *aliCloudImplementation) GetClusterTags() map[string]string { + return c.tags } func ZoneToVSwitchID(VPCID string, zones []string, vswitchIDs []string) (map[string]string, error) { @@ -190,52 +289,25 @@ func ZoneToVSwitchID(VPCID string, zones []string, vswitchIDs []string) (map[str VSwitchId: VSwitchId, } - vswitcheList, _, err := aliCloud.EcsClient().DescribeVSwitches(describeVSwitchesArgs) + vswitchList, _, err := aliCloud.EcsClient().DescribeVSwitches(describeVSwitchesArgs) if err != nil { return nil, fmt.Errorf("error listing VSwitchs: %v", err) } - if len(vswitcheList) == 0 { + if len(vswitchList) == 0 { return nil, fmt.Errorf("VSwitch %q not found", VSwitchId) } - if len(vswitcheList) != 1 { + if len(vswitchList) != 1 { return nil, fmt.Errorf("found multiple VSwitchs for %q", VSwitchId) } - zone := vswitcheList[0].ZoneId + zone := vswitchList[0].ZoneId if res[zone] != "" { - return res, fmt.Errorf("vswitch %s and %s have the same zone", vswitcheList[0].VSwitchId, zone) + return res, fmt.Errorf("vswitch %s and %s have the same zone", vswitchList[0].VSwitchId, zone) } - res[zone] = vswitcheList[0].VSwitchId + res[zone] = vswitchList[0].VSwitchId } return res, nil } - -func getRegionByZones(zones []string) (string, error) { - region := "" - - for _, zone := range zones { - zoneSplit := strings.Split(zone, "-") - zoneRegion := "" - if len(zoneSplit) != 3 { - return "", fmt.Errorf("invalid ALI zone: %q ", zone) - } - - if len(zoneSplit[2]) == 1 { - zoneRegion = zoneSplit[0] + "-" + zoneSplit[1] - } else if len(zoneSplit[2]) == 2 { - zoneRegion = zone[:len(zone)-1] - } else { - return "", fmt.Errorf("invalid ALI zone: %q ", zone) - } - - if region != "" && zoneRegion != region { - return "", fmt.Errorf("clusters cannot span multiple regions (found zone %q, but region is %q)", zone, region) - } - region = zoneRegion - } - - return region, nil -} diff --git a/upup/pkg/fi/cloudup/aliup/ali_utils.go b/upup/pkg/fi/cloudup/aliup/ali_utils.go index 5bf917582c153..2a92b2be5e2cc 100644 --- a/upup/pkg/fi/cloudup/aliup/ali_utils.go +++ b/upup/pkg/fi/cloudup/aliup/ali_utils.go @@ -26,24 +26,34 @@ import ( // FindRegion determines the region from the zones specified in the cluster func FindRegion(cluster *kops.Cluster) (string, error) { - region := "" + zones := []string{} for _, subnet := range cluster.Spec.Subnets { - zoneSplit := strings.Split(subnet.Zone, "-") + zones = append(zones, subnet.Zone) + } + return getRegionByZones(zones) + +} + +func getRegionByZones(zones []string) (string, error) { + region := "" + + for _, zone := range zones { + zoneSplit := strings.Split(zone, "-") zoneRegion := "" if len(zoneSplit) != 3 { - return "", fmt.Errorf("invalid ALI zone: %q in subnet %q", subnet.Zone, subnet.Name) + return "", fmt.Errorf("invalid ALI zone: %q ", zone) } if len(zoneSplit[2]) == 1 { zoneRegion = zoneSplit[0] + "-" + zoneSplit[1] } else if len(zoneSplit[2]) == 2 { - zoneRegion = subnet.Zone[:len(subnet.Zone)-1] + zoneRegion = zone[:len(zone)-1] } else { - return "", fmt.Errorf("invalid ALI zone: %q in subnet %q", subnet.Zone, subnet.Name) + return "", fmt.Errorf("invalid ALI zone: %q ", zone) } if region != "" && zoneRegion != region { - return "", fmt.Errorf("Clusters cannot span multiple regions (found zone %q, but region is %q)", subnet.Zone, region) + return "", fmt.Errorf("clusters cannot span multiple regions (found zone %q, but region is %q)", zone, region) } region = zoneRegion }