From 812e7d66edbf6b981af6f6c299fe3047689b6bd0 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Wed, 8 Jun 2016 02:32:31 -0400 Subject: [PATCH] upup: support for terraform on AWS All seems good except for a bug with volume tagging --- upup/cmd/cloudup/main.go | 5 +- .../_aws/master/_master_lb/master_lb.yaml | 10 +- .../master/_not_master_lb/not_master_lb.yaml | 2 +- upup/models/cloudup/_aws/master/master.yaml | 12 +- upup/models/cloudup/_aws/nodes.yaml | 12 +- ...toscaling_group.go => autoscalinggroup.go} | 123 +++++++++ upup/pkg/fi/cloudup/awstasks/dhcp_options.go | 27 ++ .../awstasks/{dns_name.go => dnsname.go} | 49 +++- .../awstasks/{dns_zone.go => dnszone.go} | 76 +++++- .../awstasks/{ebs_volume.go => ebsvolume.go} | 25 ++ ...tance_profile.go => iaminstanceprofile.go} | 13 +- ...file_role.go => iaminstanceprofilerole.go} | 18 +- .../awstasks/{iam_role.go => iamrole.go} | 27 +- .../{iam_role_policy.go => iamrolepolicy.go} | 29 +- upup/pkg/fi/cloudup/awstasks/instance.go | 5 + ...internet_gateway.go => internetgateway.go} | 10 + ...chment.go => internetgatewayattachment.go} | 17 ++ upup/pkg/fi/cloudup/awstasks/route.go | 32 ++- upup/pkg/fi/cloudup/awstasks/route_fitask.go | 43 +++ .../{route_table.go => routetable.go} | 21 ++ ...ssociation.go => routetableassociation.go} | 26 +- .../awstasks/routetableassociation_fitask.go | 43 +++ .../awstasks/security_group_ingress.go | 205 -------------- .../{security_group.go => securitygroup.go} | 25 ++ .../fi/cloudup/awstasks/securitygrouprule.go | 252 ++++++++++++++++++ .../awstasks/securitygrouprule_fitask.go | 43 +++ .../awstasks/{ssh_key.go => sshkey.go} | 24 ++ upup/pkg/fi/cloudup/awstasks/subnet.go | 25 ++ upup/pkg/fi/cloudup/awstasks/vpc.go | 25 ++ .../awstasks/vpc_dhcpoptions_association.go | 15 ++ upup/pkg/fi/cloudup/gce/gce_apitarget.go | 4 +- upup/pkg/fi/cloudup/loader.go | 4 +- upup/pkg/fi/cloudup/terraform/hcl_printer.go | 83 ++++++ upup/pkg/fi/cloudup/terraform/literal.go | 4 +- upup/pkg/fi/cloudup/terraform/target.go | 101 +++++-- upup/pkg/fi/default_methods.go | 7 +- upup/pkg/fi/fitasks/keypair.go | 10 +- upup/pkg/fi/fs_castore.go | 2 + upup/pkg/fi/task.go | 4 + 39 files changed, 1193 insertions(+), 265 deletions(-) rename upup/pkg/fi/cloudup/awstasks/{autoscaling_group.go => autoscalinggroup.go} (69%) rename upup/pkg/fi/cloudup/awstasks/{dns_name.go => dnsname.go} (69%) rename upup/pkg/fi/cloudup/awstasks/{dns_zone.go => dnszone.go} (58%) rename upup/pkg/fi/cloudup/awstasks/{ebs_volume.go => ebsvolume.go} (77%) rename upup/pkg/fi/cloudup/awstasks/{iam_instance_profile.go => iaminstanceprofile.go} (81%) rename upup/pkg/fi/cloudup/awstasks/{iam_instance_profile_role.go => iaminstanceprofilerole.go} (78%) rename upup/pkg/fi/cloudup/awstasks/{iam_role.go => iamrole.go} (84%) rename upup/pkg/fi/cloudup/awstasks/{iam_role_policy.go => iamrolepolicy.go} (76%) rename upup/pkg/fi/cloudup/awstasks/{internet_gateway.go => internetgateway.go} (84%) rename upup/pkg/fi/cloudup/awstasks/{internet_gateway_attachment.go => internetgatewayattachment.go} (81%) create mode 100644 upup/pkg/fi/cloudup/awstasks/route_fitask.go rename upup/pkg/fi/cloudup/awstasks/{route_table.go => routetable.go} (77%) rename upup/pkg/fi/cloudup/awstasks/{route_table_association.go => routetableassociation.go} (77%) create mode 100644 upup/pkg/fi/cloudup/awstasks/routetableassociation_fitask.go delete mode 100644 upup/pkg/fi/cloudup/awstasks/security_group_ingress.go rename upup/pkg/fi/cloudup/awstasks/{security_group.go => securitygroup.go} (75%) create mode 100644 upup/pkg/fi/cloudup/awstasks/securitygrouprule.go create mode 100644 upup/pkg/fi/cloudup/awstasks/securitygrouprule_fitask.go rename upup/pkg/fi/cloudup/awstasks/{ssh_key.go => sshkey.go} (88%) create mode 100644 upup/pkg/fi/cloudup/terraform/hcl_printer.go diff --git a/upup/cmd/cloudup/main.go b/upup/cmd/cloudup/main.go index efa4b2ad5..0a062525f 100644 --- a/upup/cmd/cloudup/main.go +++ b/upup/cmd/cloudup/main.go @@ -281,7 +281,7 @@ func (c *CreateClusterCmd) Run() error { "routeTable": &awstasks.RouteTable{}, "routeTableAssociation": &awstasks.RouteTableAssociation{}, "securityGroup": &awstasks.SecurityGroup{}, - "securityGroupIngress": &awstasks.SecurityGroupIngress{}, + "securityGroupRule": &awstasks.SecurityGroupRule{}, "subnet": &awstasks.Subnet{}, "vpc": &awstasks.VPC{}, "vpcDHDCPOptionsAssociation": &awstasks.VPCDHCPOptionsAssociation{}, @@ -409,7 +409,8 @@ func (c *CreateClusterCmd) Run() error { case "terraform": checkExisting = false - target = terraform.NewTerraformTarget(region, project, os.Stdout) + outDir := path.Join(c.StateDir, "terraform") + target = terraform.NewTerraformTarget(cloud, region, project, outDir) case "dryrun": target = fi.NewDryRunTarget(os.Stdout) diff --git a/upup/models/cloudup/_aws/master/_master_lb/master_lb.yaml b/upup/models/cloudup/_aws/master/_master_lb/master_lb.yaml index c1d48f0ff..8d7639bb2 100644 --- a/upup/models/cloudup/_aws/master/_master_lb/master_lb.yaml +++ b/upup/models/cloudup/_aws/master/_master_lb/master_lb.yaml @@ -26,8 +26,14 @@ securityGroup/api.{{ .ClusterName }}: vpc: vpc/kubernetes-{{ .ClusterName }} description: 'Security group for ELB in front of masters' +# Allow full egress +securityGroupRule/egress-api-lb: + securityGroup: securityGroup/api.{{ .ClusterName} + egress: true + cidr: 0.0.0.0/0 + # HTTPS to the master ELB is allowed (for API access) -securityGroupIngress/https-external-to-api: +securityGroupRule/https-external-to-api: securityGroup: securityGroup/api.{{ .ClusterName }} cidr: 0.0.0.0/0 protocol: tcp @@ -35,7 +41,7 @@ securityGroupIngress/https-external-to-api: toPort: 443 # Allow HTTPS to the master from the master ELB -securityGroupIngress/https-elb-to-master: +securityGroupRule/https-elb-to-master: securityGroup: securityGroup/kubernetes.master.{{ .ClusterName }} sourceGroup: securityGroup/api.{{ .ClusterName }} protocol: tcp diff --git a/upup/models/cloudup/_aws/master/_not_master_lb/not_master_lb.yaml b/upup/models/cloudup/_aws/master/_not_master_lb/not_master_lb.yaml index 5c46e543b..39f9ed368 100644 --- a/upup/models/cloudup/_aws/master/_not_master_lb/not_master_lb.yaml +++ b/upup/models/cloudup/_aws/master/_not_master_lb/not_master_lb.yaml @@ -3,7 +3,7 @@ # We need to open security groups directly to the master nodes (instead of via the ELB) # HTTPS to the master is allowed (for API access) -securityGroupIngress/https-external-to-master: +securityGroupRule/https-external-to-master: securityGroup: securityGroup/kubernetes.master.{{ .ClusterName }} cidr: 0.0.0.0/0 protocol: tcp diff --git a/upup/models/cloudup/_aws/master/master.yaml b/upup/models/cloudup/_aws/master/master.yaml index c471651e8..103a36858 100644 --- a/upup/models/cloudup/_aws/master/master.yaml +++ b/upup/models/cloudup/_aws/master/master.yaml @@ -18,8 +18,14 @@ securityGroup/kubernetes.master.{{ .ClusterName }}: vpc: vpc/kubernetes.{{ .ClusterName }} description: 'Security group for masters' +# Allow full egress +securityGroupRule/master-egress: + securityGroup: securityGroup/kubernetes.master.{{.ClusterName}} + egress: true + cidr: 0.0.0.0/0 + # SSH is open to the world -securityGroupIngress/ssh-external-to-master: +securityGroupRule/ssh-external-to-master: securityGroup: securityGroup/kubernetes.master.{{ .ClusterName }} cidr: 0.0.0.0/0 protocol: tcp @@ -27,12 +33,12 @@ securityGroupIngress/ssh-external-to-master: toPort: 22 # Masters can talk to masters -securityGroupIngress/all-master-to-master: +securityGroupRule/all-master-to-master: securityGroup: securityGroup/kubernetes.master.{{ .ClusterName }} sourceGroup: securityGroup/kubernetes.master.{{ .ClusterName }} # Masters can talk to nodes -securityGroupIngress/all-master-to-node: +securityGroupRule/all-master-to-node: securityGroup: securityGroup/kubernetes.node.{{ .ClusterName }} sourceGroup: securityGroup/kubernetes.master.{{ .ClusterName }} diff --git a/upup/models/cloudup/_aws/nodes.yaml b/upup/models/cloudup/_aws/nodes.yaml index 9b93470b7..d08b9ca13 100644 --- a/upup/models/cloudup/_aws/nodes.yaml +++ b/upup/models/cloudup/_aws/nodes.yaml @@ -18,8 +18,14 @@ securityGroup/kubernetes.node.{{.ClusterName}}: vpc: vpc/kubernetes.{{ .ClusterName }} description: 'Security group for nodes' +# Allow full egress +securityGroupRule/node-egress: + securityGroup: securityGroup/kubernetes.node.{{.ClusterName}} + egress: true + cidr: 0.0.0.0/0 + # SSH is open to the world -securityGroupIngress/ssh-external-to-node: +securityGroupRule/ssh-external-to-node: securityGroup: securityGroup/kubernetes.node.{{.ClusterName}} cidr: 0.0.0.0/0 protocol: tcp @@ -27,12 +33,12 @@ securityGroupIngress/ssh-external-to-node: toPort: 22 # Nodes can talk to nodes -securityGroupIngress/all-node-to-node: +securityGroupRule/all-node-to-node: securityGroup: securityGroup/kubernetes.node.{{.ClusterName}} sourceGroup: securityGroup/kubernetes.node.{{.ClusterName}} # Nodes can talk masters nodes -securityGroupIngress/all-node-to-master: +securityGroupRule/all-node-to-master: securityGroup: securityGroup/kubernetes.master.{{ .ClusterName }} sourceGroup: securityGroup/kubernetes.node.{{.ClusterName}} diff --git a/upup/pkg/fi/cloudup/awstasks/autoscaling_group.go b/upup/pkg/fi/cloudup/awstasks/autoscalinggroup.go similarity index 69% rename from upup/pkg/fi/cloudup/awstasks/autoscaling_group.go rename to upup/pkg/fi/cloudup/awstasks/autoscalinggroup.go index 849b5e3f9..ed1c544f7 100644 --- a/upup/pkg/fi/cloudup/awstasks/autoscaling_group.go +++ b/upup/pkg/fi/cloudup/awstasks/autoscalinggroup.go @@ -9,6 +9,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" "strings" ) @@ -209,6 +210,8 @@ func (_ *AutoscalingGroup) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Autos } } + // TODO: Use PropagateAtLaunch = false for tagging? + return nil //return output.AddAWSTags(cloud.Tags(), v, "vpc") } @@ -323,3 +326,123 @@ func renderAutoscalingLaunchConfigurationAWS(t *awsup.AWSAPITarget, name string, return nil //return output.AddAWSTags(cloud.Tags(), v, "vpc") } + +type terraformASGTag struct { + Key *string `json:"key"` + Value *string `json:"value"` + PropagateAtLaunch *bool `json:"propagate_at_launch"` +} +type terraformAutoscalingGroup struct { + Name *string `json:"name,omitempty"` + LaunchConfigurationName *terraform.Literal `json:"launch_configuration,omitempty"` + MaxSize *int64 `json:"max_size,omitempty"` + MinSize *int64 `json:"min_size,omitempty"` + VPCZoneIdentifier []*terraform.Literal `json:"vpc_zone_identifier,omitempty"` + Tags []*terraformASGTag `json:"tag,omitempty"` +} + +type terraformBlockDevice struct { + DeviceName *string `json:"device_name"` + VirtualName *string `json:"virtual_name"` +} + +type terraformLaunchConfiguration struct { + NamePrefix *string `json:"name_prefix,omitempty"` + ImageID *string `json:"image_id,omitempty"` + InstanceType *string `json:"instance_type,omitempty"` + KeyName *terraform.Literal `json:"key_name,omitempty"` + IAMInstanceProfile *terraform.Literal `json:"iam_instance_profile,omitempty"` + SecurityGroups []*terraform.Literal `json:"security_groups,omitempty"` + AssociatePublicIpAddress *bool `json:"associate_public_ip_address,omitempty"` + UserData *terraform.Literal `json:"user_data,omitempty"` + EphemeralBlockDevice []*terraformBlockDevice `json:"ephemeral_block_device,omitempty"` + Lifecycle *terraformLifecycle `json:"lifecycle,omitempty"` +} + +type terraformLifecycle struct { + CreateBeforeDestroy *bool `json:"create_before_destroy,omitempty"` +} + +func (_ *AutoscalingGroup) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *AutoscalingGroup) error { + err := renderAutoscalingLaunchConfigurationTerraform(t, *e.Name, e) + if err != nil { + return err + } + + tf := &terraformAutoscalingGroup{ + Name: e.Name, + MinSize: e.MinSize, + MaxSize: e.MaxSize, + LaunchConfigurationName: terraform.LiteralProperty("aws_launch_configuration", *e.Name, "id"), + } + + for _, s := range e.Subnets { + tf.VPCZoneIdentifier = append(tf.VPCZoneIdentifier, s.TerraformLink()) + } + + for k, v := range e.buildTags(t.Cloud) { + tf.Tags = append(tf.Tags, &terraformASGTag{ + Key: fi.String(k), + Value: fi.String(v), + PropagateAtLaunch: fi.Bool(true), + }) + } + + return t.RenderResource("aws_autoscaling_group", *e.Name, tf) +} + +func (e *AutoscalingGroup) TerraformLink() *terraform.Literal { + return terraform.LiteralProperty("aws_autoscaling_group", *e.Name, "id") +} + +func renderAutoscalingLaunchConfigurationTerraform(t *terraform.TerraformTarget, namePrefix string, e *AutoscalingGroup) error { + cloud := t.Cloud.(*awsup.AWSCloud) + + if e.ImageID == nil { + return fi.RequiredField("ImageID") + } + image, err := cloud.ResolveImage(*e.ImageID) + if err != nil { + return err + } + + tf := &terraformLaunchConfiguration{ + NamePrefix: &namePrefix, + ImageID: image.ImageId, + InstanceType: e.InstanceType, + } + + if e.SSHKey != nil { + tf.KeyName = e.SSHKey.TerraformLink() + } + + for _, sg := range e.SecurityGroups { + tf.SecurityGroups = append(tf.SecurityGroups, sg.TerraformLink()) + } + tf.AssociatePublicIpAddress = e.AssociatePublicIP + + if e.BlockDeviceMappings != nil { + tf.EphemeralBlockDevice = []*terraformBlockDevice{} + for _, b := range e.BlockDeviceMappings { + tf.EphemeralBlockDevice = append(tf.EphemeralBlockDevice, &terraformBlockDevice{ + VirtualName: b.VirtualName, + DeviceName: b.DeviceName, + }) + } + } + + if e.UserData != nil { + tf.UserData, err = t.AddFile("aws_launch_configuration", *e.Name, "user_data", e.UserData) + if err != nil { + return err + } + } + if e.IAMInstanceProfile != nil { + tf.IAMInstanceProfile = e.IAMInstanceProfile.TerraformLink() + } + + // So that we can update configurations + tf.Lifecycle = &terraformLifecycle{CreateBeforeDestroy: fi.Bool(true)} + + return t.RenderResource("aws_launch_configuration", *e.Name, tf) +} diff --git a/upup/pkg/fi/cloudup/awstasks/dhcp_options.go b/upup/pkg/fi/cloudup/awstasks/dhcp_options.go index 2aceb41b6..a94a8d43e 100644 --- a/upup/pkg/fi/cloudup/awstasks/dhcp_options.go +++ b/upup/pkg/fi/cloudup/awstasks/dhcp_options.go @@ -8,6 +8,8 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" + "strings" ) //go:generate fitask -type=DHCPOptions @@ -135,3 +137,28 @@ func (_ *DHCPOptions) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *DHCPOption return t.AddAWSTags(*e.ID, t.Cloud.BuildTags(e.Name, nil)) } + +type terraformDHCPOptions struct { + DomainName *string `json:"domain_name,omitempty"` + DomainNameServers []string `json:"domain_name_servers,omitempty"` + Tags map[string]string `json:"tags,omitempty"` +} + +func (_ *DHCPOptions) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *DHCPOptions) error { + cloud := t.Cloud.(*awsup.AWSCloud) + + tf := &terraformDHCPOptions{ + DomainName: e.DomainName, + Tags: cloud.BuildTags(e.Name, nil), + } + + if e.DomainNameServers != nil { + tf.DomainNameServers = strings.Split(*e.DomainNameServers, ",") + } + + return t.RenderResource("aws_vpc_dhcp_options", *e.Name, tf) +} + +func (e *DHCPOptions) TerraformLink() *terraform.Literal { + return terraform.LiteralProperty("aws_vpc_dhcp_options", *e.Name, "id") +} diff --git a/upup/pkg/fi/cloudup/awstasks/dns_name.go b/upup/pkg/fi/cloudup/awstasks/dnsname.go similarity index 69% rename from upup/pkg/fi/cloudup/awstasks/dns_name.go rename to upup/pkg/fi/cloudup/awstasks/dnsname.go index b8bd13ee4..8d635f97a 100644 --- a/upup/pkg/fi/cloudup/awstasks/dns_name.go +++ b/upup/pkg/fi/cloudup/awstasks/dnsname.go @@ -8,6 +8,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" "strings" ) @@ -16,7 +17,7 @@ type DNSName struct { Name *string ID *string Zone *DNSZone - ResourceType string + ResourceType *string TargetLoadBalancer *LoadBalancer } @@ -29,7 +30,11 @@ func (e *DNSName) Find(c *fi.Context) (*DNSName, error) { return nil, nil } findName = strings.TrimSuffix(findName, ".") - findType := e.ResourceType + + findType := fi.StringValue(e.ResourceType) + if findType == "" { + return nil, nil + } request := &route53.ListResourceRecordSetsInput{ HostedZoneId: e.Zone.ID, @@ -94,7 +99,7 @@ func (s *DNSName) CheckChanges(a, e, changes *DNSName) error { func (_ *DNSName) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *DNSName) error { rrs := &route53.ResourceRecordSet{ Name: e.Name, - Type: aws.String(e.ResourceType), + Type: e.ResourceType, } if e.TargetLoadBalancer != nil { @@ -128,3 +133,41 @@ func (_ *DNSName) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *DNSName) error return nil } + +type terraformRoute53Record struct { + Name *string `json:"name"` + Type *string `json:"type"` + TTL *string `json:"ttl"` + Records []string `json:"records"` + + Alias *terraformAlias `json:"alias"` + ZoneID *terraform.Literal `json:"zone_id"` +} + +type terraformAlias struct { + Name *string `json:"name"` + HostedZoneId *string `json:"zone_id"` + EvaluateTargetHealth *bool `json:"evaluate_target_health"` +} + +func (_ *DNSName) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *DNSName) error { + tf := &terraformRoute53Record{ + Name: e.Name, + ZoneID: e.Zone.TerraformLink(), + Type: e.ResourceType, + } + + if e.TargetLoadBalancer != nil { + tf.Alias = &terraformAlias{ + Name: e.TargetLoadBalancer.DNSName, + EvaluateTargetHealth: aws.Bool(false), + HostedZoneId: e.TargetLoadBalancer.HostedZoneId, + } + } + + return t.RenderResource("aws_route53_record", *e.Name, tf) +} + +func (e *DNSName) TerraformLink() *terraform.Literal { + return terraform.LiteralSelfLink("aws_route53_record", *e.Name) +} diff --git a/upup/pkg/fi/cloudup/awstasks/dns_zone.go b/upup/pkg/fi/cloudup/awstasks/dnszone.go similarity index 58% rename from upup/pkg/fi/cloudup/awstasks/dns_zone.go rename to upup/pkg/fi/cloudup/awstasks/dnszone.go index b561e77c3..4506a4b58 100644 --- a/upup/pkg/fi/cloudup/awstasks/dns_zone.go +++ b/upup/pkg/fi/cloudup/awstasks/dnszone.go @@ -8,6 +8,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" "strings" ) @@ -26,6 +27,27 @@ func (e *DNSZone) CompareWithID() *string { func (e *DNSZone) Find(c *fi.Context) (*DNSZone, error) { cloud := c.Cloud.(*awsup.AWSCloud) + z, err := e.findExisting(cloud) + if err != nil { + return nil, err + } + + if z == nil { + return nil, nil + } + + actual := &DNSZone{} + actual.Name = e.Name + actual.ID = z.Id + + if e.ID == nil { + e.ID = actual.ID + } + + return actual, nil +} + +func (e *DNSZone) findExisting(cloud *awsup.AWSCloud) (*route53.HostedZone, error) { findName := fi.StringValue(e.Name) if findName == "" { return nil, nil @@ -55,17 +77,7 @@ func (e *DNSZone) Find(c *fi.Context) (*DNSZone, error) { return nil, fmt.Errorf("found multiple hosted zones matched name %q", findName) } - z := zones[0] - - actual := &DNSZone{} - actual.Name = e.Name - actual.ID = z.Id - - if e.ID == nil { - e.ID = actual.ID - } - - return actual, nil + return zones[0], nil } func (e *DNSZone) Run(c *fi.Context) error { @@ -94,5 +106,47 @@ func (_ *DNSZone) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *DNSZone) error e.ID = response.HostedZone.Id } + // We don't tag the zone - we expect it to be shared return nil } + +type terraformRoute53Zone struct { + Name *string `json:"name"` + Tags map[string]string `json:"tags,omitempty"` +} + +func (_ *DNSZone) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *DNSZone) error { + cloud := t.Cloud.(*awsup.AWSCloud) + + // As a special case, we check for an existing zone + // It is really painful to have TF create a new one... + // (you have to reconfigure the DNS NS records) + glog.Infof("Check for existing route53 zone to re-use with name %q", *e.Name) + z, err := e.findExisting(cloud) + if err != nil { + return err + } + + if z != nil { + glog.Infof("Existing zone %q found; will configure TF to reuse", aws.StringValue(z.Name)) + + e.ID = z.Id + return nil + } + + tf := &terraformRoute53Zone{ + Name: e.Name, + //Tags: cloud.BuildTags(e.Name, nil), + } + + return t.RenderResource("aws_route53_zone", *e.Name, tf) +} + +func (e *DNSZone) TerraformLink() *terraform.Literal { + if e.ID != nil { + glog.V(4).Infof("reusing existing route53 zone with id %q", *e.ID) + return terraform.LiteralFromStringValue(*e.ID) + } + + return terraform.LiteralSelfLink("aws_route53_zone", *e.Name) +} diff --git a/upup/pkg/fi/cloudup/awstasks/ebs_volume.go b/upup/pkg/fi/cloudup/awstasks/ebsvolume.go similarity index 77% rename from upup/pkg/fi/cloudup/awstasks/ebs_volume.go rename to upup/pkg/fi/cloudup/awstasks/ebsvolume.go index 564ba3b46..4dd60dd9c 100644 --- a/upup/pkg/fi/cloudup/awstasks/ebs_volume.go +++ b/upup/pkg/fi/cloudup/awstasks/ebsvolume.go @@ -7,6 +7,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" ) //go:generate fitask -type=EBSVolume @@ -121,3 +122,27 @@ func (_ *EBSVolume) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *EBSVolume) e return t.AddAWSTags(*e.ID, t.Cloud.BuildTags(e.Name, e.Tags)) } + +type terraformVolume struct { + AvailabilityZone *string `json:"availability_zone"` + Size *int64 `json:"size"` + Type *string `json:"type"` + Tags map[string]string `json:"tags,omitempty"` +} + +func (_ *EBSVolume) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *EBSVolume) error { + cloud := t.Cloud.(*awsup.AWSCloud) + + tf := &terraformVolume{ + AvailabilityZone: e.AvailabilityZone, + Size: e.SizeGB, + Type: e.VolumeType, + Tags: cloud.BuildTags(e.Name, e.Tags), + } + + return t.RenderResource("aws_ebs_volume", *e.Name, tf) +} + +func (e *EBSVolume) TerraformLink() *terraform.Literal { + return terraform.LiteralSelfLink("aws_ebs_volume", *e.Name) +} diff --git a/upup/pkg/fi/cloudup/awstasks/iam_instance_profile.go b/upup/pkg/fi/cloudup/awstasks/iaminstanceprofile.go similarity index 81% rename from upup/pkg/fi/cloudup/awstasks/iam_instance_profile.go rename to upup/pkg/fi/cloudup/awstasks/iaminstanceprofile.go index 34b59f00f..4b8fa9418 100644 --- a/upup/pkg/fi/cloudup/awstasks/iam_instance_profile.go +++ b/upup/pkg/fi/cloudup/awstasks/iaminstanceprofile.go @@ -8,6 +8,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" ) //go:generate fitask -type=IAMInstanceProfile @@ -80,5 +81,15 @@ func (_ *IAMInstanceProfile) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *IAM e.Name = response.InstanceProfile.InstanceProfileName } - return nil //return output.AddAWSTags(cloud.Tags(), v, "vpc") + // TODO: Should we use path as our tag? + return nil // No tags in IAM +} + +func (_ *IAMInstanceProfile) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *IAMInstanceProfile) error { + // Done on IAMInstanceProfileRole + return nil +} + +func (e *IAMInstanceProfile) TerraformLink() *terraform.Literal { + return terraform.LiteralProperty("aws_iam_instance_profile", *e.Name, "id") } diff --git a/upup/pkg/fi/cloudup/awstasks/iam_instance_profile_role.go b/upup/pkg/fi/cloudup/awstasks/iaminstanceprofilerole.go similarity index 78% rename from upup/pkg/fi/cloudup/awstasks/iam_instance_profile_role.go rename to upup/pkg/fi/cloudup/awstasks/iaminstanceprofilerole.go index fb63051d7..688140537 100644 --- a/upup/pkg/fi/cloudup/awstasks/iam_instance_profile_role.go +++ b/upup/pkg/fi/cloudup/awstasks/iaminstanceprofilerole.go @@ -9,6 +9,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" ) type IAMInstanceProfileRole struct { @@ -84,5 +85,20 @@ func (_ *IAMInstanceProfileRole) RenderAWS(t *awsup.AWSAPITarget, a, e, changes } } - return nil //return output.AddAWSTags(cloud.Tags(), v, "vpc") + // TODO: Should we use path as our tag? + return nil // No tags in IAM +} + +type terraformIAMInstanceProfile struct { + Name *string `json:"name"` + Roles []*terraform.Literal `json:"roles"` +} + +func (_ *IAMInstanceProfileRole) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *IAMInstanceProfileRole) error { + tf := &terraformIAMInstanceProfile{ + Name: e.InstanceProfile.Name, + Roles: []*terraform.Literal{e.Role.TerraformLink()}, + } + + return t.RenderResource("aws_iam_instance_profile", *e.InstanceProfile.Name, tf) } diff --git a/upup/pkg/fi/cloudup/awstasks/iam_role.go b/upup/pkg/fi/cloudup/awstasks/iamrole.go similarity index 84% rename from upup/pkg/fi/cloudup/awstasks/iam_role.go rename to upup/pkg/fi/cloudup/awstasks/iamrole.go index ba357b674..0e2b1da17 100644 --- a/upup/pkg/fi/cloudup/awstasks/iam_role.go +++ b/upup/pkg/fi/cloudup/awstasks/iamrole.go @@ -10,6 +10,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" "k8s.io/kubernetes/pkg/util/diff" "net/url" "reflect" @@ -156,5 +157,29 @@ func (_ *IAMRole) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *IAMRole) error } } - return nil //return output.AddAWSTags(cloud.Tags(), v, "vpc") + // TODO: Should we use path as our tag? + return nil // No tags in IAM +} + +type terraformIAMRole struct { + Name *string `json:"name"` + AssumeRolePolicy *terraform.Literal `json:"assume_role_policy"` +} + +func (_ *IAMRole) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *IAMRole) error { + policy, err := t.AddFile("aws_iam_role", *e.Name, "policy", e.RolePolicyDocument) + if err != nil { + return fmt.Errorf("error rendering RolePolicyDocument: %v", err) + } + + tf := &terraformIAMRole{ + Name: e.Name, + AssumeRolePolicy: policy, + } + + return t.RenderResource("aws_iam_role", *e.Name, tf) +} + +func (e *IAMRole) TerraformLink() *terraform.Literal { + return terraform.LiteralProperty("aws_iam_role", *e.Name, "name") } diff --git a/upup/pkg/fi/cloudup/awstasks/iam_role_policy.go b/upup/pkg/fi/cloudup/awstasks/iamrolepolicy.go similarity index 76% rename from upup/pkg/fi/cloudup/awstasks/iam_role_policy.go rename to upup/pkg/fi/cloudup/awstasks/iamrolepolicy.go index 5dd95bd4a..0f703be2a 100644 --- a/upup/pkg/fi/cloudup/awstasks/iam_role_policy.go +++ b/upup/pkg/fi/cloudup/awstasks/iamrolepolicy.go @@ -9,6 +9,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" "k8s.io/kubernetes/pkg/util/diff" "net/url" ) @@ -122,5 +123,31 @@ func (_ *IAMRolePolicy) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *IAMRoleP } } - return nil //return output.AddAWSTags(cloud.Tags(), v, "vpc") + // TODO: Should we use path as our tag? + return nil // No tags in IAM +} + +type terraformIAMRolePolicy struct { + Name *string `json:"name"` + Role *terraform.Literal `json:"role"` + PolicyDocument *terraform.Literal `json:"policy"` +} + +func (_ *IAMRolePolicy) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *IAMRolePolicy) error { + policy, err := t.AddFile("aws_iam_role_policy", *e.Name, "policy", e.PolicyDocument) + if err != nil { + return fmt.Errorf("error rendering PolicyDocument: %v", err) + } + + tf := &terraformIAMRolePolicy{ + Name: e.Name, + Role: e.Role.TerraformLink(), + PolicyDocument: policy, + } + + return t.RenderResource("aws_iam_role_policy", *e.Name, tf) +} + +func (e *IAMRolePolicy) TerraformLink() *terraform.Literal { + return terraform.LiteralSelfLink("aws_iam_role_policy", *e.Name) } diff --git a/upup/pkg/fi/cloudup/awstasks/instance.go b/upup/pkg/fi/cloudup/awstasks/instance.go index 314331840..35329a2dc 100644 --- a/upup/pkg/fi/cloudup/awstasks/instance.go +++ b/upup/pkg/fi/cloudup/awstasks/instance.go @@ -9,6 +9,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" "strings" ) @@ -251,3 +252,7 @@ func (_ *Instance) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Instance) err return t.AddAWSTags(*e.ID, t.Cloud.BuildTags(e.Name, e.Tags)) } + +func (e *Instance) TerraformLink() *terraform.Literal { + return terraform.LiteralSelfLink("aws_instance", *e.Name) +} diff --git a/upup/pkg/fi/cloudup/awstasks/internet_gateway.go b/upup/pkg/fi/cloudup/awstasks/internetgateway.go similarity index 84% rename from upup/pkg/fi/cloudup/awstasks/internet_gateway.go rename to upup/pkg/fi/cloudup/awstasks/internetgateway.go index 52de60cb7..1642e71cd 100644 --- a/upup/pkg/fi/cloudup/awstasks/internet_gateway.go +++ b/upup/pkg/fi/cloudup/awstasks/internetgateway.go @@ -7,6 +7,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" ) //go:generate fitask -type=InternetGateway @@ -81,3 +82,12 @@ func (_ *InternetGateway) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Intern return t.AddAWSTags(*e.ID, t.Cloud.BuildTags(e.Name, nil)) } + +func (_ *InternetGateway) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *InternetGateway) error { + // Rendered in the InternetGatewayAttachment instead + return nil +} + +func (e *InternetGateway) TerraformLink() *terraform.Literal { + return terraform.LiteralProperty("aws_internet_gateway", *e.Name, "id") +} diff --git a/upup/pkg/fi/cloudup/awstasks/internet_gateway_attachment.go b/upup/pkg/fi/cloudup/awstasks/internetgatewayattachment.go similarity index 81% rename from upup/pkg/fi/cloudup/awstasks/internet_gateway_attachment.go rename to upup/pkg/fi/cloudup/awstasks/internetgatewayattachment.go index 6fbc0b7ae..09d469dbf 100644 --- a/upup/pkg/fi/cloudup/awstasks/internet_gateway_attachment.go +++ b/upup/pkg/fi/cloudup/awstasks/internetgatewayattachment.go @@ -8,6 +8,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" ) type InternetGatewayAttachment struct { @@ -97,3 +98,19 @@ func (_ *InternetGatewayAttachment) RenderAWS(t *awsup.AWSAPITarget, a, e, chang return nil // No tags } + +type terraformInternetGateway struct { + VPCID *terraform.Literal `json:"vpc_id"` + Tags map[string]string `json:"tags,omitempty"` +} + +func (_ *InternetGatewayAttachment) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *InternetGatewayAttachment) error { + cloud := t.Cloud.(*awsup.AWSCloud) + + tf := &terraformInternetGateway{ + VPCID: e.VPC.TerraformLink(), + Tags: cloud.BuildTags(e.InternetGateway.Name, nil), + } + + return t.RenderResource("aws_internet_gateway", *e.InternetGateway.Name, tf) +} diff --git a/upup/pkg/fi/cloudup/awstasks/route.go b/upup/pkg/fi/cloudup/awstasks/route.go index 4e06cce34..651003a98 100644 --- a/upup/pkg/fi/cloudup/awstasks/route.go +++ b/upup/pkg/fi/cloudup/awstasks/route.go @@ -8,19 +8,19 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" ) +//go:generate fitask -type=Route type Route struct { + Name *string + RouteTable *RouteTable InternetGateway *InternetGateway Instance *Instance CIDR *string } -func (e *Route) String() string { - return fi.TaskAsString(e) -} - func (e *Route) Find(c *fi.Context) (*Route, error) { cloud := c.Cloud.(*awsup.AWSCloud) @@ -170,3 +170,27 @@ func checkNotNil(s *string) *string { } return s } + +type terraformRoute struct { + RouteTableID *terraform.Literal `json:"route_table_id"` + CIDR *string `json:"destination_cidr_block,omitempty"` + InternetGatewayID *terraform.Literal `json:"gateway_id,omitempty"` + InstanceID *terraform.Literal `json:"instance_id,omitempty"` +} + +func (_ *Route) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Route) error { + tf := &terraformRoute{ + CIDR: e.CIDR, + RouteTableID: e.RouteTable.TerraformLink(), + } + + if e.InternetGateway != nil { + tf.InternetGatewayID = e.InternetGateway.TerraformLink() + } + + if e.Instance != nil { + tf.InstanceID = e.Instance.TerraformLink() + } + + return t.RenderResource("aws_route", *e.Name, tf) +} diff --git a/upup/pkg/fi/cloudup/awstasks/route_fitask.go b/upup/pkg/fi/cloudup/awstasks/route_fitask.go new file mode 100644 index 000000000..ad044b7f0 --- /dev/null +++ b/upup/pkg/fi/cloudup/awstasks/route_fitask.go @@ -0,0 +1,43 @@ +// Code generated by ""fitask" -type=Route"; DO NOT EDIT + +package awstasks + +import ( + "encoding/json" + + "k8s.io/kube-deploy/upup/pkg/fi" +) + +// Route + +// JSON marshalling boilerplate +type realRoute Route + +func (o *Route) UnmarshalJSON(data []byte) error { + var jsonName string + if err := json.Unmarshal(data, &jsonName); err == nil { + o.Name = &jsonName + return nil + } + + var r realRoute + if err := json.Unmarshal(data, &r); err != nil { + return err + } + *o = Route(r) + return nil +} + +var _ fi.HasName = &Route{} + +func (e *Route) GetName() *string { + return e.Name +} + +func (e *Route) SetName(name string) { + e.Name = &name +} + +func (e *Route) String() string { + return fi.TaskAsString(e) +} diff --git a/upup/pkg/fi/cloudup/awstasks/route_table.go b/upup/pkg/fi/cloudup/awstasks/routetable.go similarity index 77% rename from upup/pkg/fi/cloudup/awstasks/route_table.go rename to upup/pkg/fi/cloudup/awstasks/routetable.go index cfb68d4aa..30e065566 100644 --- a/upup/pkg/fi/cloudup/awstasks/route_table.go +++ b/upup/pkg/fi/cloudup/awstasks/routetable.go @@ -7,6 +7,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" ) //go:generate fitask -type=RouteTable @@ -98,3 +99,23 @@ func (_ *RouteTable) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *RouteTable) return t.AddAWSTags(*e.ID, t.Cloud.BuildTags(e.Name, nil)) } + +type terraformRouteTable struct { + VPCID *terraform.Literal `json:"vpc_id"` + Tags map[string]string `json:"tags,omitempty"` +} + +func (_ *RouteTable) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *RouteTable) error { + cloud := t.Cloud.(*awsup.AWSCloud) + + tf := &terraformRouteTable{ + VPCID: e.VPC.TerraformLink(), + Tags: cloud.BuildTags(e.Name, nil), + } + + return t.RenderResource("aws_route_table", *e.Name, tf) +} + +func (e *RouteTable) TerraformLink() *terraform.Literal { + return terraform.LiteralProperty("aws_route_table", *e.Name, "id") +} diff --git a/upup/pkg/fi/cloudup/awstasks/route_table_association.go b/upup/pkg/fi/cloudup/awstasks/routetableassociation.go similarity index 77% rename from upup/pkg/fi/cloudup/awstasks/route_table_association.go rename to upup/pkg/fi/cloudup/awstasks/routetableassociation.go index ab96e17f8..fcca54e33 100644 --- a/upup/pkg/fi/cloudup/awstasks/route_table_association.go +++ b/upup/pkg/fi/cloudup/awstasks/routetableassociation.go @@ -8,9 +8,13 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" ) +//go:generate fitask -type=RouteTableAssociation type RouteTableAssociation struct { + Name *string + ID *string RouteTable *RouteTable Subnet *Subnet @@ -20,10 +24,6 @@ func (s *RouteTableAssociation) CompareWithID() *string { return s.ID } -func (e *RouteTableAssociation) String() string { - return fi.TaskAsString(e) -} - func (e *RouteTableAssociation) Find(c *fi.Context) (*RouteTableAssociation, error) { cloud := c.Cloud.(*awsup.AWSCloud) @@ -110,3 +110,21 @@ func (_ *RouteTableAssociation) RenderAWS(t *awsup.AWSAPITarget, a, e, changes * return nil // no tags } + +type terraformRouteTableAssociation struct { + SubnetID *terraform.Literal `json:"subnet_id"` + RouteTableID *terraform.Literal `json:"route_table_id"` +} + +func (_ *RouteTableAssociation) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *RouteTableAssociation) error { + tf := &terraformRouteTableAssociation{ + SubnetID: e.Subnet.TerraformLink(), + RouteTableID: e.RouteTable.TerraformLink(), + } + + return t.RenderResource("aws_route_table_association", *e.Name, tf) +} + +func (e *RouteTableAssociation) TerraformLink() *terraform.Literal { + return terraform.LiteralSelfLink("aws_route_table_association", *e.Name) +} diff --git a/upup/pkg/fi/cloudup/awstasks/routetableassociation_fitask.go b/upup/pkg/fi/cloudup/awstasks/routetableassociation_fitask.go new file mode 100644 index 000000000..3e9724dae --- /dev/null +++ b/upup/pkg/fi/cloudup/awstasks/routetableassociation_fitask.go @@ -0,0 +1,43 @@ +// Code generated by ""fitask" -type=RouteTableAssociation"; DO NOT EDIT + +package awstasks + +import ( + "encoding/json" + + "k8s.io/kube-deploy/upup/pkg/fi" +) + +// RouteTableAssociation + +// JSON marshalling boilerplate +type realRouteTableAssociation RouteTableAssociation + +func (o *RouteTableAssociation) UnmarshalJSON(data []byte) error { + var jsonName string + if err := json.Unmarshal(data, &jsonName); err == nil { + o.Name = &jsonName + return nil + } + + var r realRouteTableAssociation + if err := json.Unmarshal(data, &r); err != nil { + return err + } + *o = RouteTableAssociation(r) + return nil +} + +var _ fi.HasName = &RouteTableAssociation{} + +func (e *RouteTableAssociation) GetName() *string { + return e.Name +} + +func (e *RouteTableAssociation) SetName(name string) { + e.Name = &name +} + +func (e *RouteTableAssociation) String() string { + return fi.TaskAsString(e) +} diff --git a/upup/pkg/fi/cloudup/awstasks/security_group_ingress.go b/upup/pkg/fi/cloudup/awstasks/security_group_ingress.go deleted file mode 100644 index 43ceea2c9..000000000 --- a/upup/pkg/fi/cloudup/awstasks/security_group_ingress.go +++ /dev/null @@ -1,205 +0,0 @@ -package awstasks - -import ( - "fmt" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ec2" - "github.com/golang/glog" - "k8s.io/kube-deploy/upup/pkg/fi" - "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" -) - -type SecurityGroupIngress struct { - SecurityGroup *SecurityGroup - CIDR *string - Protocol *string - FromPort *int64 - ToPort *int64 - SourceGroup *SecurityGroup -} - -func (e *SecurityGroupIngress) String() string { - return fi.TaskAsString(e) -} - -//func (s *SecurityGroupIngress) Key() string { -// key := s.SecurityGroup.Key() -// if s.Protocol != nil { -// key += "-" + *s.Protocol -// } -// if s.FromPort != nil { -// key += "-" + strconv.FormatInt(*s.FromPort, 10) -// } -// if s.ToPort != nil { -// key += "-" + strconv.FormatInt(*s.ToPort, 10) -// } -// if s.CIDR != nil { -// key += "-" + *s.CIDR -// } -// if s.SourceGroup != nil { -// key += "-" + s.SourceGroup.Key() -// } -// return key -//} - -func (e *SecurityGroupIngress) Find(c *fi.Context) (*SecurityGroupIngress, error) { - cloud := c.Cloud.(*awsup.AWSCloud) - - if e.SecurityGroup == nil || e.SecurityGroup.ID == nil { - return nil, nil - } - - request := &ec2.DescribeSecurityGroupsInput{ - Filters: []*ec2.Filter{ - awsup.NewEC2Filter("group-id", *e.SecurityGroup.ID), - }, - } - - response, err := cloud.EC2.DescribeSecurityGroups(request) - if err != nil { - return nil, fmt.Errorf("error listing SecurityGroup: %v", err) - } - - if response == nil || len(response.SecurityGroups) == 0 { - return nil, nil - } - - if len(response.SecurityGroups) != 1 { - glog.Fatalf("found multiple security groups for id=%s", *e.SecurityGroup.ID) - } - sg := response.SecurityGroups[0] - //glog.V(2).Info("found existing security group") - - var foundRule *ec2.IpPermission - - matchProtocol := "-1" // Wildcard - if e.Protocol != nil { - matchProtocol = *e.Protocol - } - - for _, rule := range sg.IpPermissions { - if aws.Int64Value(rule.FromPort) != aws.Int64Value(e.FromPort) { - continue - } - if aws.Int64Value(rule.ToPort) != aws.Int64Value(e.ToPort) { - continue - } - if aws.StringValue(rule.IpProtocol) != matchProtocol { - continue - } - if e.CIDR != nil { - // TODO: Only if len 1? - match := false - for _, ipRange := range rule.IpRanges { - if aws.StringValue(ipRange.CidrIp) == *e.CIDR { - match = true - break - } - } - if !match { - continue - } - } - - if e.SourceGroup != nil { - // TODO: Only if len 1? - match := false - for _, spec := range rule.UserIdGroupPairs { - if aws.StringValue(spec.GroupId) == *e.SourceGroup.ID { - match = true - break - } - } - if !match { - continue - } - } - foundRule = rule - break - } - - if foundRule != nil { - actual := &SecurityGroupIngress{ - SecurityGroup: &SecurityGroup{ID: e.SecurityGroup.ID}, - FromPort: foundRule.FromPort, - ToPort: foundRule.ToPort, - Protocol: foundRule.IpProtocol, - } - - if aws.StringValue(actual.Protocol) == "-1" { - actual.Protocol = nil - } - if e.CIDR != nil { - actual.CIDR = e.CIDR - } - if e.SourceGroup != nil { - actual.SourceGroup = &SecurityGroup{ID: e.SourceGroup.ID} - } - return actual, nil - } - - return nil, nil -} - -func (e *SecurityGroupIngress) Run(c *fi.Context) error { - return fi.DefaultDeltaRunMethod(e, c) -} - -func (_ *SecurityGroupIngress) CheckChanges(a, e, changes *SecurityGroupIngress) error { - if a == nil { - if e.SecurityGroup == nil { - return fi.RequiredField("SecurityGroup") - } - } - return nil -} - -func (_ *SecurityGroupIngress) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *SecurityGroupIngress) error { - if a == nil { - request := &ec2.AuthorizeSecurityGroupIngressInput{ - GroupId: e.SecurityGroup.ID, - } - - protocol := e.Protocol - if protocol == nil { - protocol = aws.String("-1") - } - - if e.SourceGroup != nil { - request.IpPermissions = []*ec2.IpPermission{ - { - IpProtocol: protocol, - UserIdGroupPairs: []*ec2.UserIdGroupPair{ - { - GroupId: e.SourceGroup.ID, - }, - }, - FromPort: e.FromPort, - ToPort: e.ToPort, - }, - } - } else { - request.IpPermissions = []*ec2.IpPermission{ - { - IpProtocol: protocol, - FromPort: e.FromPort, - ToPort: e.ToPort, - IpRanges: []*ec2.IpRange{ - {CidrIp: e.CIDR}, - }, - }, - } - } - - glog.V(2).Infof("Calling EC2 AuthorizeSecurityGroupIngress") - _, err := t.Cloud.EC2.AuthorizeSecurityGroupIngress(request) - if err != nil { - return fmt.Errorf("error creating SecurityGroupIngress: %v", err) - } - } - - // No tags on ingress rules (there are tags on the group though) - - return nil -} diff --git a/upup/pkg/fi/cloudup/awstasks/security_group.go b/upup/pkg/fi/cloudup/awstasks/securitygroup.go similarity index 75% rename from upup/pkg/fi/cloudup/awstasks/security_group.go rename to upup/pkg/fi/cloudup/awstasks/securitygroup.go index b47c7cdf7..7ccde1d25 100644 --- a/upup/pkg/fi/cloudup/awstasks/security_group.go +++ b/upup/pkg/fi/cloudup/awstasks/securitygroup.go @@ -7,6 +7,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" ) //go:generate fitask -type=SecurityGroup @@ -108,3 +109,27 @@ func (_ *SecurityGroup) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Security return t.AddAWSTags(*e.ID, t.Cloud.BuildTags(e.Name, nil)) } + +type terraformSecurityGroup struct { + Name *string `json:"name"` + VPCID *terraform.Literal `json:"vpc_id"` + Description *string `json:"description"` + Tags map[string]string `json:"tags,omitempty"` +} + +func (_ *SecurityGroup) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *SecurityGroup) error { + cloud := t.Cloud.(*awsup.AWSCloud) + + tf := &terraformSecurityGroup{ + Name: e.Name, + VPCID: e.VPC.TerraformLink(), + Description: e.Description, + Tags: cloud.BuildTags(e.Name, nil), + } + + return t.RenderResource("aws_security_group", *e.Name, tf) +} + +func (e *SecurityGroup) TerraformLink() *terraform.Literal { + return terraform.LiteralProperty("aws_security_group", *e.Name, "id") +} diff --git a/upup/pkg/fi/cloudup/awstasks/securitygrouprule.go b/upup/pkg/fi/cloudup/awstasks/securitygrouprule.go new file mode 100644 index 000000000..21573f1a2 --- /dev/null +++ b/upup/pkg/fi/cloudup/awstasks/securitygrouprule.go @@ -0,0 +1,252 @@ +package awstasks + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/golang/glog" + "k8s.io/kube-deploy/upup/pkg/fi" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" +) + +//go:generate fitask -type=SecurityGroupRule +type SecurityGroupRule struct { + Name *string + + SecurityGroup *SecurityGroup + CIDR *string + Protocol *string + FromPort *int64 + ToPort *int64 + SourceGroup *SecurityGroup + + Egress *bool +} + +func (e *SecurityGroupRule) Find(c *fi.Context) (*SecurityGroupRule, error) { + cloud := c.Cloud.(*awsup.AWSCloud) + + if e.SecurityGroup == nil || e.SecurityGroup.ID == nil { + return nil, nil + } + + request := &ec2.DescribeSecurityGroupsInput{ + Filters: []*ec2.Filter{ + awsup.NewEC2Filter("group-id", *e.SecurityGroup.ID), + }, + } + + response, err := cloud.EC2.DescribeSecurityGroups(request) + if err != nil { + return nil, fmt.Errorf("error listing SecurityGroup: %v", err) + } + + if response == nil || len(response.SecurityGroups) == 0 { + return nil, nil + } + + if len(response.SecurityGroups) != 1 { + glog.Fatalf("found multiple security groups for id=%s", *e.SecurityGroup.ID) + } + sg := response.SecurityGroups[0] + //glog.V(2).Info("found existing security group") + + var foundRule *ec2.IpPermission + + matchProtocol := "-1" // Wildcard + if e.Protocol != nil { + matchProtocol = *e.Protocol + } + + ipPermissions := sg.IpPermissions + if fi.BoolValue(e.Egress) { + ipPermissions = sg.IpPermissionsEgress + } + + for _, rule := range ipPermissions { + if aws.Int64Value(rule.FromPort) != aws.Int64Value(e.FromPort) { + continue + } + if aws.Int64Value(rule.ToPort) != aws.Int64Value(e.ToPort) { + continue + } + if aws.StringValue(rule.IpProtocol) != matchProtocol { + continue + } + if e.CIDR != nil { + // TODO: Only if len 1? + match := false + for _, ipRange := range rule.IpRanges { + if aws.StringValue(ipRange.CidrIp) == *e.CIDR { + match = true + break + } + } + if !match { + continue + } + } + + if e.SourceGroup != nil { + // TODO: Only if len 1? + match := false + for _, spec := range rule.UserIdGroupPairs { + if aws.StringValue(spec.GroupId) == *e.SourceGroup.ID { + match = true + break + } + } + if !match { + continue + } + } + foundRule = rule + break + } + + if foundRule != nil { + actual := &SecurityGroupRule{ + SecurityGroup: &SecurityGroup{ID: e.SecurityGroup.ID}, + FromPort: foundRule.FromPort, + ToPort: foundRule.ToPort, + Protocol: foundRule.IpProtocol, + Egress: e.Egress, + } + + if aws.StringValue(actual.Protocol) == "-1" { + actual.Protocol = nil + } + if e.CIDR != nil { + actual.CIDR = e.CIDR + } + if e.SourceGroup != nil { + actual.SourceGroup = &SecurityGroup{ID: e.SourceGroup.ID} + } + return actual, nil + } + + return nil, nil +} + +func (e *SecurityGroupRule) Run(c *fi.Context) error { + return fi.DefaultDeltaRunMethod(e, c) +} + +func (_ *SecurityGroupRule) CheckChanges(a, e, changes *SecurityGroupRule) error { + if a == nil { + if e.SecurityGroup == nil { + return fi.RequiredField("SecurityGroup") + } + } + return nil +} + +func (_ *SecurityGroupRule) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *SecurityGroupRule) error { + if a == nil { + + protocol := e.Protocol + if protocol == nil { + protocol = aws.String("-1") + } + + ipPermission := &ec2.IpPermission{ + IpProtocol: protocol, + FromPort: e.FromPort, + ToPort: e.ToPort, + } + + if e.SourceGroup != nil { + ipPermission.UserIdGroupPairs = []*ec2.UserIdGroupPair{ + { + GroupId: e.SourceGroup.ID, + }, + } + } else { + // Default to 0.0.0.0/0 ? + ipPermission.IpRanges = []*ec2.IpRange{ + {CidrIp: e.CIDR}, + } + } + + if fi.BoolValue(e.Egress) { + request := &ec2.AuthorizeSecurityGroupEgressInput{ + GroupId: e.SecurityGroup.ID, + } + request.IpPermissions = []*ec2.IpPermission{ipPermission} + + glog.V(2).Infof("Calling EC2 AuthorizeSecurityGroupEgress") + _, err := t.Cloud.EC2.AuthorizeSecurityGroupEgress(request) + if err != nil { + return fmt.Errorf("error creating SecurityGroupEgress: %v", err) + } + } else { + request := &ec2.AuthorizeSecurityGroupIngressInput{ + GroupId: e.SecurityGroup.ID, + } + request.IpPermissions = []*ec2.IpPermission{ipPermission} + + glog.V(2).Infof("Calling EC2 AuthorizeSecurityGroupIngress") + _, err := t.Cloud.EC2.AuthorizeSecurityGroupIngress(request) + if err != nil { + return fmt.Errorf("error creating SecurityGroupIngress: %v", err) + } + } + + } + + // No tags on security group rules (there are tags on the group though) + + return nil +} + +type terraformSecurityGroupIngress struct { + Type *string `json:"type"` + + SecurityGroup *terraform.Literal `json:"security_group_id"` + SourceGroup *terraform.Literal `json:"source_security_group_id,omitempty"` + + FromPort *int64 `json:"from_port,omitempty"` + ToPort *int64 `json:"to_port,omitempty"` + + Protocol *string `json:"protocol,omitempty"` + CIDRBlocks []string `json:"cidr_blocks,omitempty"` +} + +func (_ *SecurityGroupRule) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *SecurityGroupRule) error { + tf := &terraformSecurityGroupIngress{ + Type: fi.String("ingress"), + SecurityGroup: e.SecurityGroup.TerraformLink(), + FromPort: e.FromPort, + ToPort: e.ToPort, + Protocol: e.Protocol, + } + if fi.BoolValue(e.Egress) { + tf.Type = fi.String("egress") + } + + if e.Protocol == nil { + tf.Protocol = fi.String("-1") + tf.FromPort = fi.Int64(0) + tf.ToPort = fi.Int64(0) + } + + if tf.FromPort == nil { + // FromPort is required by tf + tf.FromPort = fi.Int64(0) + } + if tf.ToPort == nil { + // ToPort is required by tf + tf.ToPort = fi.Int64(65535) + } + + if e.SourceGroup != nil { + tf.SourceGroup = e.SourceGroup.TerraformLink() + } + + if e.CIDR != nil { + tf.CIDRBlocks = append(tf.CIDRBlocks, *e.CIDR) + } + return t.RenderResource("aws_security_group_rule", *e.Name, tf) +} diff --git a/upup/pkg/fi/cloudup/awstasks/securitygrouprule_fitask.go b/upup/pkg/fi/cloudup/awstasks/securitygrouprule_fitask.go new file mode 100644 index 000000000..6c0d372d1 --- /dev/null +++ b/upup/pkg/fi/cloudup/awstasks/securitygrouprule_fitask.go @@ -0,0 +1,43 @@ +// Code generated by ""fitask" -type=SecurityGroupRule"; DO NOT EDIT + +package awstasks + +import ( + "encoding/json" + + "k8s.io/kube-deploy/upup/pkg/fi" +) + +// SecurityGroupRule + +// JSON marshalling boilerplate +type realSecurityGroupRule SecurityGroupRule + +func (o *SecurityGroupRule) UnmarshalJSON(data []byte) error { + var jsonName string + if err := json.Unmarshal(data, &jsonName); err == nil { + o.Name = &jsonName + return nil + } + + var r realSecurityGroupRule + if err := json.Unmarshal(data, &r); err != nil { + return err + } + *o = SecurityGroupRule(r) + return nil +} + +var _ fi.HasName = &SecurityGroupRule{} + +func (e *SecurityGroupRule) GetName() *string { + return e.Name +} + +func (e *SecurityGroupRule) SetName(name string) { + e.Name = &name +} + +func (e *SecurityGroupRule) String() string { + return fi.TaskAsString(e) +} diff --git a/upup/pkg/fi/cloudup/awstasks/ssh_key.go b/upup/pkg/fi/cloudup/awstasks/sshkey.go similarity index 88% rename from upup/pkg/fi/cloudup/awstasks/ssh_key.go rename to upup/pkg/fi/cloudup/awstasks/sshkey.go index c59a39429..dfc7d4042 100644 --- a/upup/pkg/fi/cloudup/awstasks/ssh_key.go +++ b/upup/pkg/fi/cloudup/awstasks/sshkey.go @@ -16,6 +16,7 @@ import ( "golang.org/x/crypto/ssh" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" "k8s.io/kube-deploy/upup/pkg/fi/utils" "reflect" "strings" @@ -196,3 +197,26 @@ func (_ *SSHKey) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *SSHKey) error { return nil //return output.AddAWSTags(cloud.Tags(), v, "vpc") } + +type terraformSSHKey struct { + Name *string `json:"key_name"` + PublicKey *terraform.Literal `json:"public_key"` +} + +func (_ *SSHKey) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *SSHKey) error { + publicKey, err := t.AddFile("aws_key_pair", *e.Name, "public_key", e.PublicKey) + if err != nil { + return fmt.Errorf("error rendering PublicKey: %v", err) + } + + tf := &terraformSSHKey{ + Name: e.Name, + PublicKey: publicKey, + } + + return t.RenderResource("aws_key_pair", *e.Name, tf) +} + +func (e *SSHKey) TerraformLink() *terraform.Literal { + return terraform.LiteralProperty("aws_key_pair", *e.Name, "id") +} diff --git a/upup/pkg/fi/cloudup/awstasks/subnet.go b/upup/pkg/fi/cloudup/awstasks/subnet.go index 44ab219ed..af41148d9 100644 --- a/upup/pkg/fi/cloudup/awstasks/subnet.go +++ b/upup/pkg/fi/cloudup/awstasks/subnet.go @@ -7,6 +7,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" "k8s.io/kube-deploy/upup/pkg/fi/utils" ) @@ -127,3 +128,27 @@ func subnetSlicesEqualIgnoreOrder(l, r []*Subnet) bool { } return utils.StringSlicesEqualIgnoreOrder(lIDs, rIDs) } + +type terraformSubnet struct { + VPCID *terraform.Literal `json:"vpc_id"` + CIDR *string `json:"cidr_block"` + AvailabilityZone *string `json:"availability_zone"` + Tags map[string]string `json:"tags,omitempty"` +} + +func (_ *Subnet) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Subnet) error { + cloud := t.Cloud.(*awsup.AWSCloud) + + tf := &terraformSubnet{ + VPCID: e.VPC.TerraformLink(), + CIDR: e.CIDR, + AvailabilityZone: e.AvailabilityZone, + Tags: cloud.BuildTags(e.Name, nil), + } + + return t.RenderResource("aws_subnet", *e.Name, tf) +} + +func (e *Subnet) TerraformLink() *terraform.Literal { + return terraform.LiteralProperty("aws_subnet", *e.Name, "id") +} diff --git a/upup/pkg/fi/cloudup/awstasks/vpc.go b/upup/pkg/fi/cloudup/awstasks/vpc.go index 779bf31f2..06d11e150 100644 --- a/upup/pkg/fi/cloudup/awstasks/vpc.go +++ b/upup/pkg/fi/cloudup/awstasks/vpc.go @@ -8,6 +8,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" ) //go:generate fitask -type=VPC @@ -140,3 +141,27 @@ func (_ *VPC) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *VPC) error { return t.AddAWSTags(*e.ID, t.Cloud.BuildTags(e.Name, nil)) } + +type terraformVPC struct { + CIDR *string `json:"cidr_block,omitempty"` + EnableDNSHostnames *bool `json:"enable_dns_hostnames,omitempty"` + EnableDNSSupport *bool `json:"enable_dns_support,omitempty"` + Tags map[string]string `json:"tags,omitempty"` +} + +func (_ *VPC) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *VPC) error { + cloud := t.Cloud.(*awsup.AWSCloud) + + tf := &terraformVPC{ + CIDR: e.CIDR, + Tags: cloud.BuildTags(e.Name, nil), + EnableDNSHostnames: e.EnableDNSHostnames, + EnableDNSSupport: e.EnableDNSSupport, + } + + return t.RenderResource("aws_vpc", *e.Name, tf) +} + +func (e *VPC) TerraformLink() *terraform.Literal { + return terraform.LiteralProperty("aws_vpc", *e.Name, "id") +} diff --git a/upup/pkg/fi/cloudup/awstasks/vpc_dhcpoptions_association.go b/upup/pkg/fi/cloudup/awstasks/vpc_dhcpoptions_association.go index 7802b27a3..39532ecfa 100644 --- a/upup/pkg/fi/cloudup/awstasks/vpc_dhcpoptions_association.go +++ b/upup/pkg/fi/cloudup/awstasks/vpc_dhcpoptions_association.go @@ -7,6 +7,7 @@ import ( "github.com/golang/glog" "k8s.io/kube-deploy/upup/pkg/fi" "k8s.io/kube-deploy/upup/pkg/fi/cloudup/awsup" + "k8s.io/kube-deploy/upup/pkg/fi/cloudup/terraform" ) type VPCDHCPOptionsAssociation struct { @@ -77,3 +78,17 @@ func (_ *VPCDHCPOptionsAssociation) RenderAWS(t *awsup.AWSAPITarget, a, e, chang return nil // no tags } + +type terraformVPCDHCPOptionsAssociation struct { + VPCID *terraform.Literal `json:"vpc_id"` + DHCPOptionsID *terraform.Literal `json:"dhcp_options_id"` +} + +func (_ *VPCDHCPOptionsAssociation) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *VPCDHCPOptionsAssociation) error { + tf := &terraformVPCDHCPOptionsAssociation{ + VPCID: e.VPC.TerraformLink(), + DHCPOptionsID: e.DHCPOptions.TerraformLink(), + } + + return t.RenderResource("aws_vpc_dhcp_options_association", *e.VPC.Name, tf) +} diff --git a/upup/pkg/fi/cloudup/gce/gce_apitarget.go b/upup/pkg/fi/cloudup/gce/gce_apitarget.go index bf52f02a4..e402bc83e 100644 --- a/upup/pkg/fi/cloudup/gce/gce_apitarget.go +++ b/upup/pkg/fi/cloudup/gce/gce_apitarget.go @@ -1,6 +1,8 @@ package gce -import "k8s.io/kube-deploy/upup/pkg/fi" +import ( + "k8s.io/kube-deploy/upup/pkg/fi" +) type GCEAPITarget struct { Cloud *GCECloud diff --git a/upup/pkg/fi/cloudup/loader.go b/upup/pkg/fi/cloudup/loader.go index e0579c2a9..4c5efc129 100644 --- a/upup/pkg/fi/cloudup/loader.go +++ b/upup/pkg/fi/cloudup/loader.go @@ -349,8 +349,8 @@ func (l *Loader) loadObjectMap(key string, data map[string]interface{}) (map[str inferredName := false if name == "" { - lastSlash := strings.LastIndex(k, "/") - name = k[lastSlash+1:] + firstSlash := strings.Index(k, "/") + name = k[firstSlash+1:] inferredName = true } diff --git a/upup/pkg/fi/cloudup/terraform/hcl_printer.go b/upup/pkg/fi/cloudup/terraform/hcl_printer.go new file mode 100644 index 000000000..bca85ee17 --- /dev/null +++ b/upup/pkg/fi/cloudup/terraform/hcl_printer.go @@ -0,0 +1,83 @@ +package terraform + +import ( + "bytes" + "fmt" + "github.com/golang/glog" + "github.com/hashicorp/hcl/hcl/ast" + hcl_printer "github.com/hashicorp/hcl/hcl/printer" + "strings" +) + +const safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" + +// sanitizer fixes up an invalid HCL AST, as produced by the HCL parser for JSON +type sanitizer struct { +} + +// output prints creates b printable HCL output and returns it. +func (v *sanitizer) visit(n interface{}) { + switch t := n.(type) { + case *ast.File: + v.visit(t.Node) + case *ast.ObjectList: + var index int + for { + if index == len(t.Items) { + break + } + + v.visit(t.Items[index]) + index++ + } + case *ast.ObjectKey: + case *ast.ObjectItem: + v.visitObjectItem(t) + case *ast.LiteralType: + case *ast.ListType: + case *ast.ObjectType: + v.visit(t.List) + default: + glog.Warningf(" unknown type: %T\n", n) + } + +} + +func (v *sanitizer) visitObjectItem(o *ast.ObjectItem) { + for i, k := range o.Keys { + if i == 0 { + text := k.Token.Text + if text != "" && text[0] == '"' && text[len(text)-1] == '"' { + v := text[1 : len(text)-1] + safe := true + for _, c := range v { + if strings.IndexRune(safeChars, c) == -1 { + safe = false + break + } + } + if safe { + k.Token.Text = v + } + } + + } + } + + // A hack so that Assign.IsValid is true, so that the printer will output = + o.Assign.Line = 1 + + v.visit(o.Val) +} + +func hclPrint(node ast.Node) ([]byte, error) { + var s sanitizer + s.visit(node) + + var b bytes.Buffer + err := hcl_printer.Fprint(&b, node) + if err != nil { + return nil, fmt.Errorf("error writing HCL: %v", err) + } + return b.Bytes(), nil +} diff --git a/upup/pkg/fi/cloudup/terraform/literal.go b/upup/pkg/fi/cloudup/terraform/literal.go index f5d048693..3e713bef5 100644 --- a/upup/pkg/fi/cloudup/terraform/literal.go +++ b/upup/pkg/fi/cloudup/terraform/literal.go @@ -21,7 +21,9 @@ func LiteralSelfLink(resourceType, resourceName string) *Literal { } func LiteralProperty(resourceType, resourceName, prop string) *Literal { - expr := "${" + resourceType + "." + resourceName + "." + prop + "}" + tfName := tfSanitize(resourceName) + + expr := "${" + resourceType + "." + tfName + "." + prop + "}" return LiteralExpression(expr) } diff --git a/upup/pkg/fi/cloudup/terraform/target.go b/upup/pkg/fi/cloudup/terraform/target.go index 6795d618c..027000846 100644 --- a/upup/pkg/fi/cloudup/terraform/target.go +++ b/upup/pkg/fi/cloudup/terraform/target.go @@ -3,23 +3,32 @@ package terraform import ( "encoding/json" "fmt" - "io" + hcl_parser "github.com/hashicorp/hcl/json/parser" + "io/ioutil" "k8s.io/kube-deploy/upup/pkg/fi" + "os" + "path" + "strings" ) type TerraformTarget struct { - Region string - Project string + Cloud fi.Cloud + Region string + Project string + resources []*terraformResource - out io.Writer + files map[string][]byte + outDir string } -func NewTerraformTarget(region, project string, out io.Writer) *TerraformTarget { +func NewTerraformTarget(cloud fi.Cloud, region, project string, outDir string) *TerraformTarget { return &TerraformTarget{ + Cloud: cloud, Region: region, Project: project, - out: out, + outDir: outDir, + files: make(map[string][]byte), } } @@ -31,6 +40,29 @@ type terraformResource struct { Item interface{} } +// A TF name can't have dots in it (if we want to refer to it from a literal), +// so we replace them +func tfSanitize(name string) string { + name = strings.Replace(name, ".", "-", -1) + name = strings.Replace(name, "/", "--", -1) + return name +} + +func (t *TerraformTarget) AddFile(resourceType string, resourceName string, key string, r fi.Resource) (*Literal, error) { + id := resourceType + "_" + resourceName + "_" + key + + d, err := fi.ResourceAsBytes(r) + if err != nil { + return nil, fmt.Errorf("error rending resource %s %v", id, err) + } + + p := path.Join("data", id) + t.files[p] = d + + l := LiteralExpression(fmt.Sprintf("${file(%q)}", p)) + return l, nil +} + func (t *TerraformTarget) RenderResource(resourceType string, resourceName string, e interface{}) error { res := &terraformResource{ ResourceType: resourceType, @@ -53,31 +85,66 @@ func (t *TerraformTarget) Finish(taskMap map[string]fi.Task) error { resourcesByType[res.ResourceType] = resources } - if resources[res.ResourceName] != nil { - return fmt.Errorf("duplicate resource found: %s.%s", res.ResourceType, res.ResourceName) + tfName := tfSanitize(res.ResourceName) + + if resources[tfName] != nil { + return fmt.Errorf("duplicate resource found: %s.%s", res.ResourceType, tfName) } - resources[res.ResourceName] = res.Item + resources[tfName] = res.Item } providersByName := make(map[string]map[string]interface{}) - providerGoogle := make(map[string]interface{}) - providerGoogle["project"] = t.Project - providerGoogle["region"] = t.Region - providersByName["google"] = providerGoogle + if t.Cloud.ProviderID() == fi.CloudProviderGCE { + providerGoogle := make(map[string]interface{}) + providerGoogle["project"] = t.Project + providerGoogle["region"] = t.Region + providersByName["google"] = providerGoogle + } data := make(map[string]interface{}) data["resource"] = resourcesByType - data["provider"] = providersByName + if len(providersByName) != 0 { + data["provider"] = providersByName + } jsonBytes, err := json.MarshalIndent(data, "", " ") if err != nil { return fmt.Errorf("error marshalling terraform data to json: %v", err) } - _, err = t.out.Write(jsonBytes) - if err != nil { - return fmt.Errorf("error writing terraform data to output: %v", err) + useJson := false + + if useJson { + t.files["kubernetes.tf"] = jsonBytes + } else { + f, err := hcl_parser.Parse(jsonBytes) + if err != nil { + return fmt.Errorf("error parsing terraform json: %v", err) + } + + b, err := hclPrint(f) + if err != nil { + return fmt.Errorf("error writing terraform data to output: %v", err) + } + + t.files["kubernetes.tf"] = b + } + + for relativePath, contents := range t.files { + p := path.Join(t.outDir, relativePath) + + err = os.MkdirAll(path.Dir(p), os.FileMode(0755)) + if err != nil { + return fmt.Errorf("error creating output directory %q: %v", path.Dir(p), err) + } + + err = ioutil.WriteFile(p, contents, os.FileMode(0644)) + if err != nil { + return fmt.Errorf("error writing terraform data to output file %q: %v", p, err) + } + } + return nil } diff --git a/upup/pkg/fi/default_methods.go b/upup/pkg/fi/default_methods.go index 86211760c..f8fb704af 100644 --- a/upup/pkg/fi/default_methods.go +++ b/upup/pkg/fi/default_methods.go @@ -11,7 +11,12 @@ func DefaultDeltaRunMethod(e Task, c *Context) error { var a Task var err error - if c.CheckExisting { + checkExisting := c.CheckExisting + if hce, ok := e.(HasCheckExisting); ok { + checkExisting = hce.CheckExisting(c) + } + + if checkExisting { a, err = invokeFind(e, c) if err != nil { return err diff --git a/upup/pkg/fi/fitasks/keypair.go b/upup/pkg/fi/fitasks/keypair.go index ffbbbd9be..205d8e7b7 100644 --- a/upup/pkg/fi/fitasks/keypair.go +++ b/upup/pkg/fi/fitasks/keypair.go @@ -24,13 +24,21 @@ type Keypair struct { AlternateNameTasks []fi.Task `json:"alternateNameTasks"` } +var _ fi.HasCheckExisting = &Keypair{} + +// It's important always to check for the existing key, so we don't regenerate keys e.g. on terraform +func (e *Keypair) CheckExisting(c *fi.Context) bool { + return true +} + func (e *Keypair) Find(c *fi.Context) (*Keypair, error) { + castore := c.CAStore + name := fi.StringValue(e.Name) if name == "" { return nil, nil } - castore := c.CAStore cert, err := castore.FindCert(name) if err != nil { return nil, err diff --git a/upup/pkg/fi/fs_castore.go b/upup/pkg/fi/fs_castore.go index 160346775..46c4ee9ca 100644 --- a/upup/pkg/fi/fs_castore.go +++ b/upup/pkg/fi/fs_castore.go @@ -177,6 +177,8 @@ func (c *FilesystemCAStore) List() ([]string, error) { } func (c *FilesystemCAStore) IssueCert(id string, privateKey *PrivateKey, template *x509.Certificate) (*Certificate, error) { + glog.Infof("Issuing new certificate: %q", id) + p := c.buildCertificatePath(id) if c.caPrivateKey == nil { diff --git a/upup/pkg/fi/task.go b/upup/pkg/fi/task.go index 43591d651..4cfb37868 100644 --- a/upup/pkg/fi/task.go +++ b/upup/pkg/fi/task.go @@ -14,3 +14,7 @@ type Task interface { func TaskAsString(t Task) string { return fmt.Sprintf("%T %s", t, DebugAsJsonString(t)) } + +type HasCheckExisting interface { + CheckExisting(c *Context) bool +}